Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -89,7 +89,6 @@ class Loader(object):
|
||||
if self.runAsDaemon():
|
||||
try: self.daemon.stop()
|
||||
except: pass
|
||||
self.daemon.delpid()
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import cleanHost, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from flask import request
|
||||
from tornado.ioloop import IOLoop
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import platform
|
||||
@@ -18,7 +18,7 @@ log = CPLog(__name__)
|
||||
|
||||
class Core(Plugin):
|
||||
|
||||
ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
|
||||
ignore_restart = ['Core.restart', 'Core.shutdown', 'Updater.check']
|
||||
shutdown_started = False
|
||||
|
||||
def __init__(self):
|
||||
@@ -37,8 +37,8 @@ class Core(Plugin):
|
||||
'desc': 'Get version.'
|
||||
})
|
||||
|
||||
addEvent('app.crappy_shutdown', self.crappyShutdown)
|
||||
addEvent('app.crappy_restart', self.crappyRestart)
|
||||
addEvent('app.shutdown', self.shutdown)
|
||||
addEvent('app.restart', self.restart)
|
||||
addEvent('app.load', self.launchBrowser, priority = 1)
|
||||
addEvent('app.base_url', self.createBaseUrl)
|
||||
addEvent('app.api_url', self.createApiUrl)
|
||||
@@ -59,34 +59,24 @@ class Core(Plugin):
|
||||
'succes': True
|
||||
})
|
||||
|
||||
def crappyShutdown(self):
|
||||
if self.shutdown_started:
|
||||
return
|
||||
|
||||
try:
|
||||
self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
|
||||
return True
|
||||
except:
|
||||
self.initShutdown()
|
||||
return False
|
||||
|
||||
def crappyRestart(self):
|
||||
if self.shutdown_started:
|
||||
return
|
||||
|
||||
try:
|
||||
self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False)
|
||||
return True
|
||||
except:
|
||||
self.initShutdown(restart = True)
|
||||
return False
|
||||
|
||||
def shutdown(self):
|
||||
self.initShutdown()
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
def shutdown():
|
||||
self.initShutdown()
|
||||
IOLoop.instance().add_callback(shutdown)
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
def restart(self):
|
||||
self.initShutdown(restart = True)
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
def restart():
|
||||
self.initShutdown(restart = True)
|
||||
IOLoop.instance().add_callback(restart)
|
||||
|
||||
return 'restarting'
|
||||
|
||||
def initShutdown(self, restart = False):
|
||||
@@ -121,7 +111,8 @@ class Core(Plugin):
|
||||
log.debug('Save to shutdown/restart')
|
||||
|
||||
try:
|
||||
request.environ.get('werkzeug.server.shutdown')()
|
||||
Env.get('httpserver').stop()
|
||||
IOLoop.instance().stop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
except:
|
||||
|
||||
@@ -27,7 +27,7 @@ if Env.get('desktop'):
|
||||
addEvent('app.after_shutdown', desktop.afterShutdown)
|
||||
|
||||
def onClose(self, event):
|
||||
return fireEvent('app.crappy_shutdown', single = True)
|
||||
return fireEvent('app.shutdown', single = True)
|
||||
|
||||
else:
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class Updater(Plugin):
|
||||
if self.updater.check():
|
||||
if self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.updater.doUpdate():
|
||||
fireEventAsync('app.crappy_restart')
|
||||
fireEventAsync('app.restart')
|
||||
else:
|
||||
if self.conf('notification'):
|
||||
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
||||
@@ -338,7 +338,7 @@ class SourceUpdater(BaseUpdater):
|
||||
return {}
|
||||
|
||||
|
||||
class DesktopUpdater(Plugin):
|
||||
class DesktopUpdater(BaseUpdater):
|
||||
|
||||
version = None
|
||||
update_failed = False
|
||||
@@ -350,9 +350,15 @@ class DesktopUpdater(Plugin):
|
||||
|
||||
def doUpdate(self):
|
||||
try:
|
||||
self.desktop.CheckForUpdate(silentUnlessUpdate = True)
|
||||
def do_restart(e):
|
||||
if e['status'] == 'done':
|
||||
fireEventAsync('app.restart')
|
||||
else:
|
||||
log.error('Failed updating desktop: %s' % e['exception'])
|
||||
self.update_failed = True
|
||||
|
||||
self.desktop._esky.auto_update(callback = do_restart)
|
||||
except:
|
||||
log.error('Failed updating desktop: %s' % traceback.format_exc())
|
||||
self.update_failed = True
|
||||
|
||||
return False
|
||||
|
||||
@@ -10,7 +10,7 @@ class Blackhole(Downloader):
|
||||
|
||||
type = ['nzb', 'torrent']
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False):
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
|
||||
return
|
||||
|
||||
@@ -19,10 +19,8 @@ class Blackhole(Downloader):
|
||||
log.error('No directory set for blackhole %s download.' % data.get('type'))
|
||||
else:
|
||||
try:
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
|
||||
if len(filedata) < 50:
|
||||
log.error('No nzb available!')
|
||||
if not filedata or len(filedata) < 50:
|
||||
log.error('No nzb/torrent available!')
|
||||
return False
|
||||
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||
@@ -42,6 +40,6 @@ class Blackhole(Downloader):
|
||||
pass
|
||||
|
||||
except:
|
||||
log.debug('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc()))
|
||||
log.info('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc()))
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -14,11 +14,15 @@ class NZBGet(Downloader):
|
||||
|
||||
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False):
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
|
||||
if not filedata:
|
||||
log.error('Unable to get NZB file: %s' % traceback.format_exc())
|
||||
return False
|
||||
|
||||
log.info('Sending "%s" to NZBGet.' % data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
@@ -40,19 +44,6 @@ class NZBGet(Downloader):
|
||||
log.error('Protocol Error: %s' % e)
|
||||
return False
|
||||
|
||||
try:
|
||||
if isfunction(data.get('download')):
|
||||
filedata = data.get('download')()
|
||||
if not filedata:
|
||||
log.error('Failed download file: %s' % nzb_name)
|
||||
return False
|
||||
else:
|
||||
log.info('Downloading: %s' % data.get('url'))
|
||||
filedata = self.urlopen(data.get('url'))
|
||||
except:
|
||||
log.error('Unable to get NZB file: %s' % traceback.format_exc())
|
||||
return False
|
||||
|
||||
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
|
||||
log.info('NZB sent successfully to NZBGet')
|
||||
return True
|
||||
|
||||
@@ -15,7 +15,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False):
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
@@ -42,15 +42,13 @@ class Sabnzbd(Downloader):
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
}
|
||||
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
|
||||
if not nzb_file or len(nzb_file) < 50:
|
||||
log.error('No nzb available!')
|
||||
if filedata:
|
||||
if len(filedata) < 50:
|
||||
log.error('No proper nzb available!')
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, nzb_file, movie)
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
params['mode'] = 'addfile'
|
||||
else:
|
||||
params['name'] = data.get('url')
|
||||
@@ -62,7 +60,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
try:
|
||||
if params.get('mode') is 'addfile':
|
||||
data = self.urlopen(url, params = {"nzbfile": (nzb_filename, nzb_file)}, multipart = True, show_error = False)
|
||||
data = self.urlopen(url, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
|
||||
else:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
except:
|
||||
|
||||
@@ -11,7 +11,7 @@ class Transmission(Downloader):
|
||||
|
||||
type = ['torrent']
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False):
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
@@ -31,8 +31,10 @@ class Transmission(Downloader):
|
||||
}
|
||||
|
||||
try:
|
||||
if not filedata:
|
||||
log.error('Failed sending torrent to transmission, no data')
|
||||
|
||||
tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password'))
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
torrent = tc.add_torrent(b64encode(filedata), **params)
|
||||
|
||||
# Change settings of added torrents
|
||||
|
||||
@@ -2,7 +2,9 @@ from couchpotato.core.logger import CPLog
|
||||
import hashlib
|
||||
import os.path
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -77,9 +79,9 @@ def cleanHost(host):
|
||||
|
||||
return host
|
||||
|
||||
def getImdb(txt):
|
||||
def getImdb(txt, check_inside = True):
|
||||
|
||||
if os.path.isfile(txt):
|
||||
if check_inside and os.path.isfile(txt):
|
||||
output = open(txt, 'r')
|
||||
txt = output.read()
|
||||
output.close()
|
||||
@@ -117,3 +119,6 @@ def getTitle(library_dict):
|
||||
log.error('Could not get title for library item: %s' % library_dict)
|
||||
return None
|
||||
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
|
||||
@@ -136,7 +136,12 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if not library.info:
|
||||
self.update(identifier)
|
||||
dates = library.get('info', {}).get('release_dates')
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time():
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
|
||||
@@ -17,7 +17,7 @@ rename_options = {
|
||||
'audio': 'Audio (DTS)',
|
||||
'group': 'Releasegroup name',
|
||||
'source': 'Source media (Bluray)',
|
||||
'original': 'Original filename',
|
||||
'filename': 'Original filename',
|
||||
'original_folder': 'Original foldername',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class Renamer(Plugin):
|
||||
movie_title = getTitle(group['library'])
|
||||
|
||||
# Add _UNKNOWN_ if no library item is connected
|
||||
unknown = False
|
||||
if not group['library'] or not movie_title:
|
||||
if group['dirname']:
|
||||
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
|
||||
@@ -94,6 +95,7 @@ class Renamer(Plugin):
|
||||
filename = os.path.basename(rename_me)
|
||||
rename_files[rename_me] = rename_me.replace(filename, '_UNKNOWN_%s' % filename)
|
||||
|
||||
unknown = True
|
||||
# Rename the files using the library data
|
||||
else:
|
||||
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
|
||||
@@ -325,6 +327,18 @@ class Renamer(Plugin):
|
||||
elif not remove_leftovers: # Don't remove anything
|
||||
remove_files = []
|
||||
|
||||
# Remove files
|
||||
for src in remove_files:
|
||||
|
||||
if isinstance(src, File):
|
||||
src = src.path
|
||||
|
||||
log.info('Removing "%s"' % src)
|
||||
try:
|
||||
os.remove(src)
|
||||
except:
|
||||
log.error('Failed removing %s: %s' % (src, traceback.format_exc()))
|
||||
|
||||
# Rename all files marked
|
||||
group['renamed_files'] = []
|
||||
for src in rename_files:
|
||||
@@ -341,18 +355,6 @@ class Renamer(Plugin):
|
||||
except:
|
||||
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
|
||||
|
||||
# Remove files
|
||||
for src in remove_files:
|
||||
|
||||
if isinstance(src, File):
|
||||
src = src.path
|
||||
|
||||
log.info('Removing "%s"' % src)
|
||||
try:
|
||||
os.remove(src)
|
||||
except:
|
||||
log.error('Failed removing %s: %s' % (src, traceback.format_exc()))
|
||||
|
||||
# Remove matching releases
|
||||
for release in remove_releases:
|
||||
log.debug('Removing release %s' % release.identifier)
|
||||
@@ -368,12 +370,13 @@ class Renamer(Plugin):
|
||||
except:
|
||||
log.error('Failed removing %s: %s' % (group['parentdir'], traceback.format_exc()))
|
||||
|
||||
# Search for trailers etc
|
||||
fireEventAsync('renamer.after', group)
|
||||
if not unknown:
|
||||
# Search for trailers etc
|
||||
fireEventAsync('renamer.after', group)
|
||||
|
||||
# Notify on download
|
||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
||||
# Notify on download
|
||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
|
||||
@@ -95,6 +95,8 @@ class Scanner(Plugin):
|
||||
|
||||
def scanFilesToLibrary(self, folder = None, files = None):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
groups = self.scan(folder = folder, files = files)
|
||||
|
||||
for group in groups.itervalues():
|
||||
@@ -103,6 +105,8 @@ class Scanner(Plugin):
|
||||
|
||||
def scanFolderToLibrary(self, folder = None, newer_than = None, simple = True):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
if not os.path.isdir(folder):
|
||||
return
|
||||
|
||||
@@ -129,6 +133,8 @@ class Scanner(Plugin):
|
||||
|
||||
def scan(self, folder = None, files = [], simple = False):
|
||||
|
||||
folder = os.path.normpath(folder)
|
||||
|
||||
if not folder or not os.path.isdir(folder):
|
||||
log.error('Folder doesn\'t exists: %s' % folder)
|
||||
return {}
|
||||
@@ -448,6 +454,18 @@ class Scanner(Plugin):
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check and see if filenames contains the imdb-id
|
||||
if not imdb_id:
|
||||
try:
|
||||
for filetype in files:
|
||||
for filetype_file in files[filetype]:
|
||||
imdb_id = getImdb(filetype_file, check_inside = False)
|
||||
if imdb_id:
|
||||
log.debug('Found movie via imdb in filename: %s' % nfo_file)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check if path is already in db
|
||||
if not imdb_id:
|
||||
db = get_session()
|
||||
@@ -703,11 +721,12 @@ class Scanner(Plugin):
|
||||
def getReleaseNameYear(self, release_name, file_name = None):
|
||||
|
||||
# Use guessit first
|
||||
guess = {}
|
||||
if file_name:
|
||||
try:
|
||||
guess = guess_movie_info(file_name)
|
||||
if guess.get('title') and guess.get('year'):
|
||||
return {
|
||||
guess = {
|
||||
'name': guess.get('title'),
|
||||
'year': guess.get('year'),
|
||||
}
|
||||
@@ -718,11 +737,12 @@ class Scanner(Plugin):
|
||||
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
|
||||
cleaned = re.sub(self.clean, ' ', cleaned)
|
||||
year = self.findYear(cleaned)
|
||||
cp_guess = {}
|
||||
|
||||
if year: # Split name on year
|
||||
try:
|
||||
movie_name = cleaned.split(year).pop(0).strip()
|
||||
return {
|
||||
cp_guess = {
|
||||
'name': movie_name,
|
||||
'year': int(year),
|
||||
}
|
||||
@@ -731,11 +751,16 @@ class Scanner(Plugin):
|
||||
else: # Split name on multiple spaces
|
||||
try:
|
||||
movie_name = cleaned.split(' ').pop(0).strip()
|
||||
return {
|
||||
cp_guess = {
|
||||
'name': movie_name,
|
||||
'year': int(year),
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return {}
|
||||
if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
|
||||
return cp_guess
|
||||
elif guess == {}:
|
||||
return cp_guess
|
||||
|
||||
return guess
|
||||
|
||||
@@ -38,4 +38,9 @@ class Score(Plugin):
|
||||
# Duplicates in name
|
||||
score += duplicateScore(nzb['name'], getTitle(movie['library']))
|
||||
|
||||
# Extra provider specific check
|
||||
extra_score = nzb.get('extra_score')
|
||||
if extra_score:
|
||||
score += extra_score(nzb)
|
||||
|
||||
return score
|
||||
|
||||
@@ -6,6 +6,7 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
import datetime
|
||||
import re
|
||||
@@ -142,9 +143,9 @@ class Searcher(Plugin):
|
||||
|
||||
for nzb in sorted_results:
|
||||
downloaded = self.download(data = nzb, movie = movie)
|
||||
if downloaded:
|
||||
if downloaded is True:
|
||||
return True
|
||||
else:
|
||||
elif downloaded != 'try_next':
|
||||
break
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title))
|
||||
@@ -161,7 +162,15 @@ class Searcher(Plugin):
|
||||
def download(self, data, movie, manual = False):
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True)
|
||||
|
||||
# Download movie to temp
|
||||
filedata = None
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
if filedata is 'try_next':
|
||||
return filedata
|
||||
|
||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True, filedata = filedata)
|
||||
|
||||
if successful:
|
||||
|
||||
@@ -214,15 +223,17 @@ class Searcher(Plugin):
|
||||
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name']))
|
||||
return False
|
||||
|
||||
movie_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', movie_name)
|
||||
required_words = [x.strip() for x in self.conf('required_words').split(',')]
|
||||
movie_name = getTitle(movie['library'])
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
|
||||
|
||||
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
|
||||
log.info("NZB doesn't contain any of the required words.")
|
||||
return False
|
||||
|
||||
ignored_words = [x.strip() for x in self.conf('ignored_words').split(',')]
|
||||
ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words))
|
||||
if self.conf('ignored_words') and blacklisted:
|
||||
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
||||
@@ -230,7 +241,7 @@ class Searcher(Plugin):
|
||||
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs']
|
||||
for p_tag in pron_tags:
|
||||
if p_tag in movie_name:
|
||||
if p_tag in nzb_words and p_tag not in movie_words:
|
||||
log.info('Wrong: %s, probably pr0n' % (nzb['name']))
|
||||
return False
|
||||
|
||||
@@ -254,6 +265,16 @@ class Searcher(Plugin):
|
||||
return False
|
||||
|
||||
|
||||
# Provider specific functions
|
||||
get_more = nzb.get('get_more_info')
|
||||
if get_more:
|
||||
get_more(nzb)
|
||||
|
||||
extra_check = nzb.get('extra_check')
|
||||
if extra_check and not extra_check(nzb):
|
||||
return False
|
||||
|
||||
|
||||
if imdb_results:
|
||||
return True
|
||||
|
||||
@@ -277,7 +298,7 @@ class Searcher(Plugin):
|
||||
if self.checkNFO(nzb['name'], movie['library']['identifier']):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], getTitle(movie['library']), movie['library']['year']))
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
|
||||
|
||||
@@ -75,7 +75,7 @@ class MetaDataBase(Plugin):
|
||||
break
|
||||
|
||||
for cur_file in data['library'].get('files', []):
|
||||
if cur_file.get('type_id') is file_type.get('id'):
|
||||
if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
|
||||
return cur_file.get('path')
|
||||
|
||||
def getFanart(self, movie_info = {}, data = {}):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.helpers.variable import mergeDicts, randomString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -23,11 +22,19 @@ class MovieResultModifier(Plugin):
|
||||
|
||||
# Combine on imdb id
|
||||
for item in results:
|
||||
imdb = item.get('imdb', 'random-%s' % time.time())
|
||||
random_string = randomString()
|
||||
imdb = item.get('imdb', random_string)
|
||||
imdb = imdb if imdb else random_string
|
||||
|
||||
if not temp.get(imdb):
|
||||
temp[imdb] = self.getLibraryTags(imdb)
|
||||
order.append(imdb)
|
||||
|
||||
if item.get('via_imdb'):
|
||||
if order.index(imdb):
|
||||
order.remove(imdb)
|
||||
order.insert(0, imdb)
|
||||
|
||||
# Merge dicts
|
||||
temp[imdb] = mergeDicts(temp[imdb], item)
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class IMDBAPI(MovieProvider):
|
||||
year = tryInt(movie.get('Year', ''))
|
||||
|
||||
movie_data = {
|
||||
'via_imdb': True,
|
||||
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
||||
'original_title': movie.get('Title', ''),
|
||||
'images': {
|
||||
@@ -109,10 +110,10 @@ class IMDBAPI(MovieProvider):
|
||||
def runtimeToMinutes(self, runtime_str):
|
||||
runtime = 0
|
||||
|
||||
regex = '(\d*.?\d+).(hr|hrs|mins|min)+'
|
||||
regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+'
|
||||
matches = re.findall(regex, runtime_str)
|
||||
for match in matches:
|
||||
nr, size = match
|
||||
runtime += tryInt(nr) * (60 if 'hr' in str(size) else 1)
|
||||
runtime += tryInt(nr) * (60 if 'h' is str(size)[0] else 1)
|
||||
|
||||
return runtime
|
||||
|
||||
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from libs.themoviedb import tmdb
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -88,6 +87,9 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
def getInfo(self, identifier = None):
|
||||
|
||||
if not identifier:
|
||||
return {}
|
||||
|
||||
cache_key = 'tmdb.cache.%s' % identifier
|
||||
result = self.getCache(cache_key)
|
||||
|
||||
@@ -148,6 +150,7 @@ class TheMovieDb(MovieProvider):
|
||||
year = None
|
||||
|
||||
movie_data = {
|
||||
'via_tmdb': True,
|
||||
'id': int(movie.get('id', 0)),
|
||||
'titles': [toUnicode(movie.get('name'))],
|
||||
'original_title': movie.get('original_name'),
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from .main import Moovee
|
||||
|
||||
def start():
|
||||
return Moovee()
|
||||
|
||||
config = [{
|
||||
'name': 'moovee',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': '#alt.binaries.moovee',
|
||||
'description': 'SD movies only',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,66 +0,0 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from dateutil.parser import parse
|
||||
import re
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Moovee(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'download': 'http://85.214.105.230/get_nzb.php?id=%s§ion=moovee',
|
||||
'search': 'http://abmoovee.allfilled.com/search.php?q=%s&Search=Search',
|
||||
}
|
||||
|
||||
regex = '<td class="cell_reqid">(?P<reqid>.*?)</td>.+?<td class="cell_request">(?P<title>.*?)</td>.+?<td class="cell_statuschange">(?P<age>.*?)</td>'
|
||||
|
||||
http_time_between_calls = 2 # Seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False):
|
||||
return results
|
||||
|
||||
q = '%s %s' % (getTitle(movie['library']), quality.get('identifier'))
|
||||
url = self.urls['search'] % tryUrlencode(q)
|
||||
|
||||
cache_key = 'moovee.%s' % q
|
||||
data = self.getCache(cache_key, url)
|
||||
if data:
|
||||
match = re.compile(self.regex, re.DOTALL).finditer(data)
|
||||
|
||||
for nzb in match:
|
||||
new = {
|
||||
'id': nzb.group('reqid'),
|
||||
'name': nzb.group('title'),
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'age': self.calculateAge(time.mktime(parse(nzb.group('age')).timetuple())),
|
||||
'size': None,
|
||||
'url': self.urls['download'] % (nzb.group('reqid')),
|
||||
'detail_url': '',
|
||||
'description': '',
|
||||
'check_nzb': False,
|
||||
}
|
||||
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single_category = False, single = True)
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
|
||||
def belongsTo(self, url, host = None):
|
||||
match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*§ion=moovee', url)
|
||||
if match:
|
||||
return self
|
||||
return
|
||||
@@ -22,7 +22,7 @@ class Mysterbin(NZBProvider):
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
@@ -39,7 +39,7 @@ class Mysterbin(NZBProvider):
|
||||
'nopasswd': 'on',
|
||||
}
|
||||
|
||||
cache_key = 'mysterbin.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
cache_key = 'mysterbin.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
|
||||
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
|
||||
if data:
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class Newzbin(NZBProvider, RSS):
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
format_id = self.getFormatId(type)
|
||||
@@ -115,12 +115,12 @@ class Newzbin(NZBProvider, RSS):
|
||||
'description': self.getTextElement(nzb, "description"),
|
||||
'check_nzb': False,
|
||||
}
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single_category = single_cat, single = True)
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
from urllib2 import HTTPError
|
||||
from urlparse import urlparse
|
||||
import time
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -20,6 +23,8 @@ class Newznab(NZBProvider, RSS):
|
||||
'search': 'movie',
|
||||
}
|
||||
|
||||
limits_reached = {}
|
||||
|
||||
cat_ids = [
|
||||
([2010], ['dvdr']),
|
||||
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||
@@ -46,7 +51,7 @@ class Newznab(NZBProvider, RSS):
|
||||
def singleFeed(self, host):
|
||||
|
||||
results = []
|
||||
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
|
||||
if self.isDisabled(host):
|
||||
return results
|
||||
|
||||
arguments = tryUrlencode({
|
||||
@@ -78,7 +83,7 @@ class Newznab(NZBProvider, RSS):
|
||||
def singleSearch(self, host, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
|
||||
if self.isDisabled(host):
|
||||
return results
|
||||
|
||||
cat_id = self.getCatId(quality['identifier'])
|
||||
@@ -139,13 +144,12 @@ class Newznab(NZBProvider, RSS):
|
||||
}
|
||||
|
||||
if not for_feed:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single_category = single_cat, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
else:
|
||||
@@ -194,3 +198,27 @@ class Newznab(NZBProvider, RSS):
|
||||
|
||||
def getApiExt(self, host):
|
||||
return '&apikey=%s' % host['api_key']
|
||||
|
||||
def download(self, url = '', nzb_id = ''):
|
||||
host = urlparse(url).hostname
|
||||
|
||||
if self.limits_reached.get(host):
|
||||
# Try again in 3 hours
|
||||
if self.limits_reached[host] > time.time() - 10800:
|
||||
return 'try_next'
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, show_error = False)
|
||||
self.limits_reached[host] = False
|
||||
return data
|
||||
except HTTPError, e:
|
||||
if e.code == 503:
|
||||
response = e.read().lower()
|
||||
if 'maximum api' in response or 'download limit' in response:
|
||||
if not self.limits_reached.get(host):
|
||||
log.error('Limit reached for newznab provider: %s' % host)
|
||||
self.limits_reached[host] = time.time()
|
||||
return 'try_next'
|
||||
|
||||
log.error('Failed download from %s' % (host, traceback.format_exc()))
|
||||
raise
|
||||
|
||||
@@ -24,7 +24,7 @@ class NZBClub(NZBProvider, RSS):
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
@@ -40,7 +40,7 @@ class NZBClub(NZBProvider, RSS):
|
||||
'ns': 1,
|
||||
}
|
||||
|
||||
cache_key = 'nzbclub.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
cache_key = 'nzbclub.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
|
||||
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
|
||||
if data:
|
||||
try:
|
||||
@@ -58,10 +58,14 @@ class NZBClub(NZBProvider, RSS):
|
||||
size = enclosure['length']
|
||||
date = self.getTextElement(nzb, "pubDate")
|
||||
|
||||
full_description = self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
|
||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||
def extra_check(item):
|
||||
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
|
||||
|
||||
if 'ARCHIVE inside ARCHIVE' in full_description:
|
||||
log.info('Wrong: Seems to be passworded files: %s' % new['name'])
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
new = {
|
||||
'id': nzbclub_id,
|
||||
@@ -73,19 +77,17 @@ class NZBClub(NZBProvider, RSS):
|
||||
'url': enclosure['url'].replace(' ', '_'),
|
||||
'download': self.download,
|
||||
'detail_url': self.getTextElement(nzb, "link"),
|
||||
'description': description,
|
||||
'description': '',
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'extra_check': extra_check
|
||||
}
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
if 'ARCHIVE inside ARCHIVE' in full_description:
|
||||
log.info('Wrong: Seems to be passworded files: %s' % new['name'])
|
||||
continue
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single_category = False, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
@@ -94,3 +96,21 @@ class NZBClub(NZBProvider, RSS):
|
||||
log.error('Failed to parse XML response from NZBClub')
|
||||
|
||||
return results
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
|
||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
def extraCheck(self, item):
|
||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||
|
||||
if 'ARCHIVE inside ARCHIVE' in full_description:
|
||||
log.info('Wrong: Seems to be passworded files: %s' % new['name'])
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -26,7 +26,7 @@ class NzbIndex(NZBProvider, RSS):
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['api']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
@@ -63,13 +63,8 @@ class NzbIndex(NZBProvider, RSS):
|
||||
|
||||
try:
|
||||
description = self.getTextElement(nzb, "description")
|
||||
if '/nfo/' in description.lower():
|
||||
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', description).group('nfo')
|
||||
full_description = self.getCache('nzbindex.%s' % nzbindex_id, url = nfo_url, cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
description = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
|
||||
except:
|
||||
pass
|
||||
description = ''
|
||||
|
||||
new = {
|
||||
'id': nzbindex_id,
|
||||
@@ -81,15 +76,16 @@ class NzbIndex(NZBProvider, RSS):
|
||||
'url': enclosure['url'],
|
||||
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
||||
'description': description,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
'check_nzb': True,
|
||||
}
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single_category = False, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
@@ -99,6 +95,15 @@ class NzbIndex(NZBProvider, RSS):
|
||||
|
||||
return results
|
||||
|
||||
def getMoreInfo(self, item):
|
||||
try:
|
||||
if '/nfo/' in item['description'].lower():
|
||||
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
|
||||
full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
|
||||
html = BeautifulSoup(full_description)
|
||||
item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
|
||||
except:
|
||||
pass
|
||||
|
||||
def isEnabled(self):
|
||||
return NZBProvider.isEnabled(self) and self.conf('enabled')
|
||||
|
||||
@@ -32,7 +32,7 @@ class NZBMatrix(NZBProvider, RSS):
|
||||
|
||||
results = []
|
||||
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
|
||||
@@ -83,13 +83,13 @@ class NZBMatrix(NZBProvider, RSS):
|
||||
'description': self.getTextElement(nzb, "description"),
|
||||
'check_nzb': True,
|
||||
}
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = True, single_category = single_cat, single = True)
|
||||
|
||||
if is_correct_movie:
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from .main import X264
|
||||
|
||||
def start():
|
||||
return X264()
|
||||
|
||||
config = [{
|
||||
'name': 'x264',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': '#alt.binaries.hdtv.x264',
|
||||
'description': 'HD movies only',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -1,70 +0,0 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class X264(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'download': 'http://85.214.105.230/get_nzb.php?id=%s§ion=hd',
|
||||
'search': 'http://85.214.105.230/x264/requests.php?release=%s&status=FILLED&age=1300&sort=ID',
|
||||
}
|
||||
|
||||
regex = '<tr class="req_filled"><td class="reqid">(?P<id>.*?)</td><td class="release">(?P<title>.*?)</td>.+?<td class="age">(?P<age>.*?)</td>'
|
||||
|
||||
http_time_between_calls = 2 # Seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False):
|
||||
return results
|
||||
|
||||
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
|
||||
url = self.urls['search'] % tryUrlencode(q)
|
||||
|
||||
cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
data = self.getCache(cache_key, url)
|
||||
if data:
|
||||
match = re.compile(self.regex, re.DOTALL).finditer(data)
|
||||
|
||||
for nzb in match:
|
||||
try:
|
||||
age_match = re.match('((?P<day>\d+)d)', nzb.group('age'))
|
||||
age = age_match.group('day')
|
||||
except:
|
||||
age = 1
|
||||
|
||||
new = {
|
||||
'id': nzb.group('id'),
|
||||
'name': nzb.group('title'),
|
||||
'type': 'nzb',
|
||||
'provider': self.getName(),
|
||||
'age': tryInt(age),
|
||||
'size': None,
|
||||
'url': self.urls['download'] % (nzb.group('id')),
|
||||
'detail_url': '',
|
||||
'description': '',
|
||||
'check_nzb': False,
|
||||
}
|
||||
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single_category = False, single = True)
|
||||
if is_correct_movie:
|
||||
results.append(new)
|
||||
self.found(new)
|
||||
|
||||
return results
|
||||
|
||||
def belongsTo(self, url, host = None):
|
||||
match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*§ion=hd', url)
|
||||
if match:
|
||||
return self
|
||||
return
|
||||
@@ -34,7 +34,7 @@ class KickAssTorrents(TorrentProvider):
|
||||
def search(self, movie, quality):
|
||||
|
||||
results = []
|
||||
if self.isDisabled() or not self.isAvailable(self.urls['test']):
|
||||
if self.isDisabled():
|
||||
return results
|
||||
|
||||
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
|
||||
@@ -77,6 +77,8 @@ class KickAssTorrents(TorrentProvider):
|
||||
new['id'] = temp.get('id')[-8:]
|
||||
new['name'] = link.text
|
||||
new['url'] = td.findAll('a', 'idownload')[1]['href']
|
||||
if new['url'][:2] == '//':
|
||||
new['url'] = 'http:%s' % new['url']
|
||||
new['score'] = 20 if td.find('a', 'iverif') else 0
|
||||
elif column_name is 'size':
|
||||
new['size'] = self.parseSize(td.text)
|
||||
|
||||
@@ -31,7 +31,7 @@ class ThePirateBay(TorrentProvider):
|
||||
def find(self, movie, quality, type):
|
||||
|
||||
results = []
|
||||
if not self.enabled() or not self.isAvailable(self.apiUrl):
|
||||
if not self.enabled():
|
||||
return results
|
||||
|
||||
url = self.apiUrl % (quote_plus(self.toSearchString(movie.name + ' ' + quality) + self.makeIgnoreString(type)), self.getCatId(type))
|
||||
|
||||
@@ -23,6 +23,7 @@ class Env(object):
|
||||
_deamonize = False
|
||||
_desktop = None
|
||||
_session = None
|
||||
_httpserver = None
|
||||
|
||||
''' Data paths and directories '''
|
||||
_app_dir = ""
|
||||
|
||||
@@ -4,8 +4,12 @@ from couchpotato.api import api
|
||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||
from logging import handlers
|
||||
from tornado import autoreload
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import RequestHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
import atexit
|
||||
import locale
|
||||
import logging
|
||||
import os.path
|
||||
@@ -39,10 +43,19 @@ def getOptions(base_path, args):
|
||||
|
||||
return options
|
||||
|
||||
# Tornado monkey patch logging..
|
||||
def _log(status_code, request):
|
||||
|
||||
def cleanup():
|
||||
fireEvent('app.crappy_shutdown', single = True)
|
||||
time.sleep(1)
|
||||
if status_code < 400:
|
||||
return
|
||||
elif status_code < 500:
|
||||
log_method = logging.warning
|
||||
else:
|
||||
log_method = logging.error
|
||||
request_time = 1000.0 * request.request_time()
|
||||
summary = request.method + " " + request.uri + " (" + \
|
||||
request.remote_ip + ")"
|
||||
log_method("%d %s %.2fms", status_code, summary, request_time)
|
||||
|
||||
|
||||
def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, Env = None, desktop = None):
|
||||
@@ -110,85 +123,77 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
# Development
|
||||
development = Env.setting('development', default = False, type = 'bool')
|
||||
Env.set('dev', development)
|
||||
if not development:
|
||||
atexit.register(cleanup)
|
||||
|
||||
# Disable logging for some modules
|
||||
for logger_name in ['enzyme', 'guessit', 'subliminal', 'apscheduler']:
|
||||
logging.getLogger(logger_name).setLevel(logging.ERROR)
|
||||
|
||||
for logger_name in ['gntp', 'werkzeug', 'migrate']:
|
||||
for logger_name in ['gntp', 'migrate']:
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
# Use reloader
|
||||
reloader = debug is True and development and not Env.get('desktop') and not options.daemon
|
||||
|
||||
# Only run once when debugging
|
||||
fire_load = False
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') or not reloader:
|
||||
# Logger
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
logger.setLevel(level)
|
||||
|
||||
# Logger
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
logger.setLevel(level)
|
||||
# To screen
|
||||
if (debug or options.console_log) and not options.quiet and not options.daemon:
|
||||
hdlr = logging.StreamHandler(sys.stderr)
|
||||
hdlr.setFormatter(formatter)
|
||||
logger.addHandler(hdlr)
|
||||
|
||||
# To screen
|
||||
if (debug or options.console_log) and not options.quiet and not options.daemon:
|
||||
hdlr = logging.StreamHandler(sys.stderr)
|
||||
hdlr.setFormatter(formatter)
|
||||
logger.addHandler(hdlr)
|
||||
# To file
|
||||
hdlr2 = handlers.RotatingFileHandler(Env.get('log_path'), 'a', 500000, 10)
|
||||
hdlr2.setFormatter(formatter)
|
||||
logger.addHandler(hdlr2)
|
||||
|
||||
# To file
|
||||
hdlr2 = handlers.RotatingFileHandler(Env.get('log_path'), 'a', 500000, 10)
|
||||
hdlr2.setFormatter(formatter)
|
||||
logger.addHandler(hdlr2)
|
||||
# Start logging & enable colors
|
||||
import color_logs
|
||||
from couchpotato.core.logger import CPLog
|
||||
log = CPLog(__name__)
|
||||
log.debug('Started with options %s' % options)
|
||||
|
||||
# Start logging & enable colors
|
||||
import color_logs
|
||||
from couchpotato.core.logger import CPLog
|
||||
log = CPLog(__name__)
|
||||
log.debug('Started with options %s' % options)
|
||||
|
||||
def customwarn(message, category, filename, lineno, file = None, line = None):
|
||||
log.warning('%s %s %s line:%s' % (category, message, filename, lineno))
|
||||
warnings.showwarning = customwarn
|
||||
def customwarn(message, category, filename, lineno, file = None, line = None):
|
||||
log.warning('%s %s %s line:%s' % (category, message, filename, lineno))
|
||||
warnings.showwarning = customwarn
|
||||
|
||||
|
||||
# Load configs & plugins
|
||||
loader = Env.get('loader')
|
||||
loader.preload(root = base_path)
|
||||
loader.run()
|
||||
# Load configs & plugins
|
||||
loader = Env.get('loader')
|
||||
loader.preload(root = base_path)
|
||||
loader.run()
|
||||
|
||||
|
||||
# Load migrations
|
||||
initialize = True
|
||||
db = Env.get('db_path')
|
||||
if os.path.isfile(db_path):
|
||||
initialize = False
|
||||
# Load migrations
|
||||
initialize = True
|
||||
db = Env.get('db_path')
|
||||
if os.path.isfile(db_path):
|
||||
initialize = False
|
||||
|
||||
from migrate.versioning.api import version_control, db_version, version, upgrade
|
||||
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
|
||||
from migrate.versioning.api import version_control, db_version, version, upgrade
|
||||
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
|
||||
|
||||
latest_db_version = version(repo)
|
||||
try:
|
||||
current_db_version = db_version(db, repo)
|
||||
except:
|
||||
version_control(db, repo, version = latest_db_version)
|
||||
current_db_version = db_version(db, repo)
|
||||
latest_db_version = version(repo)
|
||||
try:
|
||||
current_db_version = db_version(db, repo)
|
||||
except:
|
||||
version_control(db, repo, version = latest_db_version)
|
||||
current_db_version = db_version(db, repo)
|
||||
|
||||
if current_db_version < latest_db_version and not debug:
|
||||
log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version))
|
||||
upgrade(db, repo)
|
||||
if current_db_version < latest_db_version and not debug:
|
||||
log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version))
|
||||
upgrade(db, repo)
|
||||
|
||||
# Configure Database
|
||||
from couchpotato.core.settings.model import setup
|
||||
setup()
|
||||
# Configure Database
|
||||
from couchpotato.core.settings.model import setup
|
||||
setup()
|
||||
|
||||
if initialize:
|
||||
fireEvent('app.initialize', in_order = True)
|
||||
|
||||
fire_load = True
|
||||
if initialize:
|
||||
fireEvent('app.initialize', in_order = True)
|
||||
|
||||
# Create app
|
||||
from couchpotato import app
|
||||
@@ -197,6 +202,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
# Basic config
|
||||
app.secret_key = api_key
|
||||
# app.debug = development
|
||||
config = {
|
||||
'use_reloader': reloader,
|
||||
'host': Env.setting('host', default = '0.0.0.0'),
|
||||
@@ -216,14 +222,26 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
# Some logging and fire load event
|
||||
try: log.info('Starting server on port %(port)s' % config)
|
||||
except: pass
|
||||
if fire_load: fireEventAsync('app.load')
|
||||
fireEventAsync('app.load')
|
||||
|
||||
# Go go go!
|
||||
web_container = WSGIContainer(app)
|
||||
web_container._log = _log
|
||||
http_server = HTTPServer(web_container, no_keep_alive = True)
|
||||
Env.set('httpserver', http_server)
|
||||
loop = IOLoop.instance()
|
||||
|
||||
try_restart = True
|
||||
restart_tries = 5
|
||||
|
||||
while try_restart:
|
||||
try:
|
||||
app.run(**config)
|
||||
http_server.listen(config['port'], config['host'])
|
||||
|
||||
if config['use_reloader']:
|
||||
autoreload.start(loop)
|
||||
|
||||
loop.start()
|
||||
except Exception, e:
|
||||
try:
|
||||
nr, msg = e
|
||||
|
||||
27
libs/tornado/__init__.py
Normal file
27
libs/tornado/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""The Tornado web server and tools."""
|
||||
|
||||
# version is a human-readable version number.
|
||||
|
||||
# version_info is a four-tuple for programmatic comparison. The first
|
||||
# three numbers are the components of the version number. The fourth
|
||||
# is zero for an official release, positive for a development branch,
|
||||
# or negative for a release candidate (after the base version number
|
||||
# has been incremented)
|
||||
version = "2.2.1"
|
||||
version_info = (2, 2, 1, 0)
|
||||
1134
libs/tornado/auth.py
Normal file
1134
libs/tornado/auth.py
Normal file
File diff suppressed because it is too large
Load Diff
250
libs/tornado/autoreload.py
Normal file
250
libs/tornado/autoreload.py
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A module to automatically restart the server when a module is modified.
|
||||
|
||||
Most applications should not call this module directly. Instead, pass the
|
||||
keyword argument ``debug=True`` to the `tornado.web.Application` constructor.
|
||||
This will enable autoreload mode as well as checking for changes to templates
|
||||
and static resources.
|
||||
|
||||
This module depends on IOLoop, so it will not work in WSGI applications
|
||||
and Google AppEngine. It also will not work correctly when HTTPServer's
|
||||
multi-process mode is used.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import sys
|
||||
import types
|
||||
import subprocess
|
||||
|
||||
from tornado import ioloop
|
||||
from tornado import process
|
||||
|
||||
try:
|
||||
import signal
|
||||
except ImportError:
|
||||
signal = None
|
||||
|
||||
def start(io_loop=None, check_time=500):
|
||||
"""Restarts the process automatically when a module is modified.
|
||||
|
||||
We run on the I/O loop, and restarting is a destructive operation,
|
||||
so will terminate any pending requests.
|
||||
"""
|
||||
io_loop = io_loop or ioloop.IOLoop.instance()
|
||||
add_reload_hook(functools.partial(_close_all_fds, io_loop))
|
||||
modify_times = {}
|
||||
callback = functools.partial(_reload_on_update, modify_times)
|
||||
scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
|
||||
scheduler.start()
|
||||
|
||||
def wait():
|
||||
"""Wait for a watched file to change, then restart the process.
|
||||
|
||||
Intended to be used at the end of scripts like unit test runners,
|
||||
to run the tests again after any source file changes (but see also
|
||||
the command-line interface in `main`)
|
||||
"""
|
||||
io_loop = ioloop.IOLoop()
|
||||
start(io_loop)
|
||||
io_loop.start()
|
||||
|
||||
_watched_files = set()
|
||||
|
||||
def watch(filename):
|
||||
"""Add a file to the watch list.
|
||||
|
||||
All imported modules are watched by default.
|
||||
"""
|
||||
_watched_files.add(filename)
|
||||
|
||||
_reload_hooks = []
|
||||
|
||||
def add_reload_hook(fn):
|
||||
"""Add a function to be called before reloading the process.
|
||||
|
||||
Note that for open file and socket handles it is generally
|
||||
preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
|
||||
`tornado.platform.auto.set_close_exec`) instead of using a reload
|
||||
hook to close them.
|
||||
"""
|
||||
_reload_hooks.append(fn)
|
||||
|
||||
def _close_all_fds(io_loop):
|
||||
for fd in io_loop._handlers.keys():
|
||||
try:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_reload_attempted = False
|
||||
|
||||
def _reload_on_update(modify_times):
|
||||
if _reload_attempted:
|
||||
# We already tried to reload and it didn't work, so don't try again.
|
||||
return
|
||||
if process.task_id() is not None:
|
||||
# We're in a child process created by fork_processes. If child
|
||||
# processes restarted themselves, they'd all restart and then
|
||||
# all call fork_processes again.
|
||||
return
|
||||
for module in sys.modules.values():
|
||||
# Some modules play games with sys.modules (e.g. email/__init__.py
|
||||
# in the standard library), and occasionally this can cause strange
|
||||
# failures in getattr. Just ignore anything that's not an ordinary
|
||||
# module.
|
||||
if not isinstance(module, types.ModuleType): continue
|
||||
path = getattr(module, "__file__", None)
|
||||
if not path: continue
|
||||
if path.endswith(".pyc") or path.endswith(".pyo"):
|
||||
path = path[:-1]
|
||||
_check_file(modify_times, path)
|
||||
for path in _watched_files:
|
||||
_check_file(modify_times, path)
|
||||
|
||||
def _check_file(modify_times, path):
|
||||
try:
|
||||
modified = os.stat(path).st_mtime
|
||||
except Exception:
|
||||
return
|
||||
if path not in modify_times:
|
||||
modify_times[path] = modified
|
||||
return
|
||||
if modify_times[path] != modified:
|
||||
logging.info("%s modified; restarting server", path)
|
||||
_reload()
|
||||
|
||||
def _reload():
|
||||
global _reload_attempted
|
||||
_reload_attempted = True
|
||||
for fn in _reload_hooks:
|
||||
fn()
|
||||
if hasattr(signal, "setitimer"):
|
||||
# Clear the alarm signal set by
|
||||
# ioloop.set_blocking_log_threshold so it doesn't fire
|
||||
# after the exec.
|
||||
signal.setitimer(signal.ITIMER_REAL, 0, 0)
|
||||
if sys.platform == 'win32':
|
||||
# os.execv is broken on Windows and can't properly parse command line
|
||||
# arguments and executable name if they contain whitespaces. subprocess
|
||||
# fixes that behavior.
|
||||
subprocess.Popen([sys.executable] + sys.argv)
|
||||
sys.exit(0)
|
||||
else:
|
||||
try:
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
except OSError:
|
||||
# Mac OS X versions prior to 10.6 do not support execv in
|
||||
# a process that contains multiple threads. Instead of
|
||||
# re-executing in the current process, start a new one
|
||||
# and cause the current process to exit. This isn't
|
||||
# ideal since the new process is detached from the parent
|
||||
# terminal and thus cannot easily be killed with ctrl-C,
|
||||
# but it's better than not being able to autoreload at
|
||||
# all.
|
||||
# Unfortunately the errno returned in this case does not
|
||||
# appear to be consistent, so we can't easily check for
|
||||
# this error specifically.
|
||||
os.spawnv(os.P_NOWAIT, sys.executable,
|
||||
[sys.executable] + sys.argv)
|
||||
sys.exit(0)
|
||||
|
||||
_USAGE = """\
|
||||
Usage:
|
||||
python -m tornado.autoreload -m module.to.run [args...]
|
||||
python -m tornado.autoreload path/to/script.py [args...]
|
||||
"""
|
||||
def main():
|
||||
"""Command-line wrapper to re-run a script whenever its source changes.
|
||||
|
||||
Scripts may be specified by filename or module name::
|
||||
|
||||
python -m tornado.autoreload -m tornado.test.runtests
|
||||
python -m tornado.autoreload tornado/test/runtests.py
|
||||
|
||||
Running a script with this wrapper is similar to calling
|
||||
`tornado.autoreload.wait` at the end of the script, but this wrapper
|
||||
can catch import-time problems like syntax errors that would otherwise
|
||||
prevent the script from reaching its call to `wait`.
|
||||
"""
|
||||
original_argv = sys.argv
|
||||
sys.argv = sys.argv[:]
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "-m":
|
||||
mode = "module"
|
||||
module = sys.argv[2]
|
||||
del sys.argv[1:3]
|
||||
elif len(sys.argv) >= 2:
|
||||
mode = "script"
|
||||
script = sys.argv[1]
|
||||
sys.argv = sys.argv[1:]
|
||||
else:
|
||||
print >>sys.stderr, _USAGE
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
if mode == "module":
|
||||
import runpy
|
||||
runpy.run_module(module, run_name="__main__", alter_sys=True)
|
||||
elif mode == "script":
|
||||
with open(script) as f:
|
||||
global __file__
|
||||
__file__ = script
|
||||
# Use globals as our "locals" dictionary so that
|
||||
# something that tries to import __main__ (e.g. the unittest
|
||||
# module) will see the right things.
|
||||
exec f.read() in globals(), globals()
|
||||
except SystemExit, e:
|
||||
logging.info("Script exited with status %s", e.code)
|
||||
except Exception, e:
|
||||
logging.warning("Script exited with uncaught exception", exc_info=True)
|
||||
if isinstance(e, SyntaxError):
|
||||
watch(e.filename)
|
||||
else:
|
||||
logging.info("Script exited normally")
|
||||
# restore sys.argv so subsequent executions will include autoreload
|
||||
sys.argv = original_argv
|
||||
|
||||
if mode == 'module':
|
||||
# runpy did a fake import of the module as __main__, but now it's
|
||||
# no longer in sys.modules. Figure out where it is and watch it.
|
||||
watch(pkgutil.get_loader(module).get_filename())
|
||||
|
||||
wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# If this module is run with "python -m tornado.autoreload", the current
|
||||
# directory is automatically prepended to sys.path, but not if it is
|
||||
# run as "path/to/tornado/autoreload.py". The processing for "-m" rewrites
|
||||
# the former to the latter, so subsequent executions won't have the same
|
||||
# path as the original. Modify os.environ here to ensure that the
|
||||
# re-executed process will have the same path.
|
||||
# Conversely, when run as path/to/tornado/autoreload.py, the directory
|
||||
# containing autoreload.py gets added to the path, but we don't want
|
||||
# tornado modules importable at top level, so remove it.
|
||||
path_prefix = '.' + os.pathsep
|
||||
if (sys.path[0] == '' and
|
||||
not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
|
||||
os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "")
|
||||
elif sys.path[0] == os.path.dirname(__file__):
|
||||
del sys.path[0]
|
||||
main()
|
||||
3576
libs/tornado/ca-certificates.crt
Normal file
3576
libs/tornado/ca-certificates.crt
Normal file
File diff suppressed because it is too large
Load Diff
435
libs/tornado/curl_httpclient.py
Normal file
435
libs/tornado/curl_httpclient.py
Normal file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Blocking and non-blocking HTTP client implementations using pycurl."""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import cStringIO
|
||||
import collections
|
||||
import logging
|
||||
import pycurl
|
||||
import threading
|
||||
import time
|
||||
|
||||
from tornado import httputil
|
||||
from tornado import ioloop
|
||||
from tornado import stack_context
|
||||
|
||||
from tornado.escape import utf8
|
||||
from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main
|
||||
|
||||
class CurlAsyncHTTPClient(AsyncHTTPClient):
|
||||
def initialize(self, io_loop=None, max_clients=10,
|
||||
max_simultaneous_connections=None):
|
||||
self.io_loop = io_loop
|
||||
self._multi = pycurl.CurlMulti()
|
||||
self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
|
||||
self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
|
||||
self._curls = [_curl_create(max_simultaneous_connections)
|
||||
for i in xrange(max_clients)]
|
||||
self._free_list = self._curls[:]
|
||||
self._requests = collections.deque()
|
||||
self._fds = {}
|
||||
self._timeout = None
|
||||
|
||||
try:
|
||||
self._socket_action = self._multi.socket_action
|
||||
except AttributeError:
|
||||
# socket_action is found in pycurl since 7.18.2 (it's been
|
||||
# in libcurl longer than that but wasn't accessible to
|
||||
# python).
|
||||
logging.warning("socket_action method missing from pycurl; "
|
||||
"falling back to socket_all. Upgrading "
|
||||
"libcurl and pycurl will improve performance")
|
||||
self._socket_action = \
|
||||
lambda fd, action: self._multi.socket_all()
|
||||
|
||||
# libcurl has bugs that sometimes cause it to not report all
|
||||
# relevant file descriptors and timeouts to TIMERFUNCTION/
|
||||
# SOCKETFUNCTION. Mitigate the effects of such bugs by
|
||||
# forcing a periodic scan of all active requests.
|
||||
self._force_timeout_callback = ioloop.PeriodicCallback(
|
||||
self._handle_force_timeout, 1000, io_loop=io_loop)
|
||||
self._force_timeout_callback.start()
|
||||
|
||||
def close(self):
|
||||
self._force_timeout_callback.stop()
|
||||
for curl in self._curls:
|
||||
curl.close()
|
||||
self._multi.close()
|
||||
self._closed = True
|
||||
super(CurlAsyncHTTPClient, self).close()
|
||||
|
||||
def fetch(self, request, callback, **kwargs):
|
||||
if not isinstance(request, HTTPRequest):
|
||||
request = HTTPRequest(url=request, **kwargs)
|
||||
self._requests.append((request, stack_context.wrap(callback)))
|
||||
self._process_queue()
|
||||
self._set_timeout(0)
|
||||
|
||||
def _handle_socket(self, event, fd, multi, data):
|
||||
"""Called by libcurl when it wants to change the file descriptors
|
||||
it cares about.
|
||||
"""
|
||||
event_map = {
|
||||
pycurl.POLL_NONE: ioloop.IOLoop.NONE,
|
||||
pycurl.POLL_IN: ioloop.IOLoop.READ,
|
||||
pycurl.POLL_OUT: ioloop.IOLoop.WRITE,
|
||||
pycurl.POLL_INOUT: ioloop.IOLoop.READ | ioloop.IOLoop.WRITE
|
||||
}
|
||||
if event == pycurl.POLL_REMOVE:
|
||||
self.io_loop.remove_handler(fd)
|
||||
del self._fds[fd]
|
||||
else:
|
||||
ioloop_event = event_map[event]
|
||||
if fd not in self._fds:
|
||||
self._fds[fd] = ioloop_event
|
||||
self.io_loop.add_handler(fd, self._handle_events,
|
||||
ioloop_event)
|
||||
else:
|
||||
self._fds[fd] = ioloop_event
|
||||
self.io_loop.update_handler(fd, ioloop_event)
|
||||
|
||||
def _set_timeout(self, msecs):
|
||||
"""Called by libcurl to schedule a timeout."""
|
||||
if self._timeout is not None:
|
||||
self.io_loop.remove_timeout(self._timeout)
|
||||
self._timeout = self.io_loop.add_timeout(
|
||||
time.time() + msecs/1000.0, self._handle_timeout)
|
||||
|
||||
def _handle_events(self, fd, events):
|
||||
"""Called by IOLoop when there is activity on one of our
|
||||
file descriptors.
|
||||
"""
|
||||
action = 0
|
||||
if events & ioloop.IOLoop.READ: action |= pycurl.CSELECT_IN
|
||||
if events & ioloop.IOLoop.WRITE: action |= pycurl.CSELECT_OUT
|
||||
while True:
|
||||
try:
|
||||
ret, num_handles = self._socket_action(fd, action)
|
||||
except pycurl.error, e:
|
||||
ret = e.args[0]
|
||||
if ret != pycurl.E_CALL_MULTI_PERFORM:
|
||||
break
|
||||
self._finish_pending_requests()
|
||||
|
||||
def _handle_timeout(self):
|
||||
"""Called by IOLoop when the requested timeout has passed."""
|
||||
with stack_context.NullContext():
|
||||
self._timeout = None
|
||||
while True:
|
||||
try:
|
||||
ret, num_handles = self._socket_action(
|
||||
pycurl.SOCKET_TIMEOUT, 0)
|
||||
except pycurl.error, e:
|
||||
ret = e.args[0]
|
||||
if ret != pycurl.E_CALL_MULTI_PERFORM:
|
||||
break
|
||||
self._finish_pending_requests()
|
||||
|
||||
# In theory, we shouldn't have to do this because curl will
|
||||
# call _set_timeout whenever the timeout changes. However,
|
||||
# sometimes after _handle_timeout we will need to reschedule
|
||||
# immediately even though nothing has changed from curl's
|
||||
# perspective. This is because when socket_action is
|
||||
# called with SOCKET_TIMEOUT, libcurl decides internally which
|
||||
# timeouts need to be processed by using a monotonic clock
|
||||
# (where available) while tornado uses python's time.time()
|
||||
# to decide when timeouts have occurred. When those clocks
|
||||
# disagree on elapsed time (as they will whenever there is an
|
||||
# NTP adjustment), tornado might call _handle_timeout before
|
||||
# libcurl is ready. After each timeout, resync the scheduled
|
||||
# timeout with libcurl's current state.
|
||||
new_timeout = self._multi.timeout()
|
||||
if new_timeout != -1:
|
||||
self._set_timeout(new_timeout)
|
||||
|
||||
def _handle_force_timeout(self):
|
||||
"""Called by IOLoop periodically to ask libcurl to process any
|
||||
events it may have forgotten about.
|
||||
"""
|
||||
with stack_context.NullContext():
|
||||
while True:
|
||||
try:
|
||||
ret, num_handles = self._multi.socket_all()
|
||||
except pycurl.error, e:
|
||||
ret = e.args[0]
|
||||
if ret != pycurl.E_CALL_MULTI_PERFORM:
|
||||
break
|
||||
self._finish_pending_requests()
|
||||
|
||||
def _finish_pending_requests(self):
|
||||
"""Process any requests that were completed by the last
|
||||
call to multi.socket_action.
|
||||
"""
|
||||
while True:
|
||||
num_q, ok_list, err_list = self._multi.info_read()
|
||||
for curl in ok_list:
|
||||
self._finish(curl)
|
||||
for curl, errnum, errmsg in err_list:
|
||||
self._finish(curl, errnum, errmsg)
|
||||
if num_q == 0:
|
||||
break
|
||||
self._process_queue()
|
||||
|
||||
def _process_queue(self):
|
||||
with stack_context.NullContext():
|
||||
while True:
|
||||
started = 0
|
||||
while self._free_list and self._requests:
|
||||
started += 1
|
||||
curl = self._free_list.pop()
|
||||
(request, callback) = self._requests.popleft()
|
||||
curl.info = {
|
||||
"headers": httputil.HTTPHeaders(),
|
||||
"buffer": cStringIO.StringIO(),
|
||||
"request": request,
|
||||
"callback": callback,
|
||||
"curl_start_time": time.time(),
|
||||
}
|
||||
# Disable IPv6 to mitigate the effects of this bug
|
||||
# on curl versions <= 7.21.0
|
||||
# http://sourceforge.net/tracker/?func=detail&aid=3017819&group_id=976&atid=100976
|
||||
if pycurl.version_info()[2] <= 0x71500: # 7.21.0
|
||||
curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
|
||||
_curl_setup_request(curl, request, curl.info["buffer"],
|
||||
curl.info["headers"])
|
||||
self._multi.add_handle(curl)
|
||||
|
||||
if not started:
|
||||
break
|
||||
|
||||
def _finish(self, curl, curl_error=None, curl_message=None):
|
||||
info = curl.info
|
||||
curl.info = None
|
||||
self._multi.remove_handle(curl)
|
||||
self._free_list.append(curl)
|
||||
buffer = info["buffer"]
|
||||
if curl_error:
|
||||
error = CurlError(curl_error, curl_message)
|
||||
code = error.code
|
||||
effective_url = None
|
||||
buffer.close()
|
||||
buffer = None
|
||||
else:
|
||||
error = None
|
||||
code = curl.getinfo(pycurl.HTTP_CODE)
|
||||
effective_url = curl.getinfo(pycurl.EFFECTIVE_URL)
|
||||
buffer.seek(0)
|
||||
# the various curl timings are documented at
|
||||
# http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
|
||||
time_info = dict(
|
||||
queue=info["curl_start_time"] - info["request"].start_time,
|
||||
namelookup=curl.getinfo(pycurl.NAMELOOKUP_TIME),
|
||||
connect=curl.getinfo(pycurl.CONNECT_TIME),
|
||||
pretransfer=curl.getinfo(pycurl.PRETRANSFER_TIME),
|
||||
starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME),
|
||||
total=curl.getinfo(pycurl.TOTAL_TIME),
|
||||
redirect=curl.getinfo(pycurl.REDIRECT_TIME),
|
||||
)
|
||||
try:
|
||||
info["callback"](HTTPResponse(
|
||||
request=info["request"], code=code, headers=info["headers"],
|
||||
buffer=buffer, effective_url=effective_url, error=error,
|
||||
request_time=time.time() - info["curl_start_time"],
|
||||
time_info=time_info))
|
||||
except Exception:
|
||||
self.handle_callback_exception(info["callback"])
|
||||
|
||||
|
||||
def handle_callback_exception(self, callback):
|
||||
self.io_loop.handle_callback_exception(callback)
|
||||
|
||||
|
||||
class CurlError(HTTPError):
|
||||
def __init__(self, errno, message):
|
||||
HTTPError.__init__(self, 599, message)
|
||||
self.errno = errno
|
||||
|
||||
|
||||
def _curl_create(max_simultaneous_connections=None):
|
||||
curl = pycurl.Curl()
|
||||
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
||||
curl.setopt(pycurl.VERBOSE, 1)
|
||||
curl.setopt(pycurl.DEBUGFUNCTION, _curl_debug)
|
||||
curl.setopt(pycurl.MAXCONNECTS, max_simultaneous_connections or 5)
|
||||
return curl
|
||||
|
||||
|
||||
def _curl_setup_request(curl, request, buffer, headers):
|
||||
curl.setopt(pycurl.URL, utf8(request.url))
|
||||
|
||||
# libcurl's magic "Expect: 100-continue" behavior causes delays
|
||||
# with servers that don't support it (which include, among others,
|
||||
# Google's OpenID endpoint). Additionally, this behavior has
|
||||
# a bug in conjunction with the curl_multi_socket_action API
|
||||
# (https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3039744&group_id=976),
|
||||
# which increases the delays. It's more trouble than it's worth,
|
||||
# so just turn off the feature (yes, setting Expect: to an empty
|
||||
# value is the official way to disable this)
|
||||
if "Expect" not in request.headers:
|
||||
request.headers["Expect"] = ""
|
||||
|
||||
# libcurl adds Pragma: no-cache by default; disable that too
|
||||
if "Pragma" not in request.headers:
|
||||
request.headers["Pragma"] = ""
|
||||
|
||||
# Request headers may be either a regular dict or HTTPHeaders object
|
||||
if isinstance(request.headers, httputil.HTTPHeaders):
|
||||
curl.setopt(pycurl.HTTPHEADER,
|
||||
[utf8("%s: %s" % i) for i in request.headers.get_all()])
|
||||
else:
|
||||
curl.setopt(pycurl.HTTPHEADER,
|
||||
[utf8("%s: %s" % i) for i in request.headers.iteritems()])
|
||||
|
||||
if request.header_callback:
|
||||
curl.setopt(pycurl.HEADERFUNCTION, request.header_callback)
|
||||
else:
|
||||
curl.setopt(pycurl.HEADERFUNCTION,
|
||||
lambda line: _curl_header_callback(headers, line))
|
||||
if request.streaming_callback:
|
||||
curl.setopt(pycurl.WRITEFUNCTION, request.streaming_callback)
|
||||
else:
|
||||
curl.setopt(pycurl.WRITEFUNCTION, buffer.write)
|
||||
curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects)
|
||||
curl.setopt(pycurl.MAXREDIRS, request.max_redirects)
|
||||
curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout))
|
||||
curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout))
|
||||
if request.user_agent:
|
||||
curl.setopt(pycurl.USERAGENT, utf8(request.user_agent))
|
||||
else:
|
||||
curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)")
|
||||
if request.network_interface:
|
||||
curl.setopt(pycurl.INTERFACE, request.network_interface)
|
||||
if request.use_gzip:
|
||||
curl.setopt(pycurl.ENCODING, "gzip,deflate")
|
||||
else:
|
||||
curl.setopt(pycurl.ENCODING, "none")
|
||||
if request.proxy_host and request.proxy_port:
|
||||
curl.setopt(pycurl.PROXY, request.proxy_host)
|
||||
curl.setopt(pycurl.PROXYPORT, request.proxy_port)
|
||||
if request.proxy_username:
|
||||
credentials = '%s:%s' % (request.proxy_username,
|
||||
request.proxy_password)
|
||||
curl.setopt(pycurl.PROXYUSERPWD, credentials)
|
||||
else:
|
||||
curl.setopt(pycurl.PROXY, '')
|
||||
if request.validate_cert:
|
||||
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
|
||||
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
|
||||
else:
|
||||
curl.setopt(pycurl.SSL_VERIFYPEER, 0)
|
||||
curl.setopt(pycurl.SSL_VERIFYHOST, 0)
|
||||
if request.ca_certs is not None:
|
||||
curl.setopt(pycurl.CAINFO, request.ca_certs)
|
||||
else:
|
||||
# There is no way to restore pycurl.CAINFO to its default value
|
||||
# (Using unsetopt makes it reject all certificates).
|
||||
# I don't see any way to read the default value from python so it
|
||||
# can be restored later. We'll have to just leave CAINFO untouched
|
||||
# if no ca_certs file was specified, and require that if any
|
||||
# request uses a custom ca_certs file, they all must.
|
||||
pass
|
||||
|
||||
if request.allow_ipv6 is False:
|
||||
# Curl behaves reasonably when DNS resolution gives an ipv6 address
|
||||
# that we can't reach, so allow ipv6 unless the user asks to disable.
|
||||
# (but see version check in _process_queue above)
|
||||
curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
|
||||
|
||||
# Set the request method through curl's irritating interface which makes
|
||||
# up names for almost every single method
|
||||
curl_options = {
|
||||
"GET": pycurl.HTTPGET,
|
||||
"POST": pycurl.POST,
|
||||
"PUT": pycurl.UPLOAD,
|
||||
"HEAD": pycurl.NOBODY,
|
||||
}
|
||||
custom_methods = set(["DELETE"])
|
||||
for o in curl_options.values():
|
||||
curl.setopt(o, False)
|
||||
if request.method in curl_options:
|
||||
curl.unsetopt(pycurl.CUSTOMREQUEST)
|
||||
curl.setopt(curl_options[request.method], True)
|
||||
elif request.allow_nonstandard_methods or request.method in custom_methods:
|
||||
curl.setopt(pycurl.CUSTOMREQUEST, request.method)
|
||||
else:
|
||||
raise KeyError('unknown method ' + request.method)
|
||||
|
||||
# Handle curl's cryptic options for every individual HTTP method
|
||||
if request.method in ("POST", "PUT"):
|
||||
request_buffer = cStringIO.StringIO(utf8(request.body))
|
||||
curl.setopt(pycurl.READFUNCTION, request_buffer.read)
|
||||
if request.method == "POST":
|
||||
def ioctl(cmd):
|
||||
if cmd == curl.IOCMD_RESTARTREAD:
|
||||
request_buffer.seek(0)
|
||||
curl.setopt(pycurl.IOCTLFUNCTION, ioctl)
|
||||
curl.setopt(pycurl.POSTFIELDSIZE, len(request.body))
|
||||
else:
|
||||
curl.setopt(pycurl.INFILESIZE, len(request.body))
|
||||
|
||||
if request.auth_username is not None:
|
||||
userpwd = "%s:%s" % (request.auth_username, request.auth_password or '')
|
||||
curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
|
||||
curl.setopt(pycurl.USERPWD, utf8(userpwd))
|
||||
logging.debug("%s %s (username: %r)", request.method, request.url,
|
||||
request.auth_username)
|
||||
else:
|
||||
curl.unsetopt(pycurl.USERPWD)
|
||||
logging.debug("%s %s", request.method, request.url)
|
||||
|
||||
if request.client_key is not None or request.client_cert is not None:
|
||||
raise ValueError("Client certificate not supported with curl_httpclient")
|
||||
|
||||
if threading.activeCount() > 1:
|
||||
# libcurl/pycurl is not thread-safe by default. When multiple threads
|
||||
# are used, signals should be disabled. This has the side effect
|
||||
# of disabling DNS timeouts in some environments (when libcurl is
|
||||
# not linked against ares), so we don't do it when there is only one
|
||||
# thread. Applications that use many short-lived threads may need
|
||||
# to set NOSIGNAL manually in a prepare_curl_callback since
|
||||
# there may not be any other threads running at the time we call
|
||||
# threading.activeCount.
|
||||
curl.setopt(pycurl.NOSIGNAL, 1)
|
||||
if request.prepare_curl_callback is not None:
|
||||
request.prepare_curl_callback(curl)
|
||||
|
||||
|
||||
def _curl_header_callback(headers, header_line):
|
||||
# header_line as returned by curl includes the end-of-line characters.
|
||||
header_line = header_line.strip()
|
||||
if header_line.startswith("HTTP/"):
|
||||
headers.clear()
|
||||
return
|
||||
if not header_line:
|
||||
return
|
||||
headers.parse_line(header_line)
|
||||
|
||||
def _curl_debug(debug_type, debug_msg):
|
||||
debug_types = ('I', '<', '>', '<', '>')
|
||||
if debug_type == 0:
|
||||
logging.debug('%s', debug_msg.strip())
|
||||
elif debug_type in (1, 2):
|
||||
for line in debug_msg.splitlines():
|
||||
logging.debug('%s %s', debug_types[debug_type], line)
|
||||
elif debug_type == 4:
|
||||
logging.debug('%s %r', debug_types[debug_type], debug_msg)
|
||||
|
||||
if __name__ == "__main__":
|
||||
AsyncHTTPClient.configure(CurlAsyncHTTPClient)
|
||||
main()
|
||||
229
libs/tornado/database.py
Normal file
229
libs/tornado/database.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A lightweight wrapper around MySQLdb."""
|
||||
|
||||
import copy
|
||||
import MySQLdb.constants
|
||||
import MySQLdb.converters
|
||||
import MySQLdb.cursors
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
|
||||
class Connection(object):
|
||||
"""A lightweight wrapper around MySQLdb DB-API connections.
|
||||
|
||||
The main value we provide is wrapping rows in a dict/object so that
|
||||
columns can be accessed by name. Typical usage::
|
||||
|
||||
db = database.Connection("localhost", "mydatabase")
|
||||
for article in db.query("SELECT * FROM articles"):
|
||||
print article.title
|
||||
|
||||
Cursors are hidden by the implementation, but other than that, the methods
|
||||
are very similar to the DB-API.
|
||||
|
||||
We explicitly set the timezone to UTC and the character encoding to
|
||||
UTF-8 on all connections to avoid time zone and encoding errors.
|
||||
"""
|
||||
def __init__(self, host, database, user=None, password=None,
|
||||
max_idle_time=7*3600):
|
||||
self.host = host
|
||||
self.database = database
|
||||
self.max_idle_time = max_idle_time
|
||||
|
||||
args = dict(conv=CONVERSIONS, use_unicode=True, charset="utf8",
|
||||
db=database, init_command='SET time_zone = "+0:00"',
|
||||
sql_mode="TRADITIONAL")
|
||||
if user is not None:
|
||||
args["user"] = user
|
||||
if password is not None:
|
||||
args["passwd"] = password
|
||||
|
||||
# We accept a path to a MySQL socket file or a host(:port) string
|
||||
if "/" in host:
|
||||
args["unix_socket"] = host
|
||||
else:
|
||||
self.socket = None
|
||||
pair = host.split(":")
|
||||
if len(pair) == 2:
|
||||
args["host"] = pair[0]
|
||||
args["port"] = int(pair[1])
|
||||
else:
|
||||
args["host"] = host
|
||||
args["port"] = 3306
|
||||
|
||||
self._db = None
|
||||
self._db_args = args
|
||||
self._last_use_time = time.time()
|
||||
try:
|
||||
self.reconnect()
|
||||
except Exception:
|
||||
logging.error("Cannot connect to MySQL on %s", self.host,
|
||||
exc_info=True)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""Closes this database connection."""
|
||||
if getattr(self, "_db", None) is not None:
|
||||
self._db.close()
|
||||
self._db = None
|
||||
|
||||
def reconnect(self):
|
||||
"""Closes the existing database connection and re-opens it."""
|
||||
self.close()
|
||||
self._db = MySQLdb.connect(**self._db_args)
|
||||
self._db.autocommit(True)
|
||||
|
||||
def iter(self, query, *parameters):
|
||||
"""Returns an iterator for the given query and parameters."""
|
||||
self._ensure_connected()
|
||||
cursor = MySQLdb.cursors.SSCursor(self._db)
|
||||
try:
|
||||
self._execute(cursor, query, parameters)
|
||||
column_names = [d[0] for d in cursor.description]
|
||||
for row in cursor:
|
||||
yield Row(zip(column_names, row))
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def query(self, query, *parameters):
|
||||
"""Returns a row list for the given query and parameters."""
|
||||
cursor = self._cursor()
|
||||
try:
|
||||
self._execute(cursor, query, parameters)
|
||||
column_names = [d[0] for d in cursor.description]
|
||||
return [Row(itertools.izip(column_names, row)) for row in cursor]
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def get(self, query, *parameters):
|
||||
"""Returns the first row returned for the given query."""
|
||||
rows = self.query(query, *parameters)
|
||||
if not rows:
|
||||
return None
|
||||
elif len(rows) > 1:
|
||||
raise Exception("Multiple rows returned for Database.get() query")
|
||||
else:
|
||||
return rows[0]
|
||||
|
||||
# rowcount is a more reasonable default return value than lastrowid,
|
||||
# but for historical compatibility execute() must return lastrowid.
|
||||
def execute(self, query, *parameters):
|
||||
"""Executes the given query, returning the lastrowid from the query."""
|
||||
return self.execute_lastrowid(query, *parameters)
|
||||
|
||||
def execute_lastrowid(self, query, *parameters):
|
||||
"""Executes the given query, returning the lastrowid from the query."""
|
||||
cursor = self._cursor()
|
||||
try:
|
||||
self._execute(cursor, query, parameters)
|
||||
return cursor.lastrowid
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def execute_rowcount(self, query, *parameters):
|
||||
"""Executes the given query, returning the rowcount from the query."""
|
||||
cursor = self._cursor()
|
||||
try:
|
||||
self._execute(cursor, query, parameters)
|
||||
return cursor.rowcount
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def executemany(self, query, parameters):
|
||||
"""Executes the given query against all the given param sequences.
|
||||
|
||||
We return the lastrowid from the query.
|
||||
"""
|
||||
return self.executemany_lastrowid(query, parameters)
|
||||
|
||||
def executemany_lastrowid(self, query, parameters):
|
||||
"""Executes the given query against all the given param sequences.
|
||||
|
||||
We return the lastrowid from the query.
|
||||
"""
|
||||
cursor = self._cursor()
|
||||
try:
|
||||
cursor.executemany(query, parameters)
|
||||
return cursor.lastrowid
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def executemany_rowcount(self, query, parameters):
|
||||
"""Executes the given query against all the given param sequences.
|
||||
|
||||
We return the rowcount from the query.
|
||||
"""
|
||||
cursor = self._cursor()
|
||||
try:
|
||||
cursor.executemany(query, parameters)
|
||||
return cursor.rowcount
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
def _ensure_connected(self):
|
||||
# Mysql by default closes client connections that are idle for
|
||||
# 8 hours, but the client library does not report this fact until
|
||||
# you try to perform a query and it fails. Protect against this
|
||||
# case by preemptively closing and reopening the connection
|
||||
# if it has been idle for too long (7 hours by default).
|
||||
if (self._db is None or
|
||||
(time.time() - self._last_use_time > self.max_idle_time)):
|
||||
self.reconnect()
|
||||
self._last_use_time = time.time()
|
||||
|
||||
def _cursor(self):
|
||||
self._ensure_connected()
|
||||
return self._db.cursor()
|
||||
|
||||
def _execute(self, cursor, query, parameters):
|
||||
try:
|
||||
return cursor.execute(query, parameters)
|
||||
except OperationalError:
|
||||
logging.error("Error connecting to MySQL on %s", self.host)
|
||||
self.close()
|
||||
raise
|
||||
|
||||
|
||||
class Row(dict):
|
||||
"""A dict that allows for object-like property access syntax."""
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
# Fix the access conversions to properly recognize unicode/binary
|
||||
FIELD_TYPE = MySQLdb.constants.FIELD_TYPE
|
||||
FLAG = MySQLdb.constants.FLAG
|
||||
CONVERSIONS = copy.copy(MySQLdb.converters.conversions)
|
||||
|
||||
field_types = [FIELD_TYPE.BLOB, FIELD_TYPE.STRING, FIELD_TYPE.VAR_STRING]
|
||||
if 'VARCHAR' in vars(FIELD_TYPE):
|
||||
field_types.append(FIELD_TYPE.VARCHAR)
|
||||
|
||||
for field_type in field_types:
|
||||
CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type]
|
||||
|
||||
|
||||
# Alias some common MySQL exceptions
|
||||
IntegrityError = MySQLdb.IntegrityError
|
||||
OperationalError = MySQLdb.OperationalError
|
||||
112
libs/tornado/epoll.c
Normal file
112
libs/tornado/epoll.c
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "Python.h"
|
||||
#include <string.h>
|
||||
#include <sys/epoll.h>
|
||||
|
||||
#define MAX_EVENTS 24
|
||||
|
||||
/*
|
||||
* Simple wrapper around epoll_create.
|
||||
*/
|
||||
static PyObject* _epoll_create(void) {
|
||||
int fd = epoll_create(MAX_EVENTS);
|
||||
if (fd == -1) {
|
||||
PyErr_SetFromErrno(PyExc_Exception);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyInt_FromLong(fd);
|
||||
}
|
||||
|
||||
/*
|
||||
* Simple wrapper around epoll_ctl. We throw an exception if the call fails
|
||||
* rather than returning the error code since it is an infrequent (and likely
|
||||
* catastrophic) event when it does happen.
|
||||
*/
|
||||
static PyObject* _epoll_ctl(PyObject* self, PyObject* args) {
|
||||
int epfd, op, fd, events;
|
||||
struct epoll_event event;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "iiiI", &epfd, &op, &fd, &events)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.events = events;
|
||||
event.data.fd = fd;
|
||||
if (epoll_ctl(epfd, op, fd, &event) == -1) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
/*
|
||||
* Simple wrapper around epoll_wait. We return None if the call times out and
|
||||
* throw an exception if an error occurs. Otherwise, we return a list of
|
||||
* (fd, event) tuples.
|
||||
*/
|
||||
static PyObject* _epoll_wait(PyObject* self, PyObject* args) {
|
||||
struct epoll_event events[MAX_EVENTS];
|
||||
int epfd, timeout, num_events, i;
|
||||
PyObject* list;
|
||||
PyObject* tuple;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "ii", &epfd, &timeout)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
num_events = epoll_wait(epfd, events, MAX_EVENTS, timeout);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (num_events == -1) {
|
||||
PyErr_SetFromErrno(PyExc_Exception);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
list = PyList_New(num_events);
|
||||
for (i = 0; i < num_events; i++) {
|
||||
tuple = PyTuple_New(2);
|
||||
PyTuple_SET_ITEM(tuple, 0, PyInt_FromLong(events[i].data.fd));
|
||||
PyTuple_SET_ITEM(tuple, 1, PyInt_FromLong(events[i].events));
|
||||
PyList_SET_ITEM(list, i, tuple);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/*
|
||||
* Our method declararations
|
||||
*/
|
||||
static PyMethodDef kEpollMethods[] = {
|
||||
{"epoll_create", (PyCFunction)_epoll_create, METH_NOARGS,
|
||||
"Create an epoll file descriptor"},
|
||||
{"epoll_ctl", _epoll_ctl, METH_VARARGS,
|
||||
"Control an epoll file descriptor"},
|
||||
{"epoll_wait", _epoll_wait, METH_VARARGS,
|
||||
"Wait for events on an epoll file descriptor"},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
/*
|
||||
* Module initialization
|
||||
*/
|
||||
PyMODINIT_FUNC initepoll(void) {
|
||||
Py_InitModule("epoll", kEpollMethods);
|
||||
}
|
||||
327
libs/tornado/escape.py
Normal file
327
libs/tornado/escape.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Escaping/unescaping methods for HTML, JSON, URLs, and others.
|
||||
|
||||
Also includes a few other miscellaneous string manipulation functions that
|
||||
have crept in over time.
|
||||
"""
|
||||
|
||||
import htmlentitydefs
|
||||
import re
|
||||
import sys
|
||||
import urllib
|
||||
|
||||
# Python3 compatibility: On python2.5, introduce the bytes alias from 2.6
|
||||
try: bytes
|
||||
except Exception: bytes = str
|
||||
|
||||
try:
|
||||
from urlparse import parse_qs # Python 2.6+
|
||||
except ImportError:
|
||||
from cgi import parse_qs
|
||||
|
||||
# json module is in the standard library as of python 2.6; fall back to
|
||||
# simplejson if present for older versions.
|
||||
try:
|
||||
import json
|
||||
assert hasattr(json, "loads") and hasattr(json, "dumps")
|
||||
_json_decode = json.loads
|
||||
_json_encode = json.dumps
|
||||
except Exception:
|
||||
try:
|
||||
import simplejson
|
||||
_json_decode = lambda s: simplejson.loads(_unicode(s))
|
||||
_json_encode = lambda v: simplejson.dumps(v)
|
||||
except ImportError:
|
||||
try:
|
||||
# For Google AppEngine
|
||||
from django.utils import simplejson
|
||||
_json_decode = lambda s: simplejson.loads(_unicode(s))
|
||||
_json_encode = lambda v: simplejson.dumps(v)
|
||||
except ImportError:
|
||||
def _json_decode(s):
|
||||
raise NotImplementedError(
|
||||
"A JSON parser is required, e.g., simplejson at "
|
||||
"http://pypi.python.org/pypi/simplejson/")
|
||||
_json_encode = _json_decode
|
||||
|
||||
|
||||
_XHTML_ESCAPE_RE = re.compile('[&<>"]')
|
||||
_XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'}
|
||||
def xhtml_escape(value):
|
||||
"""Escapes a string so it is valid within XML or XHTML."""
|
||||
return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)],
|
||||
to_basestring(value))
|
||||
|
||||
|
||||
def xhtml_unescape(value):
|
||||
"""Un-escapes an XML-escaped string."""
|
||||
return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
|
||||
|
||||
|
||||
def json_encode(value):
|
||||
"""JSON-encodes the given Python object."""
|
||||
# JSON permits but does not require forward slashes to be escaped.
|
||||
# This is useful when json data is emitted in a <script> tag
|
||||
# in HTML, as it prevents </script> tags from prematurely terminating
|
||||
# the javscript. Some json libraries do this escaping by default,
|
||||
# although python's standard library does not, so we do it here.
|
||||
# http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped
|
||||
return _json_encode(recursive_unicode(value)).replace("</", "<\\/")
|
||||
|
||||
|
||||
def json_decode(value):
|
||||
"""Returns Python objects for the given JSON string."""
|
||||
return _json_decode(to_basestring(value))
|
||||
|
||||
|
||||
def squeeze(value):
|
||||
"""Replace all sequences of whitespace chars with a single space."""
|
||||
return re.sub(r"[\x00-\x20]+", " ", value).strip()
|
||||
|
||||
|
||||
def url_escape(value):
|
||||
"""Returns a valid URL-encoded version of the given value."""
|
||||
return urllib.quote_plus(utf8(value))
|
||||
|
||||
# python 3 changed things around enough that we need two separate
|
||||
# implementations of url_unescape. We also need our own implementation
|
||||
# of parse_qs since python 3's version insists on decoding everything.
|
||||
if sys.version_info[0] < 3:
|
||||
def url_unescape(value, encoding='utf-8'):
|
||||
"""Decodes the given value from a URL.
|
||||
|
||||
The argument may be either a byte or unicode string.
|
||||
|
||||
If encoding is None, the result will be a byte string. Otherwise,
|
||||
the result is a unicode string in the specified encoding.
|
||||
"""
|
||||
if encoding is None:
|
||||
return urllib.unquote_plus(utf8(value))
|
||||
else:
|
||||
return unicode(urllib.unquote_plus(utf8(value)), encoding)
|
||||
|
||||
parse_qs_bytes = parse_qs
|
||||
else:
|
||||
def url_unescape(value, encoding='utf-8'):
|
||||
"""Decodes the given value from a URL.
|
||||
|
||||
The argument may be either a byte or unicode string.
|
||||
|
||||
If encoding is None, the result will be a byte string. Otherwise,
|
||||
the result is a unicode string in the specified encoding.
|
||||
"""
|
||||
if encoding is None:
|
||||
return urllib.parse.unquote_to_bytes(value)
|
||||
else:
|
||||
return urllib.unquote_plus(to_basestring(value), encoding=encoding)
|
||||
|
||||
def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False):
|
||||
"""Parses a query string like urlparse.parse_qs, but returns the
|
||||
values as byte strings.
|
||||
|
||||
Keys still become type str (interpreted as latin1 in python3!)
|
||||
because it's too painful to keep them as byte strings in
|
||||
python3 and in practice they're nearly always ascii anyway.
|
||||
"""
|
||||
# This is gross, but python3 doesn't give us another way.
|
||||
# Latin1 is the universal donor of character encodings.
|
||||
result = parse_qs(qs, keep_blank_values, strict_parsing,
|
||||
encoding='latin1', errors='strict')
|
||||
encoded = {}
|
||||
for k,v in result.iteritems():
|
||||
encoded[k] = [i.encode('latin1') for i in v]
|
||||
return encoded
|
||||
|
||||
|
||||
|
||||
_UTF8_TYPES = (bytes, type(None))
|
||||
def utf8(value):
|
||||
"""Converts a string argument to a byte string.
|
||||
|
||||
If the argument is already a byte string or None, it is returned unchanged.
|
||||
Otherwise it must be a unicode string and is encoded as utf8.
|
||||
"""
|
||||
if isinstance(value, _UTF8_TYPES):
|
||||
return value
|
||||
assert isinstance(value, unicode)
|
||||
return value.encode("utf-8")
|
||||
|
||||
_TO_UNICODE_TYPES = (unicode, type(None))
|
||||
def to_unicode(value):
|
||||
"""Converts a string argument to a unicode string.
|
||||
|
||||
If the argument is already a unicode string or None, it is returned
|
||||
unchanged. Otherwise it must be a byte string and is decoded as utf8.
|
||||
"""
|
||||
if isinstance(value, _TO_UNICODE_TYPES):
|
||||
return value
|
||||
assert isinstance(value, bytes)
|
||||
return value.decode("utf-8")
|
||||
|
||||
# to_unicode was previously named _unicode not because it was private,
|
||||
# but to avoid conflicts with the built-in unicode() function/type
|
||||
_unicode = to_unicode
|
||||
|
||||
# When dealing with the standard library across python 2 and 3 it is
|
||||
# sometimes useful to have a direct conversion to the native string type
|
||||
if str is unicode:
|
||||
native_str = to_unicode
|
||||
else:
|
||||
native_str = utf8
|
||||
|
||||
_BASESTRING_TYPES = (basestring, type(None))
|
||||
def to_basestring(value):
|
||||
"""Converts a string argument to a subclass of basestring.
|
||||
|
||||
In python2, byte and unicode strings are mostly interchangeable,
|
||||
so functions that deal with a user-supplied argument in combination
|
||||
with ascii string constants can use either and should return the type
|
||||
the user supplied. In python3, the two types are not interchangeable,
|
||||
so this method is needed to convert byte strings to unicode.
|
||||
"""
|
||||
if isinstance(value, _BASESTRING_TYPES):
|
||||
return value
|
||||
assert isinstance(value, bytes)
|
||||
return value.decode("utf-8")
|
||||
|
||||
def recursive_unicode(obj):
|
||||
"""Walks a simple data structure, converting byte strings to unicode.
|
||||
|
||||
Supports lists, tuples, and dictionaries.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return dict((recursive_unicode(k), recursive_unicode(v)) for (k,v) in obj.iteritems())
|
||||
elif isinstance(obj, list):
|
||||
return list(recursive_unicode(i) for i in obj)
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(recursive_unicode(i) for i in obj)
|
||||
elif isinstance(obj, bytes):
|
||||
return to_unicode(obj)
|
||||
else:
|
||||
return obj
|
||||
|
||||
# I originally used the regex from
|
||||
# http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||
# but it gets all exponential on certain patterns (such as too many trailing
|
||||
# dots), causing the regex matcher to never return.
|
||||
# This regex should avoid those problems.
|
||||
_URL_RE = re.compile(ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)""")
|
||||
|
||||
|
||||
def linkify(text, shorten=False, extra_params="",
|
||||
require_protocol=False, permitted_protocols=["http", "https"]):
|
||||
"""Converts plain text into HTML with links.
|
||||
|
||||
For example: ``linkify("Hello http://tornadoweb.org!")`` would return
|
||||
``Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!``
|
||||
|
||||
Parameters:
|
||||
|
||||
shorten: Long urls will be shortened for display.
|
||||
|
||||
extra_params: Extra text to include in the link tag,
|
||||
e.g. linkify(text, extra_params='rel="nofollow" class="external"')
|
||||
|
||||
require_protocol: Only linkify urls which include a protocol. If this is
|
||||
False, urls such as www.facebook.com will also be linkified.
|
||||
|
||||
permitted_protocols: List (or set) of protocols which should be linkified,
|
||||
e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]).
|
||||
It is very unsafe to include protocols such as "javascript".
|
||||
"""
|
||||
if extra_params:
|
||||
extra_params = " " + extra_params.strip()
|
||||
|
||||
def make_link(m):
|
||||
url = m.group(1)
|
||||
proto = m.group(2)
|
||||
if require_protocol and not proto:
|
||||
return url # not protocol, no linkify
|
||||
|
||||
if proto and proto not in permitted_protocols:
|
||||
return url # bad protocol, no linkify
|
||||
|
||||
href = m.group(1)
|
||||
if not proto:
|
||||
href = "http://" + href # no proto specified, use http
|
||||
|
||||
params = extra_params
|
||||
|
||||
# clip long urls. max_len is just an approximation
|
||||
max_len = 30
|
||||
if shorten and len(url) > max_len:
|
||||
before_clip = url
|
||||
if proto:
|
||||
proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for :
|
||||
else:
|
||||
proto_len = 0
|
||||
|
||||
parts = url[proto_len:].split("/")
|
||||
if len(parts) > 1:
|
||||
# Grab the whole host part plus the first bit of the path
|
||||
# The path is usually not that interesting once shortened
|
||||
# (no more slug, etc), so it really just provides a little
|
||||
# extra indication of shortening.
|
||||
url = url[:proto_len] + parts[0] + "/" + \
|
||||
parts[1][:8].split('?')[0].split('.')[0]
|
||||
|
||||
if len(url) > max_len * 1.5: # still too long
|
||||
url = url[:max_len]
|
||||
|
||||
if url != before_clip:
|
||||
amp = url.rfind('&')
|
||||
# avoid splitting html char entities
|
||||
if amp > max_len - 5:
|
||||
url = url[:amp]
|
||||
url += "..."
|
||||
|
||||
if len(url) >= len(before_clip):
|
||||
url = before_clip
|
||||
else:
|
||||
# full url is visible on mouse-over (for those who don't
|
||||
# have a status bar, such as Safari by default)
|
||||
params += ' title="%s"' % href
|
||||
|
||||
return u'<a href="%s"%s>%s</a>' % (href, params, url)
|
||||
|
||||
# First HTML-escape so that our strings are all safe.
|
||||
# The regex is modified to avoid character entites other than & so
|
||||
# that we won't pick up ", etc.
|
||||
text = _unicode(xhtml_escape(text))
|
||||
return _URL_RE.sub(make_link, text)
|
||||
|
||||
|
||||
def _convert_entity(m):
|
||||
if m.group(1) == "#":
|
||||
try:
|
||||
return unichr(int(m.group(2)))
|
||||
except ValueError:
|
||||
return "&#%s;" % m.group(2)
|
||||
try:
|
||||
return _HTML_UNICODE_MAP[m.group(2)]
|
||||
except KeyError:
|
||||
return "&%s;" % m.group(2)
|
||||
|
||||
|
||||
def _build_unicode_map():
|
||||
unicode_map = {}
|
||||
for name, value in htmlentitydefs.name2codepoint.iteritems():
|
||||
unicode_map[name] = unichr(value)
|
||||
return unicode_map
|
||||
|
||||
_HTML_UNICODE_MAP = _build_unicode_map()
|
||||
382
libs/tornado/gen.py
Normal file
382
libs/tornado/gen.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""``tornado.gen`` is a generator-based interface to make it easier to
|
||||
work in an asynchronous environment. Code using the ``gen`` module
|
||||
is technically asynchronous, but it is written as a single generator
|
||||
instead of a collection of separate functions.
|
||||
|
||||
For example, the following asynchronous handler::
|
||||
|
||||
class AsyncHandler(RequestHandler):
|
||||
@asynchronous
|
||||
def get(self):
|
||||
http_client = AsyncHTTPClient()
|
||||
http_client.fetch("http://example.com",
|
||||
callback=self.on_fetch)
|
||||
|
||||
def on_fetch(self, response):
|
||||
do_something_with_response(response)
|
||||
self.render("template.html")
|
||||
|
||||
could be written with ``gen`` as::
|
||||
|
||||
class GenAsyncHandler(RequestHandler):
|
||||
@asynchronous
|
||||
@gen.engine
|
||||
def get(self):
|
||||
http_client = AsyncHTTPClient()
|
||||
response = yield gen.Task(http_client.fetch, "http://example.com")
|
||||
do_something_with_response(response)
|
||||
self.render("template.html")
|
||||
|
||||
`Task` works with any function that takes a ``callback`` keyword
|
||||
argument. You can also yield a list of ``Tasks``, which will be
|
||||
started at the same time and run in parallel; a list of results will
|
||||
be returned when they are all finished::
|
||||
|
||||
def get(self):
|
||||
http_client = AsyncHTTPClient()
|
||||
response1, response2 = yield [gen.Task(http_client.fetch, url1),
|
||||
gen.Task(http_client.fetch, url2)]
|
||||
|
||||
For more complicated interfaces, `Task` can be split into two parts:
|
||||
`Callback` and `Wait`::
|
||||
|
||||
class GenAsyncHandler2(RequestHandler):
|
||||
@asynchronous
|
||||
@gen.engine
|
||||
def get(self):
|
||||
http_client = AsyncHTTPClient()
|
||||
http_client.fetch("http://example.com",
|
||||
callback=(yield gen.Callback("key"))
|
||||
response = yield gen.Wait("key")
|
||||
do_something_with_response(response)
|
||||
self.render("template.html")
|
||||
|
||||
The ``key`` argument to `Callback` and `Wait` allows for multiple
|
||||
asynchronous operations to be started at different times and proceed
|
||||
in parallel: yield several callbacks with different keys, then wait
|
||||
for them once all the async operations have started.
|
||||
|
||||
The result of a `Wait` or `Task` yield expression depends on how the callback
|
||||
was run. If it was called with no arguments, the result is ``None``. If
|
||||
it was called with one argument, the result is that argument. If it was
|
||||
called with more than one argument or any keyword arguments, the result
|
||||
is an `Arguments` object, which is a named tuple ``(args, kwargs)``.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
import functools
|
||||
import operator
|
||||
import sys
|
||||
import types
|
||||
|
||||
from tornado.stack_context import ExceptionStackContext
|
||||
|
||||
class KeyReuseError(Exception): pass
|
||||
class UnknownKeyError(Exception): pass
|
||||
class LeakedCallbackError(Exception): pass
|
||||
class BadYieldError(Exception): pass
|
||||
|
||||
def engine(func):
|
||||
"""Decorator for asynchronous generators.
|
||||
|
||||
Any generator that yields objects from this module must be wrapped
|
||||
in this decorator. The decorator only works on functions that are
|
||||
already asynchronous. For `~tornado.web.RequestHandler`
|
||||
``get``/``post``/etc methods, this means that both the
|
||||
`tornado.web.asynchronous` and `tornado.gen.engine` decorators
|
||||
must be used (for proper exception handling, ``asynchronous``
|
||||
should come before ``gen.engine``). In most other cases, it means
|
||||
that it doesn't make sense to use ``gen.engine`` on functions that
|
||||
don't already take a callback argument.
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
runner = None
|
||||
def handle_exception(typ, value, tb):
|
||||
# if the function throws an exception before its first "yield"
|
||||
# (or is not a generator at all), the Runner won't exist yet.
|
||||
# However, in that case we haven't reached anything asynchronous
|
||||
# yet, so we can just let the exception propagate.
|
||||
if runner is not None:
|
||||
return runner.handle_exception(typ, value, tb)
|
||||
return False
|
||||
with ExceptionStackContext(handle_exception):
|
||||
gen = func(*args, **kwargs)
|
||||
if isinstance(gen, types.GeneratorType):
|
||||
runner = Runner(gen)
|
||||
runner.run()
|
||||
return
|
||||
assert gen is None, gen
|
||||
# no yield, so we're done
|
||||
return wrapper
|
||||
|
||||
class YieldPoint(object):
|
||||
"""Base class for objects that may be yielded from the generator."""
|
||||
def start(self, runner):
|
||||
"""Called by the runner after the generator has yielded.
|
||||
|
||||
No other methods will be called on this object before ``start``.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_ready(self):
|
||||
"""Called by the runner to determine whether to resume the generator.
|
||||
|
||||
Returns a boolean; may be called more than once.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_result(self):
|
||||
"""Returns the value to use as the result of the yield expression.
|
||||
|
||||
This method will only be called once, and only after `is_ready`
|
||||
has returned true.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
class Callback(YieldPoint):
|
||||
"""Returns a callable object that will allow a matching `Wait` to proceed.
|
||||
|
||||
The key may be any value suitable for use as a dictionary key, and is
|
||||
used to match ``Callbacks`` to their corresponding ``Waits``. The key
|
||||
must be unique among outstanding callbacks within a single run of the
|
||||
generator function, but may be reused across different runs of the same
|
||||
function (so constants generally work fine).
|
||||
|
||||
The callback may be called with zero or one arguments; if an argument
|
||||
is given it will be returned by `Wait`.
|
||||
"""
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def start(self, runner):
|
||||
self.runner = runner
|
||||
runner.register_callback(self.key)
|
||||
|
||||
def is_ready(self):
|
||||
return True
|
||||
|
||||
def get_result(self):
|
||||
return self.runner.result_callback(self.key)
|
||||
|
||||
class Wait(YieldPoint):
|
||||
"""Returns the argument passed to the result of a previous `Callback`."""
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def start(self, runner):
|
||||
self.runner = runner
|
||||
|
||||
def is_ready(self):
|
||||
return self.runner.is_ready(self.key)
|
||||
|
||||
def get_result(self):
|
||||
return self.runner.pop_result(self.key)
|
||||
|
||||
class WaitAll(YieldPoint):
|
||||
"""Returns the results of multiple previous `Callbacks`.
|
||||
|
||||
The argument is a sequence of `Callback` keys, and the result is
|
||||
a list of results in the same order.
|
||||
|
||||
`WaitAll` is equivalent to yielding a list of `Wait` objects.
|
||||
"""
|
||||
def __init__(self, keys):
|
||||
self.keys = keys
|
||||
|
||||
def start(self, runner):
|
||||
self.runner = runner
|
||||
|
||||
def is_ready(self):
|
||||
return all(self.runner.is_ready(key) for key in self.keys)
|
||||
|
||||
def get_result(self):
|
||||
return [self.runner.pop_result(key) for key in self.keys]
|
||||
|
||||
|
||||
class Task(YieldPoint):
|
||||
"""Runs a single asynchronous operation.
|
||||
|
||||
Takes a function (and optional additional arguments) and runs it with
|
||||
those arguments plus a ``callback`` keyword argument. The argument passed
|
||||
to the callback is returned as the result of the yield expression.
|
||||
|
||||
A `Task` is equivalent to a `Callback`/`Wait` pair (with a unique
|
||||
key generated automatically)::
|
||||
|
||||
result = yield gen.Task(func, args)
|
||||
|
||||
func(args, callback=(yield gen.Callback(key)))
|
||||
result = yield gen.Wait(key)
|
||||
"""
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
assert "callback" not in kwargs
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.func = func
|
||||
|
||||
def start(self, runner):
|
||||
self.runner = runner
|
||||
self.key = object()
|
||||
runner.register_callback(self.key)
|
||||
self.kwargs["callback"] = runner.result_callback(self.key)
|
||||
self.func(*self.args, **self.kwargs)
|
||||
|
||||
def is_ready(self):
|
||||
return self.runner.is_ready(self.key)
|
||||
|
||||
def get_result(self):
|
||||
return self.runner.pop_result(self.key)
|
||||
|
||||
class Multi(YieldPoint):
|
||||
"""Runs multiple asynchronous operations in parallel.
|
||||
|
||||
Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of
|
||||
their responses. It is not necessary to call `Multi` explicitly,
|
||||
since the engine will do so automatically when the generator yields
|
||||
a list of ``YieldPoints``.
|
||||
"""
|
||||
def __init__(self, children):
|
||||
assert all(isinstance(i, YieldPoint) for i in children)
|
||||
self.children = children
|
||||
|
||||
def start(self, runner):
|
||||
for i in self.children:
|
||||
i.start(runner)
|
||||
|
||||
def is_ready(self):
|
||||
return all(i.is_ready() for i in self.children)
|
||||
|
||||
def get_result(self):
|
||||
return [i.get_result() for i in self.children]
|
||||
|
||||
class _NullYieldPoint(YieldPoint):
|
||||
def start(self, runner):
|
||||
pass
|
||||
def is_ready(self):
|
||||
return True
|
||||
def get_result(self):
|
||||
return None
|
||||
|
||||
class Runner(object):
|
||||
"""Internal implementation of `tornado.gen.engine`.
|
||||
|
||||
Maintains information about pending callbacks and their results.
|
||||
"""
|
||||
def __init__(self, gen):
|
||||
self.gen = gen
|
||||
self.yield_point = _NullYieldPoint()
|
||||
self.pending_callbacks = set()
|
||||
self.results = {}
|
||||
self.running = False
|
||||
self.finished = False
|
||||
self.exc_info = None
|
||||
self.had_exception = False
|
||||
|
||||
def register_callback(self, key):
|
||||
"""Adds ``key`` to the list of callbacks."""
|
||||
if key in self.pending_callbacks:
|
||||
raise KeyReuseError("key %r is already pending" % key)
|
||||
self.pending_callbacks.add(key)
|
||||
|
||||
def is_ready(self, key):
|
||||
"""Returns true if a result is available for ``key``."""
|
||||
if key not in self.pending_callbacks:
|
||||
raise UnknownKeyError("key %r is not pending" % key)
|
||||
return key in self.results
|
||||
|
||||
def set_result(self, key, result):
|
||||
"""Sets the result for ``key`` and attempts to resume the generator."""
|
||||
self.results[key] = result
|
||||
self.run()
|
||||
|
||||
def pop_result(self, key):
|
||||
"""Returns the result for ``key`` and unregisters it."""
|
||||
self.pending_callbacks.remove(key)
|
||||
return self.results.pop(key)
|
||||
|
||||
def run(self):
|
||||
"""Starts or resumes the generator, running until it reaches a
|
||||
yield point that is not ready.
|
||||
"""
|
||||
if self.running or self.finished:
|
||||
return
|
||||
try:
|
||||
self.running = True
|
||||
while True:
|
||||
if self.exc_info is None:
|
||||
try:
|
||||
if not self.yield_point.is_ready():
|
||||
return
|
||||
next = self.yield_point.get_result()
|
||||
except Exception:
|
||||
self.exc_info = sys.exc_info()
|
||||
try:
|
||||
if self.exc_info is not None:
|
||||
self.had_exception = True
|
||||
exc_info = self.exc_info
|
||||
self.exc_info = None
|
||||
yielded = self.gen.throw(*exc_info)
|
||||
else:
|
||||
yielded = self.gen.send(next)
|
||||
except StopIteration:
|
||||
self.finished = True
|
||||
if self.pending_callbacks and not self.had_exception:
|
||||
# If we ran cleanly without waiting on all callbacks
|
||||
# raise an error (really more of a warning). If we
|
||||
# had an exception then some callbacks may have been
|
||||
# orphaned, so skip the check in that case.
|
||||
raise LeakedCallbackError(
|
||||
"finished without waiting for callbacks %r" %
|
||||
self.pending_callbacks)
|
||||
return
|
||||
except Exception:
|
||||
self.finished = True
|
||||
raise
|
||||
if isinstance(yielded, list):
|
||||
yielded = Multi(yielded)
|
||||
if isinstance(yielded, YieldPoint):
|
||||
self.yield_point = yielded
|
||||
try:
|
||||
self.yield_point.start(self)
|
||||
except Exception:
|
||||
self.exc_info = sys.exc_info()
|
||||
else:
|
||||
self.exc_info = (BadYieldError("yielded unknown object %r" % yielded),)
|
||||
finally:
|
||||
self.running = False
|
||||
|
||||
def result_callback(self, key):
|
||||
def inner(*args, **kwargs):
|
||||
if kwargs or len(args) > 1:
|
||||
result = Arguments(args, kwargs)
|
||||
elif args:
|
||||
result = args[0]
|
||||
else:
|
||||
result = None
|
||||
self.set_result(key, result)
|
||||
return inner
|
||||
|
||||
def handle_exception(self, typ, value, tb):
|
||||
if not self.running and not self.finished:
|
||||
self.exc_info = (typ, value, tb)
|
||||
self.run()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# in python 2.6+ this could be a collections.namedtuple
|
||||
class Arguments(tuple):
|
||||
"""The result of a yield expression whose callback had more than one
|
||||
argument (or keyword arguments).
|
||||
|
||||
The `Arguments` object can be used as a tuple ``(args, kwargs)``
|
||||
or an object with attributes ``args`` and ``kwargs``.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, args, kwargs):
|
||||
return tuple.__new__(cls, (args, kwargs))
|
||||
|
||||
args = property(operator.itemgetter(0))
|
||||
kwargs = property(operator.itemgetter(1))
|
||||
417
libs/tornado/httpclient.py
Normal file
417
libs/tornado/httpclient.py
Normal file
@@ -0,0 +1,417 @@
|
||||
"""Blocking and non-blocking HTTP client interfaces.
|
||||
|
||||
This module defines a common interface shared by two implementations,
|
||||
`simple_httpclient` and `curl_httpclient`. Applications may either
|
||||
instantiate their chosen implementation class directly or use the
|
||||
`AsyncHTTPClient` class from this module, which selects an implementation
|
||||
that can be overridden with the `AsyncHTTPClient.configure` method.
|
||||
|
||||
The default implementation is `simple_httpclient`, and this is expected
|
||||
to be suitable for most users' needs. However, some applications may wish
|
||||
to switch to `curl_httpclient` for reasons such as the following:
|
||||
|
||||
* `curl_httpclient` has some features not found in `simple_httpclient`,
|
||||
including support for HTTP proxies and the ability to use a specified
|
||||
network interface.
|
||||
|
||||
* `curl_httpclient` is more likely to be compatible with sites that are
|
||||
not-quite-compliant with the HTTP spec, or sites that use little-exercised
|
||||
features of HTTP.
|
||||
|
||||
* `simple_httpclient` only supports SSL on Python 2.6 and above.
|
||||
|
||||
* `curl_httpclient` is faster
|
||||
|
||||
* `curl_httpclient` was the default prior to Tornado 2.0.
|
||||
|
||||
Note that if you are using `curl_httpclient`, it is highly recommended that
|
||||
you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum
|
||||
supported version is 7.18.2, and the recommended version is 7.21.1 or newer.
|
||||
"""
|
||||
|
||||
import calendar
|
||||
import email.utils
|
||||
import httplib
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from tornado.escape import utf8
|
||||
from tornado import httputil
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.util import import_object, bytes_type
|
||||
|
||||
class HTTPClient(object):
|
||||
"""A blocking HTTP client.
|
||||
|
||||
This interface is provided for convenience and testing; most applications
|
||||
that are running an IOLoop will want to use `AsyncHTTPClient` instead.
|
||||
Typical usage looks like this::
|
||||
|
||||
http_client = httpclient.HTTPClient()
|
||||
try:
|
||||
response = http_client.fetch("http://www.google.com/")
|
||||
print response.body
|
||||
except httpclient.HTTPError, e:
|
||||
print "Error:", e
|
||||
"""
|
||||
def __init__(self, async_client_class=None):
|
||||
self._io_loop = IOLoop()
|
||||
if async_client_class is None:
|
||||
async_client_class = AsyncHTTPClient
|
||||
self._async_client = async_client_class(self._io_loop)
|
||||
self._response = None
|
||||
self._closed = False
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""Closes the HTTPClient, freeing any resources used."""
|
||||
if not self._closed:
|
||||
self._async_client.close()
|
||||
self._io_loop.close()
|
||||
self._closed = True
|
||||
|
||||
def fetch(self, request, **kwargs):
|
||||
"""Executes a request, returning an `HTTPResponse`.
|
||||
|
||||
The request may be either a string URL or an `HTTPRequest` object.
|
||||
If it is a string, we construct an `HTTPRequest` using any additional
|
||||
kwargs: ``HTTPRequest(request, **kwargs)``
|
||||
|
||||
If an error occurs during the fetch, we raise an `HTTPError`.
|
||||
"""
|
||||
def callback(response):
|
||||
self._response = response
|
||||
self._io_loop.stop()
|
||||
self._async_client.fetch(request, callback, **kwargs)
|
||||
self._io_loop.start()
|
||||
response = self._response
|
||||
self._response = None
|
||||
response.rethrow()
|
||||
return response
|
||||
|
||||
class AsyncHTTPClient(object):
|
||||
"""An non-blocking HTTP client.
|
||||
|
||||
Example usage::
|
||||
|
||||
import ioloop
|
||||
|
||||
def handle_request(response):
|
||||
if response.error:
|
||||
print "Error:", response.error
|
||||
else:
|
||||
print response.body
|
||||
ioloop.IOLoop.instance().stop()
|
||||
|
||||
http_client = httpclient.AsyncHTTPClient()
|
||||
http_client.fetch("http://www.google.com/", handle_request)
|
||||
ioloop.IOLoop.instance().start()
|
||||
|
||||
The constructor for this class is magic in several respects: It actually
|
||||
creates an instance of an implementation-specific subclass, and instances
|
||||
are reused as a kind of pseudo-singleton (one per IOLoop). The keyword
|
||||
argument force_instance=True can be used to suppress this singleton
|
||||
behavior. Constructor arguments other than io_loop and force_instance
|
||||
are deprecated. The implementation subclass as well as arguments to
|
||||
its constructor can be set with the static method configure()
|
||||
"""
|
||||
_impl_class = None
|
||||
_impl_kwargs = None
|
||||
|
||||
@classmethod
|
||||
def _async_clients(cls):
|
||||
assert cls is not AsyncHTTPClient, "should only be called on subclasses"
|
||||
if not hasattr(cls, '_async_client_dict'):
|
||||
cls._async_client_dict = weakref.WeakKeyDictionary()
|
||||
return cls._async_client_dict
|
||||
|
||||
def __new__(cls, io_loop=None, max_clients=10, force_instance=False,
|
||||
**kwargs):
|
||||
io_loop = io_loop or IOLoop.instance()
|
||||
if cls is AsyncHTTPClient:
|
||||
if cls._impl_class is None:
|
||||
from tornado.simple_httpclient import SimpleAsyncHTTPClient
|
||||
AsyncHTTPClient._impl_class = SimpleAsyncHTTPClient
|
||||
impl = AsyncHTTPClient._impl_class
|
||||
else:
|
||||
impl = cls
|
||||
if io_loop in impl._async_clients() and not force_instance:
|
||||
return impl._async_clients()[io_loop]
|
||||
else:
|
||||
instance = super(AsyncHTTPClient, cls).__new__(impl)
|
||||
args = {}
|
||||
if cls._impl_kwargs:
|
||||
args.update(cls._impl_kwargs)
|
||||
args.update(kwargs)
|
||||
instance.initialize(io_loop, max_clients, **args)
|
||||
if not force_instance:
|
||||
impl._async_clients()[io_loop] = instance
|
||||
return instance
|
||||
|
||||
def close(self):
|
||||
"""Destroys this http client, freeing any file descriptors used.
|
||||
Not needed in normal use, but may be helpful in unittests that
|
||||
create and destroy http clients. No other methods may be called
|
||||
on the AsyncHTTPClient after close().
|
||||
"""
|
||||
if self._async_clients().get(self.io_loop) is self:
|
||||
del self._async_clients()[self.io_loop]
|
||||
|
||||
def fetch(self, request, callback, **kwargs):
|
||||
"""Executes a request, calling callback with an `HTTPResponse`.
|
||||
|
||||
The request may be either a string URL or an `HTTPRequest` object.
|
||||
If it is a string, we construct an `HTTPRequest` using any additional
|
||||
kwargs: ``HTTPRequest(request, **kwargs)``
|
||||
|
||||
If an error occurs during the fetch, the HTTPResponse given to the
|
||||
callback has a non-None error attribute that contains the exception
|
||||
encountered during the request. You can call response.rethrow() to
|
||||
throw the exception (if any) in the callback.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def configure(impl, **kwargs):
|
||||
"""Configures the AsyncHTTPClient subclass to use.
|
||||
|
||||
AsyncHTTPClient() actually creates an instance of a subclass.
|
||||
This method may be called with either a class object or the
|
||||
fully-qualified name of such a class (or None to use the default,
|
||||
SimpleAsyncHTTPClient)
|
||||
|
||||
If additional keyword arguments are given, they will be passed
|
||||
to the constructor of each subclass instance created. The
|
||||
keyword argument max_clients determines the maximum number of
|
||||
simultaneous fetch() operations that can execute in parallel
|
||||
on each IOLoop. Additional arguments may be supported depending
|
||||
on the implementation class in use.
|
||||
|
||||
Example::
|
||||
|
||||
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
|
||||
"""
|
||||
if isinstance(impl, (unicode, bytes_type)):
|
||||
impl = import_object(impl)
|
||||
if impl is not None and not issubclass(impl, AsyncHTTPClient):
|
||||
raise ValueError("Invalid AsyncHTTPClient implementation")
|
||||
AsyncHTTPClient._impl_class = impl
|
||||
AsyncHTTPClient._impl_kwargs = kwargs
|
||||
|
||||
class HTTPRequest(object):
|
||||
"""HTTP client request object."""
|
||||
def __init__(self, url, method="GET", headers=None, body=None,
|
||||
auth_username=None, auth_password=None,
|
||||
connect_timeout=20.0, request_timeout=20.0,
|
||||
if_modified_since=None, follow_redirects=True,
|
||||
max_redirects=5, user_agent=None, use_gzip=True,
|
||||
network_interface=None, streaming_callback=None,
|
||||
header_callback=None, prepare_curl_callback=None,
|
||||
proxy_host=None, proxy_port=None, proxy_username=None,
|
||||
proxy_password='', allow_nonstandard_methods=False,
|
||||
validate_cert=True, ca_certs=None,
|
||||
allow_ipv6=None,
|
||||
client_key=None, client_cert=None):
|
||||
"""Creates an `HTTPRequest`.
|
||||
|
||||
All parameters except `url` are optional.
|
||||
|
||||
:arg string url: URL to fetch
|
||||
:arg string method: HTTP method, e.g. "GET" or "POST"
|
||||
:arg headers: Additional HTTP headers to pass on the request
|
||||
:type headers: `~tornado.httputil.HTTPHeaders` or `dict`
|
||||
:arg string auth_username: Username for HTTP "Basic" authentication
|
||||
:arg string auth_password: Password for HTTP "Basic" authentication
|
||||
:arg float connect_timeout: Timeout for initial connection in seconds
|
||||
:arg float request_timeout: Timeout for entire request in seconds
|
||||
:arg datetime if_modified_since: Timestamp for ``If-Modified-Since``
|
||||
header
|
||||
:arg bool follow_redirects: Should redirects be followed automatically
|
||||
or return the 3xx response?
|
||||
:arg int max_redirects: Limit for `follow_redirects`
|
||||
:arg string user_agent: String to send as ``User-Agent`` header
|
||||
:arg bool use_gzip: Request gzip encoding from the server
|
||||
:arg string network_interface: Network interface to use for request
|
||||
:arg callable streaming_callback: If set, `streaming_callback` will
|
||||
be run with each chunk of data as it is received, and
|
||||
`~HTTPResponse.body` and `~HTTPResponse.buffer` will be empty in
|
||||
the final response.
|
||||
:arg callable header_callback: If set, `header_callback` will
|
||||
be run with each header line as it is received, and
|
||||
`~HTTPResponse.headers` will be empty in the final response.
|
||||
:arg callable prepare_curl_callback: If set, will be called with
|
||||
a `pycurl.Curl` object to allow the application to make additional
|
||||
`setopt` calls.
|
||||
:arg string proxy_host: HTTP proxy hostname. To use proxies,
|
||||
`proxy_host` and `proxy_port` must be set; `proxy_username` and
|
||||
`proxy_pass` are optional. Proxies are currently only support
|
||||
with `curl_httpclient`.
|
||||
:arg int proxy_port: HTTP proxy port
|
||||
:arg string proxy_username: HTTP proxy username
|
||||
:arg string proxy_password: HTTP proxy password
|
||||
:arg bool allow_nonstandard_methods: Allow unknown values for `method`
|
||||
argument?
|
||||
:arg bool validate_cert: For HTTPS requests, validate the server's
|
||||
certificate?
|
||||
:arg string ca_certs: filename of CA certificates in PEM format,
|
||||
or None to use defaults. Note that in `curl_httpclient`, if
|
||||
any request uses a custom `ca_certs` file, they all must (they
|
||||
don't have to all use the same `ca_certs`, but it's not possible
|
||||
to mix requests with ca_certs and requests that use the defaults.
|
||||
:arg bool allow_ipv6: Use IPv6 when available? Default is false in
|
||||
`simple_httpclient` and true in `curl_httpclient`
|
||||
:arg string client_key: Filename for client SSL key, if any
|
||||
:arg string client_cert: Filename for client SSL certificate, if any
|
||||
"""
|
||||
if headers is None:
|
||||
headers = httputil.HTTPHeaders()
|
||||
if if_modified_since:
|
||||
timestamp = calendar.timegm(if_modified_since.utctimetuple())
|
||||
headers["If-Modified-Since"] = email.utils.formatdate(
|
||||
timestamp, localtime=False, usegmt=True)
|
||||
self.proxy_host = proxy_host
|
||||
self.proxy_port = proxy_port
|
||||
self.proxy_username = proxy_username
|
||||
self.proxy_password = proxy_password
|
||||
self.url = url
|
||||
self.method = method
|
||||
self.headers = headers
|
||||
self.body = utf8(body)
|
||||
self.auth_username = auth_username
|
||||
self.auth_password = auth_password
|
||||
self.connect_timeout = connect_timeout
|
||||
self.request_timeout = request_timeout
|
||||
self.follow_redirects = follow_redirects
|
||||
self.max_redirects = max_redirects
|
||||
self.user_agent = user_agent
|
||||
self.use_gzip = use_gzip
|
||||
self.network_interface = network_interface
|
||||
self.streaming_callback = streaming_callback
|
||||
self.header_callback = header_callback
|
||||
self.prepare_curl_callback = prepare_curl_callback
|
||||
self.allow_nonstandard_methods = allow_nonstandard_methods
|
||||
self.validate_cert = validate_cert
|
||||
self.ca_certs = ca_certs
|
||||
self.allow_ipv6 = allow_ipv6
|
||||
self.client_key = client_key
|
||||
self.client_cert = client_cert
|
||||
self.start_time = time.time()
|
||||
|
||||
|
||||
class HTTPResponse(object):
|
||||
"""HTTP Response object.
|
||||
|
||||
Attributes:
|
||||
|
||||
* request: HTTPRequest object
|
||||
|
||||
* code: numeric HTTP status code, e.g. 200 or 404
|
||||
|
||||
* headers: httputil.HTTPHeaders object
|
||||
|
||||
* buffer: cStringIO object for response body
|
||||
|
||||
* body: respose body as string (created on demand from self.buffer)
|
||||
|
||||
* error: Exception object, if any
|
||||
|
||||
* request_time: seconds from request start to finish
|
||||
|
||||
* time_info: dictionary of diagnostic timing information from the request.
|
||||
Available data are subject to change, but currently uses timings
|
||||
available from http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html,
|
||||
plus 'queue', which is the delay (if any) introduced by waiting for
|
||||
a slot under AsyncHTTPClient's max_clients setting.
|
||||
"""
|
||||
def __init__(self, request, code, headers={}, buffer=None,
|
||||
effective_url=None, error=None, request_time=None,
|
||||
time_info={}):
|
||||
self.request = request
|
||||
self.code = code
|
||||
self.headers = headers
|
||||
self.buffer = buffer
|
||||
self._body = None
|
||||
if effective_url is None:
|
||||
self.effective_url = request.url
|
||||
else:
|
||||
self.effective_url = effective_url
|
||||
if error is None:
|
||||
if self.code < 200 or self.code >= 300:
|
||||
self.error = HTTPError(self.code, response=self)
|
||||
else:
|
||||
self.error = None
|
||||
else:
|
||||
self.error = error
|
||||
self.request_time = request_time
|
||||
self.time_info = time_info
|
||||
|
||||
def _get_body(self):
|
||||
if self.buffer is None:
|
||||
return None
|
||||
elif self._body is None:
|
||||
self._body = self.buffer.getvalue()
|
||||
|
||||
return self._body
|
||||
|
||||
body = property(_get_body)
|
||||
|
||||
def rethrow(self):
|
||||
"""If there was an error on the request, raise an `HTTPError`."""
|
||||
if self.error:
|
||||
raise self.error
|
||||
|
||||
def __repr__(self):
|
||||
args = ",".join("%s=%r" % i for i in self.__dict__.iteritems())
|
||||
return "%s(%s)" % (self.__class__.__name__, args)
|
||||
|
||||
|
||||
class HTTPError(Exception):
|
||||
"""Exception thrown for an unsuccessful HTTP request.
|
||||
|
||||
Attributes:
|
||||
|
||||
code - HTTP error integer error code, e.g. 404. Error code 599 is
|
||||
used when no HTTP response was received, e.g. for a timeout.
|
||||
|
||||
response - HTTPResponse object, if any.
|
||||
|
||||
Note that if follow_redirects is False, redirects become HTTPErrors,
|
||||
and you can look at error.response.headers['Location'] to see the
|
||||
destination of the redirect.
|
||||
"""
|
||||
def __init__(self, code, message=None, response=None):
|
||||
self.code = code
|
||||
message = message or httplib.responses.get(code, "Unknown")
|
||||
self.response = response
|
||||
Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
|
||||
|
||||
|
||||
def main():
|
||||
from tornado.options import define, options, parse_command_line
|
||||
define("print_headers", type=bool, default=False)
|
||||
define("print_body", type=bool, default=True)
|
||||
define("follow_redirects", type=bool, default=True)
|
||||
define("validate_cert", type=bool, default=True)
|
||||
args = parse_command_line()
|
||||
client = HTTPClient()
|
||||
for arg in args:
|
||||
try:
|
||||
response = client.fetch(arg,
|
||||
follow_redirects=options.follow_redirects,
|
||||
validate_cert=options.validate_cert,
|
||||
)
|
||||
except HTTPError, e:
|
||||
if e.response is not None:
|
||||
response = e.response
|
||||
else:
|
||||
raise
|
||||
if options.print_headers:
|
||||
print response.headers
|
||||
if options.print_body:
|
||||
print response.body
|
||||
client.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
476
libs/tornado/httpserver.py
Normal file
476
libs/tornado/httpserver.py
Normal file
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A non-blocking, single-threaded HTTP server.
|
||||
|
||||
Typical applications have little direct interaction with the `HTTPServer`
|
||||
class except to start a server at the beginning of the process
|
||||
(and even that is often done indirectly via `tornado.web.Application.listen`).
|
||||
|
||||
This module also defines the `HTTPRequest` class which is exposed via
|
||||
`tornado.web.RequestHandler.request`.
|
||||
"""
|
||||
|
||||
import Cookie
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
from tornado.escape import utf8, native_str, parse_qs_bytes
|
||||
from tornado import httputil
|
||||
from tornado import iostream
|
||||
from tornado.netutil import TCPServer
|
||||
from tornado import stack_context
|
||||
from tornado.util import b, bytes_type
|
||||
|
||||
try:
|
||||
import ssl # Python 2.6+
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
class HTTPServer(TCPServer):
|
||||
r"""A non-blocking, single-threaded HTTP server.
|
||||
|
||||
A server is defined by a request callback that takes an HTTPRequest
|
||||
instance as an argument and writes a valid HTTP response with
|
||||
`HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does
|
||||
not necessarily close the connection in the case of HTTP/1.1 keep-alive
|
||||
requests). A simple example server that echoes back the URI you
|
||||
requested::
|
||||
|
||||
import httpserver
|
||||
import ioloop
|
||||
|
||||
def handle_request(request):
|
||||
message = "You requested %s\n" % request.uri
|
||||
request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
|
||||
len(message), message))
|
||||
request.finish()
|
||||
|
||||
http_server = httpserver.HTTPServer(handle_request)
|
||||
http_server.listen(8888)
|
||||
ioloop.IOLoop.instance().start()
|
||||
|
||||
`HTTPServer` is a very basic connection handler. Beyond parsing the
|
||||
HTTP request body and headers, the only HTTP semantics implemented
|
||||
in `HTTPServer` is HTTP/1.1 keep-alive connections. We do not, however,
|
||||
implement chunked encoding, so the request callback must provide a
|
||||
``Content-Length`` header or implement chunked encoding for HTTP/1.1
|
||||
requests for the server to run correctly for HTTP/1.1 clients. If
|
||||
the request handler is unable to do this, you can provide the
|
||||
``no_keep_alive`` argument to the `HTTPServer` constructor, which will
|
||||
ensure the connection is closed on every request no matter what HTTP
|
||||
version the client is using.
|
||||
|
||||
If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme``
|
||||
headers, which override the remote IP and HTTP scheme for all requests.
|
||||
These headers are useful when running Tornado behind a reverse proxy or
|
||||
load balancer.
|
||||
|
||||
`HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
|
||||
To make this server serve SSL traffic, send the ssl_options dictionary
|
||||
argument with the arguments required for the `ssl.wrap_socket` method,
|
||||
including "certfile" and "keyfile"::
|
||||
|
||||
HTTPServer(applicaton, ssl_options={
|
||||
"certfile": os.path.join(data_dir, "mydomain.crt"),
|
||||
"keyfile": os.path.join(data_dir, "mydomain.key"),
|
||||
})
|
||||
|
||||
`HTTPServer` initialization follows one of three patterns (the
|
||||
initialization methods are defined on `tornado.netutil.TCPServer`):
|
||||
|
||||
1. `~tornado.netutil.TCPServer.listen`: simple single-process::
|
||||
|
||||
server = HTTPServer(app)
|
||||
server.listen(8888)
|
||||
IOLoop.instance().start()
|
||||
|
||||
In many cases, `tornado.web.Application.listen` can be used to avoid
|
||||
the need to explicitly create the `HTTPServer`.
|
||||
|
||||
2. `~tornado.netutil.TCPServer.bind`/`~tornado.netutil.TCPServer.start`:
|
||||
simple multi-process::
|
||||
|
||||
server = HTTPServer(app)
|
||||
server.bind(8888)
|
||||
server.start(0) # Forks multiple sub-processes
|
||||
IOLoop.instance().start()
|
||||
|
||||
When using this interface, an `IOLoop` must *not* be passed
|
||||
to the `HTTPServer` constructor. `start` will always start
|
||||
the server on the default singleton `IOLoop`.
|
||||
|
||||
3. `~tornado.netutil.TCPServer.add_sockets`: advanced multi-process::
|
||||
|
||||
sockets = tornado.netutil.bind_sockets(8888)
|
||||
tornado.process.fork_processes(0)
|
||||
server = HTTPServer(app)
|
||||
server.add_sockets(sockets)
|
||||
IOLoop.instance().start()
|
||||
|
||||
The `add_sockets` interface is more complicated, but it can be
|
||||
used with `tornado.process.fork_processes` to give you more
|
||||
flexibility in when the fork happens. `add_sockets` can
|
||||
also be used in single-process servers if you want to create
|
||||
your listening sockets in some way other than
|
||||
`tornado.netutil.bind_sockets`.
|
||||
|
||||
"""
|
||||
def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
|
||||
xheaders=False, ssl_options=None, **kwargs):
|
||||
self.request_callback = request_callback
|
||||
self.no_keep_alive = no_keep_alive
|
||||
self.xheaders = xheaders
|
||||
TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
|
||||
**kwargs)
|
||||
|
||||
def handle_stream(self, stream, address):
|
||||
HTTPConnection(stream, address, self.request_callback,
|
||||
self.no_keep_alive, self.xheaders)
|
||||
|
||||
class _BadRequestException(Exception):
|
||||
"""Exception class for malformed HTTP requests."""
|
||||
pass
|
||||
|
||||
class HTTPConnection(object):
|
||||
"""Handles a connection to an HTTP client, executing HTTP requests.
|
||||
|
||||
We parse HTTP headers and bodies, and execute the request callback
|
||||
until the HTTP conection is closed.
|
||||
"""
|
||||
def __init__(self, stream, address, request_callback, no_keep_alive=False,
|
||||
xheaders=False):
|
||||
self.stream = stream
|
||||
if self.stream.socket.family not in (socket.AF_INET, socket.AF_INET6):
|
||||
# Unix (or other) socket; fake the remote address
|
||||
address = ('0.0.0.0', 0)
|
||||
self.address = address
|
||||
self.request_callback = request_callback
|
||||
self.no_keep_alive = no_keep_alive
|
||||
self.xheaders = xheaders
|
||||
self._request = None
|
||||
self._request_finished = False
|
||||
# Save stack context here, outside of any request. This keeps
|
||||
# contexts from one request from leaking into the next.
|
||||
self._header_callback = stack_context.wrap(self._on_headers)
|
||||
self.stream.read_until(b("\r\n\r\n"), self._header_callback)
|
||||
self._write_callback = None
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes a chunk of output to the stream."""
|
||||
assert self._request, "Request closed"
|
||||
if not self.stream.closed():
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
self.stream.write(chunk, self._on_write_complete)
|
||||
|
||||
def finish(self):
|
||||
"""Finishes the request."""
|
||||
assert self._request, "Request closed"
|
||||
self._request_finished = True
|
||||
if not self.stream.writing():
|
||||
self._finish_request()
|
||||
|
||||
def _on_write_complete(self):
|
||||
if self._write_callback is not None:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
callback()
|
||||
# _on_write_complete is enqueued on the IOLoop whenever the
|
||||
# IOStream's write buffer becomes empty, but it's possible for
|
||||
# another callback that runs on the IOLoop before it to
|
||||
# simultaneously write more data and finish the request. If
|
||||
# there is still data in the IOStream, a future
|
||||
# _on_write_complete will be responsible for calling
|
||||
# _finish_request.
|
||||
if self._request_finished and not self.stream.writing():
|
||||
self._finish_request()
|
||||
|
||||
def _finish_request(self):
|
||||
if self.no_keep_alive:
|
||||
disconnect = True
|
||||
else:
|
||||
connection_header = self._request.headers.get("Connection")
|
||||
if connection_header is not None:
|
||||
connection_header = connection_header.lower()
|
||||
if self._request.supports_http_1_1():
|
||||
disconnect = connection_header == "close"
|
||||
elif ("Content-Length" in self._request.headers
|
||||
or self._request.method in ("HEAD", "GET")):
|
||||
disconnect = connection_header != "keep-alive"
|
||||
else:
|
||||
disconnect = True
|
||||
self._request = None
|
||||
self._request_finished = False
|
||||
if disconnect:
|
||||
self.stream.close()
|
||||
return
|
||||
self.stream.read_until(b("\r\n\r\n"), self._header_callback)
|
||||
|
||||
def _on_headers(self, data):
|
||||
try:
|
||||
data = native_str(data.decode('latin1'))
|
||||
eol = data.find("\r\n")
|
||||
start_line = data[:eol]
|
||||
try:
|
||||
method, uri, version = start_line.split(" ")
|
||||
except ValueError:
|
||||
raise _BadRequestException("Malformed HTTP request line")
|
||||
if not version.startswith("HTTP/"):
|
||||
raise _BadRequestException("Malformed HTTP version in HTTP Request-Line")
|
||||
headers = httputil.HTTPHeaders.parse(data[eol:])
|
||||
self._request = HTTPRequest(
|
||||
connection=self, method=method, uri=uri, version=version,
|
||||
headers=headers, remote_ip=self.address[0])
|
||||
|
||||
content_length = headers.get("Content-Length")
|
||||
if content_length:
|
||||
content_length = int(content_length)
|
||||
if content_length > self.stream.max_buffer_size:
|
||||
raise _BadRequestException("Content-Length too long")
|
||||
if headers.get("Expect") == "100-continue":
|
||||
self.stream.write(b("HTTP/1.1 100 (Continue)\r\n\r\n"))
|
||||
self.stream.read_bytes(content_length, self._on_request_body)
|
||||
return
|
||||
|
||||
self.request_callback(self._request)
|
||||
except _BadRequestException, e:
|
||||
logging.info("Malformed HTTP request from %s: %s",
|
||||
self.address[0], e)
|
||||
self.stream.close()
|
||||
return
|
||||
|
||||
def _on_request_body(self, data):
|
||||
self._request.body = data
|
||||
content_type = self._request.headers.get("Content-Type", "")
|
||||
if self._request.method in ("POST", "PUT"):
|
||||
if content_type.startswith("application/x-www-form-urlencoded"):
|
||||
arguments = parse_qs_bytes(native_str(self._request.body))
|
||||
for name, values in arguments.iteritems():
|
||||
values = [v for v in values if v]
|
||||
if values:
|
||||
self._request.arguments.setdefault(name, []).extend(
|
||||
values)
|
||||
elif content_type.startswith("multipart/form-data"):
|
||||
fields = content_type.split(";")
|
||||
for field in fields:
|
||||
k, sep, v = field.strip().partition("=")
|
||||
if k == "boundary" and v:
|
||||
httputil.parse_multipart_form_data(
|
||||
utf8(v), data,
|
||||
self._request.arguments,
|
||||
self._request.files)
|
||||
break
|
||||
else:
|
||||
logging.warning("Invalid multipart/form-data")
|
||||
self.request_callback(self._request)
|
||||
|
||||
|
||||
class HTTPRequest(object):
|
||||
"""A single HTTP request.
|
||||
|
||||
All attributes are type `str` unless otherwise noted.
|
||||
|
||||
.. attribute:: method
|
||||
|
||||
HTTP request method, e.g. "GET" or "POST"
|
||||
|
||||
.. attribute:: uri
|
||||
|
||||
The requested uri.
|
||||
|
||||
.. attribute:: path
|
||||
|
||||
The path portion of `uri`
|
||||
|
||||
.. attribute:: query
|
||||
|
||||
The query portion of `uri`
|
||||
|
||||
.. attribute:: version
|
||||
|
||||
HTTP version specified in request, e.g. "HTTP/1.1"
|
||||
|
||||
.. attribute:: headers
|
||||
|
||||
`HTTPHeader` dictionary-like object for request headers. Acts like
|
||||
a case-insensitive dictionary with additional methods for repeated
|
||||
headers.
|
||||
|
||||
.. attribute:: body
|
||||
|
||||
Request body, if present, as a byte string.
|
||||
|
||||
.. attribute:: remote_ip
|
||||
|
||||
Client's IP address as a string. If `HTTPServer.xheaders` is set,
|
||||
will pass along the real IP address provided by a load balancer
|
||||
in the ``X-Real-Ip`` header
|
||||
|
||||
.. attribute:: protocol
|
||||
|
||||
The protocol used, either "http" or "https". If `HTTPServer.xheaders`
|
||||
is set, will pass along the protocol used by a load balancer if
|
||||
reported via an ``X-Scheme`` header.
|
||||
|
||||
.. attribute:: host
|
||||
|
||||
The requested hostname, usually taken from the ``Host`` header.
|
||||
|
||||
.. attribute:: arguments
|
||||
|
||||
GET/POST arguments are available in the arguments property, which
|
||||
maps arguments names to lists of values (to support multiple values
|
||||
for individual names). Names are of type `str`, while arguments
|
||||
are byte strings. Note that this is different from
|
||||
`RequestHandler.get_argument`, which returns argument values as
|
||||
unicode strings.
|
||||
|
||||
.. attribute:: files
|
||||
|
||||
File uploads are available in the files property, which maps file
|
||||
names to lists of :class:`HTTPFile`.
|
||||
|
||||
.. attribute:: connection
|
||||
|
||||
An HTTP request is attached to a single HTTP connection, which can
|
||||
be accessed through the "connection" attribute. Since connections
|
||||
are typically kept open in HTTP/1.1, multiple requests can be handled
|
||||
sequentially on a single connection.
|
||||
"""
|
||||
def __init__(self, method, uri, version="HTTP/1.0", headers=None,
|
||||
body=None, remote_ip=None, protocol=None, host=None,
|
||||
files=None, connection=None):
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
self.headers = headers or httputil.HTTPHeaders()
|
||||
self.body = body or ""
|
||||
if connection and connection.xheaders:
|
||||
# Squid uses X-Forwarded-For, others use X-Real-Ip
|
||||
self.remote_ip = self.headers.get(
|
||||
"X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
|
||||
if not self._valid_ip(self.remote_ip):
|
||||
self.remote_ip = remote_ip
|
||||
# AWS uses X-Forwarded-Proto
|
||||
self.protocol = self.headers.get(
|
||||
"X-Scheme", self.headers.get("X-Forwarded-Proto", protocol))
|
||||
if self.protocol not in ("http", "https"):
|
||||
self.protocol = "http"
|
||||
else:
|
||||
self.remote_ip = remote_ip
|
||||
if protocol:
|
||||
self.protocol = protocol
|
||||
elif connection and isinstance(connection.stream,
|
||||
iostream.SSLIOStream):
|
||||
self.protocol = "https"
|
||||
else:
|
||||
self.protocol = "http"
|
||||
self.host = host or self.headers.get("Host") or "127.0.0.1"
|
||||
self.files = files or {}
|
||||
self.connection = connection
|
||||
self._start_time = time.time()
|
||||
self._finish_time = None
|
||||
|
||||
scheme, netloc, path, query, fragment = urlparse.urlsplit(native_str(uri))
|
||||
self.path = path
|
||||
self.query = query
|
||||
arguments = parse_qs_bytes(query)
|
||||
self.arguments = {}
|
||||
for name, values in arguments.iteritems():
|
||||
values = [v for v in values if v]
|
||||
if values: self.arguments[name] = values
|
||||
|
||||
def supports_http_1_1(self):
|
||||
"""Returns True if this request supports HTTP/1.1 semantics"""
|
||||
return self.version == "HTTP/1.1"
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
"""A dictionary of Cookie.Morsel objects."""
|
||||
if not hasattr(self, "_cookies"):
|
||||
self._cookies = Cookie.SimpleCookie()
|
||||
if "Cookie" in self.headers:
|
||||
try:
|
||||
self._cookies.load(
|
||||
native_str(self.headers["Cookie"]))
|
||||
except Exception:
|
||||
self._cookies = {}
|
||||
return self._cookies
|
||||
|
||||
def write(self, chunk, callback=None):
|
||||
"""Writes the given chunk to the response stream."""
|
||||
assert isinstance(chunk, bytes_type)
|
||||
self.connection.write(chunk, callback=callback)
|
||||
|
||||
def finish(self):
|
||||
"""Finishes this HTTP request on the open connection."""
|
||||
self.connection.finish()
|
||||
self._finish_time = time.time()
|
||||
|
||||
def full_url(self):
|
||||
"""Reconstructs the full URL for this request."""
|
||||
return self.protocol + "://" + self.host + self.uri
|
||||
|
||||
def request_time(self):
|
||||
"""Returns the amount of time it took for this request to execute."""
|
||||
if self._finish_time is None:
|
||||
return time.time() - self._start_time
|
||||
else:
|
||||
return self._finish_time - self._start_time
|
||||
|
||||
def get_ssl_certificate(self):
|
||||
"""Returns the client's SSL certificate, if any.
|
||||
|
||||
To use client certificates, the HTTPServer must have been constructed
|
||||
with cert_reqs set in ssl_options, e.g.::
|
||||
|
||||
server = HTTPServer(app,
|
||||
ssl_options=dict(
|
||||
certfile="foo.crt",
|
||||
keyfile="foo.key",
|
||||
cert_reqs=ssl.CERT_REQUIRED,
|
||||
ca_certs="cacert.crt"))
|
||||
|
||||
The return value is a dictionary, see SSLSocket.getpeercert() in
|
||||
the standard library for more details.
|
||||
http://docs.python.org/library/ssl.html#sslsocket-objects
|
||||
"""
|
||||
try:
|
||||
return self.connection.stream.socket.getpeercert()
|
||||
except ssl.SSLError:
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
|
||||
"body")
|
||||
args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
|
||||
return "%s(%s, headers=%s)" % (
|
||||
self.__class__.__name__, args, dict(self.headers))
|
||||
|
||||
def _valid_ip(self, ip):
|
||||
try:
|
||||
res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM,
|
||||
0, socket.AI_NUMERICHOST)
|
||||
return bool(res)
|
||||
except socket.gaierror, e:
|
||||
if e.args[0] == socket.EAI_NONAME:
|
||||
return False
|
||||
raise
|
||||
return True
|
||||
|
||||
280
libs/tornado/httputil.py
Normal file
280
libs/tornado/httputil.py
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""HTTP utility code shared by clients and servers."""
|
||||
|
||||
import logging
|
||||
import urllib
|
||||
import re
|
||||
|
||||
from tornado.util import b, ObjectDict
|
||||
|
||||
class HTTPHeaders(dict):
|
||||
"""A dictionary that maintains Http-Header-Case for all keys.
|
||||
|
||||
Supports multiple values per key via a pair of new methods,
|
||||
add() and get_list(). The regular dictionary interface returns a single
|
||||
value per key, with multiple values joined by a comma.
|
||||
|
||||
>>> h = HTTPHeaders({"content-type": "text/html"})
|
||||
>>> h.keys()
|
||||
['Content-Type']
|
||||
>>> h["Content-Type"]
|
||||
'text/html'
|
||||
|
||||
>>> h.add("Set-Cookie", "A=B")
|
||||
>>> h.add("Set-Cookie", "C=D")
|
||||
>>> h["set-cookie"]
|
||||
'A=B,C=D'
|
||||
>>> h.get_list("set-cookie")
|
||||
['A=B', 'C=D']
|
||||
|
||||
>>> for (k,v) in sorted(h.get_all()):
|
||||
... print '%s: %s' % (k,v)
|
||||
...
|
||||
Content-Type: text/html
|
||||
Set-Cookie: A=B
|
||||
Set-Cookie: C=D
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Don't pass args or kwargs to dict.__init__, as it will bypass
|
||||
# our __setitem__
|
||||
dict.__init__(self)
|
||||
self._as_list = {}
|
||||
self._last_key = None
|
||||
self.update(*args, **kwargs)
|
||||
|
||||
# new public methods
|
||||
|
||||
def add(self, name, value):
|
||||
"""Adds a new value for the given key."""
|
||||
norm_name = HTTPHeaders._normalize_name(name)
|
||||
self._last_key = norm_name
|
||||
if norm_name in self:
|
||||
# bypass our override of __setitem__ since it modifies _as_list
|
||||
dict.__setitem__(self, norm_name, self[norm_name] + ',' + value)
|
||||
self._as_list[norm_name].append(value)
|
||||
else:
|
||||
self[norm_name] = value
|
||||
|
||||
def get_list(self, name):
|
||||
"""Returns all values for the given header as a list."""
|
||||
norm_name = HTTPHeaders._normalize_name(name)
|
||||
return self._as_list.get(norm_name, [])
|
||||
|
||||
def get_all(self):
|
||||
"""Returns an iterable of all (name, value) pairs.
|
||||
|
||||
If a header has multiple values, multiple pairs will be
|
||||
returned with the same name.
|
||||
"""
|
||||
for name, list in self._as_list.iteritems():
|
||||
for value in list:
|
||||
yield (name, value)
|
||||
|
||||
def parse_line(self, line):
|
||||
"""Updates the dictionary with a single header line.
|
||||
|
||||
>>> h = HTTPHeaders()
|
||||
>>> h.parse_line("Content-Type: text/html")
|
||||
>>> h.get('content-type')
|
||||
'text/html'
|
||||
"""
|
||||
if line[0].isspace():
|
||||
# continuation of a multi-line header
|
||||
new_part = ' ' + line.lstrip()
|
||||
self._as_list[self._last_key][-1] += new_part
|
||||
dict.__setitem__(self, self._last_key,
|
||||
self[self._last_key] + new_part)
|
||||
else:
|
||||
name, value = line.split(":", 1)
|
||||
self.add(name, value.strip())
|
||||
|
||||
@classmethod
|
||||
def parse(cls, headers):
|
||||
"""Returns a dictionary from HTTP header text.
|
||||
|
||||
>>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
|
||||
>>> sorted(h.iteritems())
|
||||
[('Content-Length', '42'), ('Content-Type', 'text/html')]
|
||||
"""
|
||||
h = cls()
|
||||
for line in headers.splitlines():
|
||||
if line:
|
||||
h.parse_line(line)
|
||||
return h
|
||||
|
||||
# dict implementation overrides
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
norm_name = HTTPHeaders._normalize_name(name)
|
||||
dict.__setitem__(self, norm_name, value)
|
||||
self._as_list[norm_name] = [value]
|
||||
|
||||
def __getitem__(self, name):
|
||||
return dict.__getitem__(self, HTTPHeaders._normalize_name(name))
|
||||
|
||||
def __delitem__(self, name):
|
||||
norm_name = HTTPHeaders._normalize_name(name)
|
||||
dict.__delitem__(self, norm_name)
|
||||
del self._as_list[norm_name]
|
||||
|
||||
def __contains__(self, name):
|
||||
norm_name = HTTPHeaders._normalize_name(name)
|
||||
return dict.__contains__(self, norm_name)
|
||||
|
||||
def get(self, name, default=None):
|
||||
return dict.get(self, HTTPHeaders._normalize_name(name), default)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# dict.update bypasses our __setitem__
|
||||
for k, v in dict(*args, **kwargs).iteritems():
|
||||
self[k] = v
|
||||
|
||||
_NORMALIZED_HEADER_RE = re.compile(r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$')
|
||||
_normalized_headers = {}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_name(name):
|
||||
"""Converts a name to Http-Header-Case.
|
||||
|
||||
>>> HTTPHeaders._normalize_name("coNtent-TYPE")
|
||||
'Content-Type'
|
||||
"""
|
||||
try:
|
||||
return HTTPHeaders._normalized_headers[name]
|
||||
except KeyError:
|
||||
if HTTPHeaders._NORMALIZED_HEADER_RE.match(name):
|
||||
normalized = name
|
||||
else:
|
||||
normalized = "-".join([w.capitalize() for w in name.split("-")])
|
||||
HTTPHeaders._normalized_headers[name] = normalized
|
||||
return normalized
|
||||
|
||||
|
||||
def url_concat(url, args):
|
||||
"""Concatenate url and argument dictionary regardless of whether
|
||||
url has existing query parameters.
|
||||
|
||||
>>> url_concat("http://example.com/foo?a=b", dict(c="d"))
|
||||
'http://example.com/foo?a=b&c=d'
|
||||
"""
|
||||
if not args: return url
|
||||
if url[-1] not in ('?', '&'):
|
||||
url += '&' if ('?' in url) else '?'
|
||||
return url + urllib.urlencode(args)
|
||||
|
||||
|
||||
class HTTPFile(ObjectDict):
|
||||
"""Represents an HTTP file. For backwards compatibility, its instance
|
||||
attributes are also accessible as dictionary keys.
|
||||
|
||||
:ivar filename:
|
||||
:ivar body:
|
||||
:ivar content_type: The content_type comes from the provided HTTP header
|
||||
and should not be trusted outright given that it can be easily forged.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def parse_multipart_form_data(boundary, data, arguments, files):
|
||||
"""Parses a multipart/form-data body.
|
||||
|
||||
The boundary and data parameters are both byte strings.
|
||||
The dictionaries given in the arguments and files parameters
|
||||
will be updated with the contents of the body.
|
||||
"""
|
||||
# The standard allows for the boundary to be quoted in the header,
|
||||
# although it's rare (it happens at least for google app engine
|
||||
# xmpp). I think we're also supposed to handle backslash-escapes
|
||||
# here but I'll save that until we see a client that uses them
|
||||
# in the wild.
|
||||
if boundary.startswith(b('"')) and boundary.endswith(b('"')):
|
||||
boundary = boundary[1:-1]
|
||||
if data.endswith(b("\r\n")):
|
||||
footer_length = len(boundary) + 6
|
||||
else:
|
||||
footer_length = len(boundary) + 4
|
||||
parts = data[:-footer_length].split(b("--") + boundary + b("\r\n"))
|
||||
for part in parts:
|
||||
if not part: continue
|
||||
eoh = part.find(b("\r\n\r\n"))
|
||||
if eoh == -1:
|
||||
logging.warning("multipart/form-data missing headers")
|
||||
continue
|
||||
headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
|
||||
disp_header = headers.get("Content-Disposition", "")
|
||||
disposition, disp_params = _parse_header(disp_header)
|
||||
if disposition != "form-data" or not part.endswith(b("\r\n")):
|
||||
logging.warning("Invalid multipart/form-data")
|
||||
continue
|
||||
value = part[eoh + 4:-2]
|
||||
if not disp_params.get("name"):
|
||||
logging.warning("multipart/form-data value missing name")
|
||||
continue
|
||||
name = disp_params["name"]
|
||||
if disp_params.get("filename"):
|
||||
ctype = headers.get("Content-Type", "application/unknown")
|
||||
files.setdefault(name, []).append(HTTPFile(
|
||||
filename=disp_params["filename"], body=value,
|
||||
content_type=ctype))
|
||||
else:
|
||||
arguments.setdefault(name, []).append(value)
|
||||
|
||||
|
||||
# _parseparam and _parse_header are copied and modified from python2.7's cgi.py
|
||||
# The original 2.7 version of this code did not correctly support some
|
||||
# combinations of semicolons and double quotes.
|
||||
def _parseparam(s):
|
||||
while s[:1] == ';':
|
||||
s = s[1:]
|
||||
end = s.find(';')
|
||||
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
|
||||
end = s.find(';', end + 1)
|
||||
if end < 0:
|
||||
end = len(s)
|
||||
f = s[:end]
|
||||
yield f.strip()
|
||||
s = s[end:]
|
||||
|
||||
def _parse_header(line):
|
||||
"""Parse a Content-type like header.
|
||||
|
||||
Return the main content-type and a dictionary of options.
|
||||
|
||||
"""
|
||||
parts = _parseparam(';' + line)
|
||||
key = parts.next()
|
||||
pdict = {}
|
||||
for p in parts:
|
||||
i = p.find('=')
|
||||
if i >= 0:
|
||||
name = p[:i].strip().lower()
|
||||
value = p[i+1:].strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
value = value[1:-1]
|
||||
value = value.replace('\\\\', '\\').replace('\\"', '"')
|
||||
pdict[name] = value
|
||||
return key, pdict
|
||||
|
||||
|
||||
def doctests():
|
||||
import doctest
|
||||
return doctest.DocTestSuite()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
643
libs/tornado/ioloop.py
Normal file
643
libs/tornado/ioloop.py
Normal file
@@ -0,0 +1,643 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""An I/O event loop for non-blocking sockets.
|
||||
|
||||
Typical applications will use a single `IOLoop` object, in the
|
||||
`IOLoop.instance` singleton. The `IOLoop.start` method should usually
|
||||
be called at the end of the ``main()`` function. Atypical applications may
|
||||
use more than one `IOLoop`, such as one `IOLoop` per thread, or per `unittest`
|
||||
case.
|
||||
|
||||
In addition to I/O events, the `IOLoop` can also schedule time-based events.
|
||||
`IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import heapq
|
||||
import os
|
||||
import logging
|
||||
import select
|
||||
import thread
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from tornado import stack_context
|
||||
|
||||
try:
|
||||
import signal
|
||||
except ImportError:
|
||||
signal = None
|
||||
|
||||
from tornado.platform.auto import set_close_exec, Waker
|
||||
|
||||
|
||||
class IOLoop(object):
|
||||
"""A level-triggered I/O loop.
|
||||
|
||||
We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python
|
||||
2.6+) if they are available, or else we fall back on select(). If
|
||||
you are implementing a system that needs to handle thousands of
|
||||
simultaneous connections, you should use a system that supports either
|
||||
epoll or queue.
|
||||
|
||||
Example usage for a simple TCP server::
|
||||
|
||||
import errno
|
||||
import functools
|
||||
import ioloop
|
||||
import socket
|
||||
|
||||
def connection_ready(sock, fd, events):
|
||||
while True:
|
||||
try:
|
||||
connection, address = sock.accept()
|
||||
except socket.error, e:
|
||||
if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
raise
|
||||
return
|
||||
connection.setblocking(0)
|
||||
handle_connection(connection, address)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setblocking(0)
|
||||
sock.bind(("", port))
|
||||
sock.listen(128)
|
||||
|
||||
io_loop = ioloop.IOLoop.instance()
|
||||
callback = functools.partial(connection_ready, sock)
|
||||
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
|
||||
io_loop.start()
|
||||
|
||||
"""
|
||||
# Constants from the epoll module
|
||||
_EPOLLIN = 0x001
|
||||
_EPOLLPRI = 0x002
|
||||
_EPOLLOUT = 0x004
|
||||
_EPOLLERR = 0x008
|
||||
_EPOLLHUP = 0x010
|
||||
_EPOLLRDHUP = 0x2000
|
||||
_EPOLLONESHOT = (1 << 30)
|
||||
_EPOLLET = (1 << 31)
|
||||
|
||||
# Our events map exactly to the epoll events
|
||||
NONE = 0
|
||||
READ = _EPOLLIN
|
||||
WRITE = _EPOLLOUT
|
||||
ERROR = _EPOLLERR | _EPOLLHUP
|
||||
|
||||
def __init__(self, impl=None):
|
||||
self._impl = impl or _poll()
|
||||
if hasattr(self._impl, 'fileno'):
|
||||
set_close_exec(self._impl.fileno())
|
||||
self._handlers = {}
|
||||
self._events = {}
|
||||
self._callbacks = []
|
||||
self._callback_lock = threading.Lock()
|
||||
self._timeouts = []
|
||||
self._running = False
|
||||
self._stopped = False
|
||||
self._thread_ident = None
|
||||
self._blocking_signal_threshold = None
|
||||
|
||||
# Create a pipe that we send bogus data to when we want to wake
|
||||
# the I/O loop when it is idle
|
||||
self._waker = Waker()
|
||||
self.add_handler(self._waker.fileno(),
|
||||
lambda fd, events: self._waker.consume(),
|
||||
self.READ)
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""Returns a global IOLoop instance.
|
||||
|
||||
Most single-threaded applications have a single, global IOLoop.
|
||||
Use this method instead of passing around IOLoop instances
|
||||
throughout your code.
|
||||
|
||||
A common pattern for classes that depend on IOLoops is to use
|
||||
a default argument to enable programs with multiple IOLoops
|
||||
but not require the argument for simpler applications::
|
||||
|
||||
class MyClass(object):
|
||||
def __init__(self, io_loop=None):
|
||||
self.io_loop = io_loop or IOLoop.instance()
|
||||
"""
|
||||
if not hasattr(IOLoop, "_instance"):
|
||||
IOLoop._instance = IOLoop()
|
||||
return IOLoop._instance
|
||||
|
||||
@staticmethod
|
||||
def initialized():
|
||||
"""Returns true if the singleton instance has been created."""
|
||||
return hasattr(IOLoop, "_instance")
|
||||
|
||||
def install(self):
|
||||
"""Installs this IOloop object as the singleton instance.
|
||||
|
||||
This is normally not necessary as `instance()` will create
|
||||
an IOLoop on demand, but you may want to call `install` to use
|
||||
a custom subclass of IOLoop.
|
||||
"""
|
||||
assert not IOLoop.initialized()
|
||||
IOLoop._instance = self
|
||||
|
||||
def close(self, all_fds=False):
|
||||
"""Closes the IOLoop, freeing any resources used.
|
||||
|
||||
If ``all_fds`` is true, all file descriptors registered on the
|
||||
IOLoop will be closed (not just the ones created by the IOLoop itself.
|
||||
"""
|
||||
self.remove_handler(self._waker.fileno())
|
||||
if all_fds:
|
||||
for fd in self._handlers.keys()[:]:
|
||||
try:
|
||||
os.close(fd)
|
||||
except Exception:
|
||||
logging.debug("error closing fd %s", fd, exc_info=True)
|
||||
self._waker.close()
|
||||
self._impl.close()
|
||||
|
||||
def add_handler(self, fd, handler, events):
|
||||
"""Registers the given handler to receive the given events for fd."""
|
||||
self._handlers[fd] = stack_context.wrap(handler)
|
||||
self._impl.register(fd, events | self.ERROR)
|
||||
|
||||
def update_handler(self, fd, events):
|
||||
"""Changes the events we listen for fd."""
|
||||
self._impl.modify(fd, events | self.ERROR)
|
||||
|
||||
def remove_handler(self, fd):
|
||||
"""Stop listening for events on fd."""
|
||||
self._handlers.pop(fd, None)
|
||||
self._events.pop(fd, None)
|
||||
try:
|
||||
self._impl.unregister(fd)
|
||||
except (OSError, IOError):
|
||||
logging.debug("Error deleting fd from IOLoop", exc_info=True)
|
||||
|
||||
def set_blocking_signal_threshold(self, seconds, action):
|
||||
"""Sends a signal if the ioloop is blocked for more than s seconds.
|
||||
|
||||
Pass seconds=None to disable. Requires python 2.6 on a unixy
|
||||
platform.
|
||||
|
||||
The action parameter is a python signal handler. Read the
|
||||
documentation for the python 'signal' module for more information.
|
||||
If action is None, the process will be killed if it is blocked for
|
||||
too long.
|
||||
"""
|
||||
if not hasattr(signal, "setitimer"):
|
||||
logging.error("set_blocking_signal_threshold requires a signal module "
|
||||
"with the setitimer method")
|
||||
return
|
||||
self._blocking_signal_threshold = seconds
|
||||
if seconds is not None:
|
||||
signal.signal(signal.SIGALRM,
|
||||
action if action is not None else signal.SIG_DFL)
|
||||
|
||||
def set_blocking_log_threshold(self, seconds):
|
||||
"""Logs a stack trace if the ioloop is blocked for more than s seconds.
|
||||
Equivalent to set_blocking_signal_threshold(seconds, self.log_stack)
|
||||
"""
|
||||
self.set_blocking_signal_threshold(seconds, self.log_stack)
|
||||
|
||||
def log_stack(self, signal, frame):
|
||||
"""Signal handler to log the stack trace of the current thread.
|
||||
|
||||
For use with set_blocking_signal_threshold.
|
||||
"""
|
||||
logging.warning('IOLoop blocked for %f seconds in\n%s',
|
||||
self._blocking_signal_threshold,
|
||||
''.join(traceback.format_stack(frame)))
|
||||
|
||||
def start(self):
|
||||
"""Starts the I/O loop.
|
||||
|
||||
The loop will run until one of the I/O handlers calls stop(), which
|
||||
will make the loop stop after the current event iteration completes.
|
||||
"""
|
||||
if self._stopped:
|
||||
self._stopped = False
|
||||
return
|
||||
self._thread_ident = thread.get_ident()
|
||||
self._running = True
|
||||
while True:
|
||||
poll_timeout = 3600.0
|
||||
|
||||
# Prevent IO event starvation by delaying new callbacks
|
||||
# to the next iteration of the event loop.
|
||||
with self._callback_lock:
|
||||
callbacks = self._callbacks
|
||||
self._callbacks = []
|
||||
for callback in callbacks:
|
||||
self._run_callback(callback)
|
||||
|
||||
if self._timeouts:
|
||||
now = time.time()
|
||||
while self._timeouts:
|
||||
if self._timeouts[0].callback is None:
|
||||
# the timeout was cancelled
|
||||
heapq.heappop(self._timeouts)
|
||||
elif self._timeouts[0].deadline <= now:
|
||||
timeout = heapq.heappop(self._timeouts)
|
||||
self._run_callback(timeout.callback)
|
||||
else:
|
||||
seconds = self._timeouts[0].deadline - now
|
||||
poll_timeout = min(seconds, poll_timeout)
|
||||
break
|
||||
|
||||
if self._callbacks:
|
||||
# If any callbacks or timeouts called add_callback,
|
||||
# we don't want to wait in poll() before we run them.
|
||||
poll_timeout = 0.0
|
||||
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
if self._blocking_signal_threshold is not None:
|
||||
# clear alarm so it doesn't fire while poll is waiting for
|
||||
# events.
|
||||
signal.setitimer(signal.ITIMER_REAL, 0, 0)
|
||||
|
||||
try:
|
||||
event_pairs = self._impl.poll(poll_timeout)
|
||||
except Exception, e:
|
||||
# Depending on python version and IOLoop implementation,
|
||||
# different exception types may be thrown and there are
|
||||
# two ways EINTR might be signaled:
|
||||
# * e.errno == errno.EINTR
|
||||
# * e.args is like (errno.EINTR, 'Interrupted system call')
|
||||
if (getattr(e, 'errno', None) == errno.EINTR or
|
||||
(isinstance(getattr(e, 'args', None), tuple) and
|
||||
len(e.args) == 2 and e.args[0] == errno.EINTR)):
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if self._blocking_signal_threshold is not None:
|
||||
signal.setitimer(signal.ITIMER_REAL,
|
||||
self._blocking_signal_threshold, 0)
|
||||
|
||||
# Pop one fd at a time from the set of pending fds and run
|
||||
# its handler. Since that handler may perform actions on
|
||||
# other file descriptors, there may be reentrant calls to
|
||||
# this IOLoop that update self._events
|
||||
self._events.update(event_pairs)
|
||||
while self._events:
|
||||
fd, events = self._events.popitem()
|
||||
try:
|
||||
self._handlers[fd](fd, events)
|
||||
except (OSError, IOError), e:
|
||||
if e.args[0] == errno.EPIPE:
|
||||
# Happens when the client closes the connection
|
||||
pass
|
||||
else:
|
||||
logging.error("Exception in I/O handler for fd %s",
|
||||
fd, exc_info=True)
|
||||
except Exception:
|
||||
logging.error("Exception in I/O handler for fd %s",
|
||||
fd, exc_info=True)
|
||||
# reset the stopped flag so another start/stop pair can be issued
|
||||
self._stopped = False
|
||||
if self._blocking_signal_threshold is not None:
|
||||
signal.setitimer(signal.ITIMER_REAL, 0, 0)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the loop after the current event loop iteration is complete.
|
||||
If the event loop is not currently running, the next call to start()
|
||||
will return immediately.
|
||||
|
||||
To use asynchronous methods from otherwise-synchronous code (such as
|
||||
unit tests), you can start and stop the event loop like this::
|
||||
|
||||
ioloop = IOLoop()
|
||||
async_method(ioloop=ioloop, callback=ioloop.stop)
|
||||
ioloop.start()
|
||||
|
||||
ioloop.start() will return after async_method has run its callback,
|
||||
whether that callback was invoked before or after ioloop.start.
|
||||
"""
|
||||
self._running = False
|
||||
self._stopped = True
|
||||
self._waker.wake()
|
||||
|
||||
def running(self):
|
||||
"""Returns true if this IOLoop is currently running."""
|
||||
return self._running
|
||||
|
||||
def add_timeout(self, deadline, callback):
|
||||
"""Calls the given callback at the time deadline from the I/O loop.
|
||||
|
||||
Returns a handle that may be passed to remove_timeout to cancel.
|
||||
|
||||
``deadline`` may be a number denoting a unix timestamp (as returned
|
||||
by ``time.time()`` or a ``datetime.timedelta`` object for a deadline
|
||||
relative to the current time.
|
||||
|
||||
Note that it is not safe to call `add_timeout` from other threads.
|
||||
Instead, you must use `add_callback` to transfer control to the
|
||||
IOLoop's thread, and then call `add_timeout` from there.
|
||||
"""
|
||||
timeout = _Timeout(deadline, stack_context.wrap(callback))
|
||||
heapq.heappush(self._timeouts, timeout)
|
||||
return timeout
|
||||
|
||||
def remove_timeout(self, timeout):
|
||||
"""Cancels a pending timeout.
|
||||
|
||||
The argument is a handle as returned by add_timeout.
|
||||
"""
|
||||
# Removing from a heap is complicated, so just leave the defunct
|
||||
# timeout object in the queue (see discussion in
|
||||
# http://docs.python.org/library/heapq.html).
|
||||
# If this turns out to be a problem, we could add a garbage
|
||||
# collection pass whenever there are too many dead timeouts.
|
||||
timeout.callback = None
|
||||
|
||||
def add_callback(self, callback):
|
||||
"""Calls the given callback on the next I/O loop iteration.
|
||||
|
||||
It is safe to call this method from any thread at any time.
|
||||
Note that this is the *only* method in IOLoop that makes this
|
||||
guarantee; all other interaction with the IOLoop must be done
|
||||
from that IOLoop's thread. add_callback() may be used to transfer
|
||||
control from other threads to the IOLoop's thread.
|
||||
"""
|
||||
with self._callback_lock:
|
||||
list_empty = not self._callbacks
|
||||
self._callbacks.append(stack_context.wrap(callback))
|
||||
if list_empty and thread.get_ident() != self._thread_ident:
|
||||
# If we're in the IOLoop's thread, we know it's not currently
|
||||
# polling. If we're not, and we added the first callback to an
|
||||
# empty list, we may need to wake it up (it may wake up on its
|
||||
# own, but an occasional extra wake is harmless). Waking
|
||||
# up a polling IOLoop is relatively expensive, so we try to
|
||||
# avoid it when we can.
|
||||
self._waker.wake()
|
||||
|
||||
def _run_callback(self, callback):
|
||||
try:
|
||||
callback()
|
||||
except Exception:
|
||||
self.handle_callback_exception(callback)
|
||||
|
||||
def handle_callback_exception(self, callback):
|
||||
"""This method is called whenever a callback run by the IOLoop
|
||||
throws an exception.
|
||||
|
||||
By default simply logs the exception as an error. Subclasses
|
||||
may override this method to customize reporting of exceptions.
|
||||
|
||||
The exception itself is not passed explicitly, but is available
|
||||
in sys.exc_info.
|
||||
"""
|
||||
logging.error("Exception in callback %r", callback, exc_info=True)
|
||||
|
||||
|
||||
class _Timeout(object):
|
||||
"""An IOLoop timeout, a UNIX timestamp and a callback"""
|
||||
|
||||
# Reduce memory overhead when there are lots of pending callbacks
|
||||
__slots__ = ['deadline', 'callback']
|
||||
|
||||
def __init__(self, deadline, callback):
|
||||
if isinstance(deadline, (int, long, float)):
|
||||
self.deadline = deadline
|
||||
elif isinstance(deadline, datetime.timedelta):
|
||||
self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline)
|
||||
else:
|
||||
raise TypeError("Unsupported deadline %r" % deadline)
|
||||
self.callback = callback
|
||||
|
||||
@staticmethod
|
||||
def timedelta_to_seconds(td):
|
||||
"""Equivalent to td.total_seconds() (introduced in python 2.7)."""
|
||||
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
|
||||
|
||||
# Comparison methods to sort by deadline, with object id as a tiebreaker
|
||||
# to guarantee a consistent ordering. The heapq module uses __le__
|
||||
# in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
|
||||
# use __lt__).
|
||||
def __lt__(self, other):
|
||||
return ((self.deadline, id(self)) <
|
||||
(other.deadline, id(other)))
|
||||
|
||||
def __le__(self, other):
|
||||
return ((self.deadline, id(self)) <=
|
||||
(other.deadline, id(other)))
|
||||
|
||||
|
||||
class PeriodicCallback(object):
|
||||
"""Schedules the given callback to be called periodically.
|
||||
|
||||
The callback is called every callback_time milliseconds.
|
||||
|
||||
`start` must be called after the PeriodicCallback is created.
|
||||
"""
|
||||
def __init__(self, callback, callback_time, io_loop=None):
|
||||
self.callback = callback
|
||||
self.callback_time = callback_time
|
||||
self.io_loop = io_loop or IOLoop.instance()
|
||||
self._running = False
|
||||
self._timeout = None
|
||||
|
||||
def start(self):
|
||||
"""Starts the timer."""
|
||||
self._running = True
|
||||
self._next_timeout = time.time()
|
||||
self._schedule_next()
|
||||
|
||||
def stop(self):
|
||||
"""Stops the timer."""
|
||||
self._running = False
|
||||
if self._timeout is not None:
|
||||
self.io_loop.remove_timeout(self._timeout)
|
||||
self._timeout = None
|
||||
|
||||
def _run(self):
|
||||
if not self._running: return
|
||||
try:
|
||||
self.callback()
|
||||
except Exception:
|
||||
logging.error("Error in periodic callback", exc_info=True)
|
||||
self._schedule_next()
|
||||
|
||||
def _schedule_next(self):
|
||||
if self._running:
|
||||
current_time = time.time()
|
||||
while self._next_timeout <= current_time:
|
||||
self._next_timeout += self.callback_time / 1000.0
|
||||
self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run)
|
||||
|
||||
|
||||
class _EPoll(object):
|
||||
"""An epoll-based event loop using our C module for Python 2.5 systems"""
|
||||
_EPOLL_CTL_ADD = 1
|
||||
_EPOLL_CTL_DEL = 2
|
||||
_EPOLL_CTL_MOD = 3
|
||||
|
||||
def __init__(self):
|
||||
self._epoll_fd = epoll.epoll_create()
|
||||
|
||||
def fileno(self):
|
||||
return self._epoll_fd
|
||||
|
||||
def close(self):
|
||||
os.close(self._epoll_fd)
|
||||
|
||||
def register(self, fd, events):
|
||||
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events)
|
||||
|
||||
def modify(self, fd, events):
|
||||
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events)
|
||||
|
||||
def unregister(self, fd):
|
||||
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0)
|
||||
|
||||
def poll(self, timeout):
|
||||
return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000))
|
||||
|
||||
|
||||
class _KQueue(object):
|
||||
"""A kqueue-based event loop for BSD/Mac systems."""
|
||||
def __init__(self):
|
||||
self._kqueue = select.kqueue()
|
||||
self._active = {}
|
||||
|
||||
def fileno(self):
|
||||
return self._kqueue.fileno()
|
||||
|
||||
def close(self):
|
||||
self._kqueue.close()
|
||||
|
||||
def register(self, fd, events):
|
||||
self._control(fd, events, select.KQ_EV_ADD)
|
||||
self._active[fd] = events
|
||||
|
||||
def modify(self, fd, events):
|
||||
self.unregister(fd)
|
||||
self.register(fd, events)
|
||||
|
||||
def unregister(self, fd):
|
||||
events = self._active.pop(fd)
|
||||
self._control(fd, events, select.KQ_EV_DELETE)
|
||||
|
||||
def _control(self, fd, events, flags):
|
||||
kevents = []
|
||||
if events & IOLoop.WRITE:
|
||||
kevents.append(select.kevent(
|
||||
fd, filter=select.KQ_FILTER_WRITE, flags=flags))
|
||||
if events & IOLoop.READ or not kevents:
|
||||
# Always read when there is not a write
|
||||
kevents.append(select.kevent(
|
||||
fd, filter=select.KQ_FILTER_READ, flags=flags))
|
||||
# Even though control() takes a list, it seems to return EINVAL
|
||||
# on Mac OS X (10.6) when there is more than one event in the list.
|
||||
for kevent in kevents:
|
||||
self._kqueue.control([kevent], 0)
|
||||
|
||||
def poll(self, timeout):
|
||||
kevents = self._kqueue.control(None, 1000, timeout)
|
||||
events = {}
|
||||
for kevent in kevents:
|
||||
fd = kevent.ident
|
||||
if kevent.filter == select.KQ_FILTER_READ:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.READ
|
||||
if kevent.filter == select.KQ_FILTER_WRITE:
|
||||
if kevent.flags & select.KQ_EV_EOF:
|
||||
# If an asynchronous connection is refused, kqueue
|
||||
# returns a write event with the EOF flag set.
|
||||
# Turn this into an error for consistency with the
|
||||
# other IOLoop implementations.
|
||||
# Note that for read events, EOF may be returned before
|
||||
# all data has been consumed from the socket buffer,
|
||||
# so we only check for EOF on write events.
|
||||
events[fd] = IOLoop.ERROR
|
||||
else:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.WRITE
|
||||
if kevent.flags & select.KQ_EV_ERROR:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.ERROR
|
||||
return events.items()
|
||||
|
||||
|
||||
class _Select(object):
|
||||
"""A simple, select()-based IOLoop implementation for non-Linux systems"""
|
||||
def __init__(self):
|
||||
self.read_fds = set()
|
||||
self.write_fds = set()
|
||||
self.error_fds = set()
|
||||
self.fd_sets = (self.read_fds, self.write_fds, self.error_fds)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def register(self, fd, events):
|
||||
if events & IOLoop.READ: self.read_fds.add(fd)
|
||||
if events & IOLoop.WRITE: self.write_fds.add(fd)
|
||||
if events & IOLoop.ERROR:
|
||||
self.error_fds.add(fd)
|
||||
# Closed connections are reported as errors by epoll and kqueue,
|
||||
# but as zero-byte reads by select, so when errors are requested
|
||||
# we need to listen for both read and error.
|
||||
self.read_fds.add(fd)
|
||||
|
||||
def modify(self, fd, events):
|
||||
self.unregister(fd)
|
||||
self.register(fd, events)
|
||||
|
||||
def unregister(self, fd):
|
||||
self.read_fds.discard(fd)
|
||||
self.write_fds.discard(fd)
|
||||
self.error_fds.discard(fd)
|
||||
|
||||
def poll(self, timeout):
|
||||
readable, writeable, errors = select.select(
|
||||
self.read_fds, self.write_fds, self.error_fds, timeout)
|
||||
events = {}
|
||||
for fd in readable:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.READ
|
||||
for fd in writeable:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.WRITE
|
||||
for fd in errors:
|
||||
events[fd] = events.get(fd, 0) | IOLoop.ERROR
|
||||
return events.items()
|
||||
|
||||
|
||||
# Choose a poll implementation. Use epoll if it is available, fall back to
|
||||
# select() for non-Linux platforms
|
||||
if hasattr(select, "epoll"):
|
||||
# Python 2.6+ on Linux
|
||||
_poll = select.epoll
|
||||
elif hasattr(select, "kqueue"):
|
||||
# Python 2.6+ on BSD or Mac
|
||||
_poll = _KQueue
|
||||
else:
|
||||
try:
|
||||
# Linux systems with our C module installed
|
||||
import epoll
|
||||
_poll = _EPoll
|
||||
except Exception:
|
||||
# All other systems
|
||||
import sys
|
||||
if "linux" in sys.platform:
|
||||
logging.warning("epoll module not found; using select()")
|
||||
_poll = _Select
|
||||
728
libs/tornado/iostream.py
Normal file
728
libs/tornado/iostream.py
Normal file
@@ -0,0 +1,728 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A utility class to write to and read from a non-blocking socket."""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import re
|
||||
|
||||
from tornado import ioloop
|
||||
from tornado import stack_context
|
||||
from tornado.util import b, bytes_type
|
||||
|
||||
try:
|
||||
import ssl # Python 2.6+
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
class IOStream(object):
|
||||
r"""A utility class to write to and read from a non-blocking socket.
|
||||
|
||||
We support a non-blocking ``write()`` and a family of ``read_*()`` methods.
|
||||
All of the methods take callbacks (since writing and reading are
|
||||
non-blocking and asynchronous).
|
||||
|
||||
The socket parameter may either be connected or unconnected. For
|
||||
server operations the socket is the result of calling socket.accept().
|
||||
For client operations the socket is created with socket.socket(),
|
||||
and may either be connected before passing it to the IOStream or
|
||||
connected with IOStream.connect.
|
||||
|
||||
A very simple (and broken) HTTP client using this class::
|
||||
|
||||
from tornado import ioloop
|
||||
from tornado import iostream
|
||||
import socket
|
||||
|
||||
def send_request():
|
||||
stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n")
|
||||
stream.read_until("\r\n\r\n", on_headers)
|
||||
|
||||
def on_headers(data):
|
||||
headers = {}
|
||||
for line in data.split("\r\n"):
|
||||
parts = line.split(":")
|
||||
if len(parts) == 2:
|
||||
headers[parts[0].strip()] = parts[1].strip()
|
||||
stream.read_bytes(int(headers["Content-Length"]), on_body)
|
||||
|
||||
def on_body(data):
|
||||
print data
|
||||
stream.close()
|
||||
ioloop.IOLoop.instance().stop()
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
|
||||
stream = iostream.IOStream(s)
|
||||
stream.connect(("friendfeed.com", 80), send_request)
|
||||
ioloop.IOLoop.instance().start()
|
||||
|
||||
"""
|
||||
def __init__(self, socket, io_loop=None, max_buffer_size=104857600,
|
||||
read_chunk_size=4096):
|
||||
self.socket = socket
|
||||
self.socket.setblocking(False)
|
||||
self.io_loop = io_loop or ioloop.IOLoop.instance()
|
||||
self.max_buffer_size = max_buffer_size
|
||||
self.read_chunk_size = read_chunk_size
|
||||
self._read_buffer = collections.deque()
|
||||
self._write_buffer = collections.deque()
|
||||
self._read_buffer_size = 0
|
||||
self._write_buffer_frozen = False
|
||||
self._read_delimiter = None
|
||||
self._read_regex = None
|
||||
self._read_bytes = None
|
||||
self._read_until_close = False
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._write_callback = None
|
||||
self._close_callback = None
|
||||
self._connect_callback = None
|
||||
self._connecting = False
|
||||
self._state = None
|
||||
self._pending_callbacks = 0
|
||||
|
||||
def connect(self, address, callback=None):
|
||||
"""Connects the socket to a remote address without blocking.
|
||||
|
||||
May only be called if the socket passed to the constructor was
|
||||
not previously connected. The address parameter is in the
|
||||
same format as for socket.connect, i.e. a (host, port) tuple.
|
||||
If callback is specified, it will be called when the
|
||||
connection is completed.
|
||||
|
||||
Note that it is safe to call IOStream.write while the
|
||||
connection is pending, in which case the data will be written
|
||||
as soon as the connection is ready. Calling IOStream read
|
||||
methods before the socket is connected works on some platforms
|
||||
but is non-portable.
|
||||
"""
|
||||
self._connecting = True
|
||||
try:
|
||||
self.socket.connect(address)
|
||||
except socket.error, e:
|
||||
# In non-blocking mode we expect connect() to raise an
|
||||
# exception with EINPROGRESS or EWOULDBLOCK.
|
||||
#
|
||||
# On freebsd, other errors such as ECONNREFUSED may be
|
||||
# returned immediately when attempting to connect to
|
||||
# localhost, so handle them the same way as an error
|
||||
# reported later in _handle_connect.
|
||||
if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK):
|
||||
logging.warning("Connect error on fd %d: %s",
|
||||
self.socket.fileno(), e)
|
||||
self.close()
|
||||
return
|
||||
self._connect_callback = stack_context.wrap(callback)
|
||||
self._add_io_state(self.io_loop.WRITE)
|
||||
|
||||
def read_until_regex(self, regex, callback):
|
||||
"""Call callback when we read the given regex pattern."""
|
||||
assert not self._read_callback, "Already reading"
|
||||
self._read_regex = re.compile(regex)
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
while True:
|
||||
# See if we've already got the data from a previous read
|
||||
if self._read_from_buffer():
|
||||
return
|
||||
self._check_closed()
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
self._add_io_state(self.io_loop.READ)
|
||||
|
||||
def read_until(self, delimiter, callback):
|
||||
"""Call callback when we read the given delimiter."""
|
||||
assert not self._read_callback, "Already reading"
|
||||
self._read_delimiter = delimiter
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
while True:
|
||||
# See if we've already got the data from a previous read
|
||||
if self._read_from_buffer():
|
||||
return
|
||||
self._check_closed()
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
self._add_io_state(self.io_loop.READ)
|
||||
|
||||
def read_bytes(self, num_bytes, callback, streaming_callback=None):
|
||||
"""Call callback when we read the given number of bytes.
|
||||
|
||||
If a ``streaming_callback`` is given, it will be called with chunks
|
||||
of data as they become available, and the argument to the final
|
||||
``callback`` will be empty.
|
||||
"""
|
||||
assert not self._read_callback, "Already reading"
|
||||
assert isinstance(num_bytes, (int, long))
|
||||
self._read_bytes = num_bytes
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
self._streaming_callback = stack_context.wrap(streaming_callback)
|
||||
while True:
|
||||
if self._read_from_buffer():
|
||||
return
|
||||
self._check_closed()
|
||||
if self._read_to_buffer() == 0:
|
||||
break
|
||||
self._add_io_state(self.io_loop.READ)
|
||||
|
||||
def read_until_close(self, callback, streaming_callback=None):
|
||||
"""Reads all data from the socket until it is closed.
|
||||
|
||||
If a ``streaming_callback`` is given, it will be called with chunks
|
||||
of data as they become available, and the argument to the final
|
||||
``callback`` will be empty.
|
||||
|
||||
Subject to ``max_buffer_size`` limit from `IOStream` constructor if
|
||||
a ``streaming_callback`` is not used.
|
||||
"""
|
||||
assert not self._read_callback, "Already reading"
|
||||
if self.closed():
|
||||
self._run_callback(callback, self._consume(self._read_buffer_size))
|
||||
return
|
||||
self._read_until_close = True
|
||||
self._read_callback = stack_context.wrap(callback)
|
||||
self._streaming_callback = stack_context.wrap(streaming_callback)
|
||||
self._add_io_state(self.io_loop.READ)
|
||||
|
||||
def write(self, data, callback=None):
|
||||
"""Write the given data to this stream.
|
||||
|
||||
If callback is given, we call it when all of the buffered write
|
||||
data has been successfully written to the stream. If there was
|
||||
previously buffered write data and an old write callback, that
|
||||
callback is simply overwritten with this new callback.
|
||||
"""
|
||||
assert isinstance(data, bytes_type)
|
||||
self._check_closed()
|
||||
if data:
|
||||
# We use bool(_write_buffer) as a proxy for write_buffer_size>0,
|
||||
# so never put empty strings in the buffer.
|
||||
self._write_buffer.append(data)
|
||||
self._write_callback = stack_context.wrap(callback)
|
||||
self._handle_write()
|
||||
if self._write_buffer:
|
||||
self._add_io_state(self.io_loop.WRITE)
|
||||
self._maybe_add_error_listener()
|
||||
|
||||
def set_close_callback(self, callback):
|
||||
"""Call the given callback when the stream is closed."""
|
||||
self._close_callback = stack_context.wrap(callback)
|
||||
|
||||
def close(self):
|
||||
"""Close this stream."""
|
||||
if self.socket is not None:
|
||||
if self._read_until_close:
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._read_until_close = False
|
||||
self._run_callback(callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
if self._state is not None:
|
||||
self.io_loop.remove_handler(self.socket.fileno())
|
||||
self._state = None
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
if self._close_callback and self._pending_callbacks == 0:
|
||||
# if there are pending callbacks, don't run the close callback
|
||||
# until they're done (see _maybe_add_error_handler)
|
||||
cb = self._close_callback
|
||||
self._close_callback = None
|
||||
self._run_callback(cb)
|
||||
|
||||
def reading(self):
|
||||
"""Returns true if we are currently reading from the stream."""
|
||||
return self._read_callback is not None
|
||||
|
||||
def writing(self):
|
||||
"""Returns true if we are currently writing to the stream."""
|
||||
return bool(self._write_buffer)
|
||||
|
||||
def closed(self):
|
||||
"""Returns true if the stream has been closed."""
|
||||
return self.socket is None
|
||||
|
||||
def _handle_events(self, fd, events):
|
||||
if not self.socket:
|
||||
logging.warning("Got events for closed stream %d", fd)
|
||||
return
|
||||
try:
|
||||
if events & self.io_loop.READ:
|
||||
self._handle_read()
|
||||
if not self.socket:
|
||||
return
|
||||
if events & self.io_loop.WRITE:
|
||||
if self._connecting:
|
||||
self._handle_connect()
|
||||
self._handle_write()
|
||||
if not self.socket:
|
||||
return
|
||||
if events & self.io_loop.ERROR:
|
||||
# We may have queued up a user callback in _handle_read or
|
||||
# _handle_write, so don't close the IOStream until those
|
||||
# callbacks have had a chance to run.
|
||||
self.io_loop.add_callback(self.close)
|
||||
return
|
||||
state = self.io_loop.ERROR
|
||||
if self.reading():
|
||||
state |= self.io_loop.READ
|
||||
if self.writing():
|
||||
state |= self.io_loop.WRITE
|
||||
if state == self.io_loop.ERROR:
|
||||
state |= self.io_loop.READ
|
||||
if state != self._state:
|
||||
assert self._state is not None, \
|
||||
"shouldn't happen: _handle_events without self._state"
|
||||
self._state = state
|
||||
self.io_loop.update_handler(self.socket.fileno(), self._state)
|
||||
except Exception:
|
||||
logging.error("Uncaught exception, closing connection.",
|
||||
exc_info=True)
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def _run_callback(self, callback, *args):
|
||||
def wrapper():
|
||||
self._pending_callbacks -= 1
|
||||
try:
|
||||
callback(*args)
|
||||
except Exception:
|
||||
logging.error("Uncaught exception, closing connection.",
|
||||
exc_info=True)
|
||||
# Close the socket on an uncaught exception from a user callback
|
||||
# (It would eventually get closed when the socket object is
|
||||
# gc'd, but we don't want to rely on gc happening before we
|
||||
# run out of file descriptors)
|
||||
self.close()
|
||||
# Re-raise the exception so that IOLoop.handle_callback_exception
|
||||
# can see it and log the error
|
||||
raise
|
||||
self._maybe_add_error_listener()
|
||||
# We schedule callbacks to be run on the next IOLoop iteration
|
||||
# rather than running them directly for several reasons:
|
||||
# * Prevents unbounded stack growth when a callback calls an
|
||||
# IOLoop operation that immediately runs another callback
|
||||
# * Provides a predictable execution context for e.g.
|
||||
# non-reentrant mutexes
|
||||
# * Ensures that the try/except in wrapper() is run outside
|
||||
# of the application's StackContexts
|
||||
with stack_context.NullContext():
|
||||
# stack_context was already captured in callback, we don't need to
|
||||
# capture it again for IOStream's wrapper. This is especially
|
||||
# important if the callback was pre-wrapped before entry to
|
||||
# IOStream (as in HTTPConnection._header_callback), as we could
|
||||
# capture and leak the wrong context here.
|
||||
self._pending_callbacks += 1
|
||||
self.io_loop.add_callback(wrapper)
|
||||
|
||||
def _handle_read(self):
|
||||
while True:
|
||||
try:
|
||||
# Read from the socket until we get EWOULDBLOCK or equivalent.
|
||||
# SSL sockets do some internal buffering, and if the data is
|
||||
# sitting in the SSL object's buffer select() and friends
|
||||
# can't see it; the only way to find out if it's there is to
|
||||
# try to read it.
|
||||
result = self._read_to_buffer()
|
||||
except Exception:
|
||||
self.close()
|
||||
return
|
||||
if result == 0:
|
||||
break
|
||||
else:
|
||||
if self._read_from_buffer():
|
||||
return
|
||||
|
||||
def _read_from_socket(self):
|
||||
"""Attempts to read from the socket.
|
||||
|
||||
Returns the data read or None if there is nothing to read.
|
||||
May be overridden in subclasses.
|
||||
"""
|
||||
try:
|
||||
chunk = self.socket.recv(self.read_chunk_size)
|
||||
except socket.error, e:
|
||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
if not chunk:
|
||||
self.close()
|
||||
return None
|
||||
return chunk
|
||||
|
||||
def _read_to_buffer(self):
|
||||
"""Reads from the socket and appends the result to the read buffer.
|
||||
|
||||
Returns the number of bytes read. Returns 0 if there is nothing
|
||||
to read (i.e. the read returns EWOULDBLOCK or equivalent). On
|
||||
error closes the socket and raises an exception.
|
||||
"""
|
||||
try:
|
||||
chunk = self._read_from_socket()
|
||||
except socket.error, e:
|
||||
# ssl.SSLError is a subclass of socket.error
|
||||
logging.warning("Read error on %d: %s",
|
||||
self.socket.fileno(), e)
|
||||
self.close()
|
||||
raise
|
||||
if chunk is None:
|
||||
return 0
|
||||
self._read_buffer.append(chunk)
|
||||
self._read_buffer_size += len(chunk)
|
||||
if self._read_buffer_size >= self.max_buffer_size:
|
||||
logging.error("Reached maximum read buffer size")
|
||||
self.close()
|
||||
raise IOError("Reached maximum read buffer size")
|
||||
return len(chunk)
|
||||
|
||||
def _read_from_buffer(self):
|
||||
"""Attempts to complete the currently-pending read from the buffer.
|
||||
|
||||
Returns True if the read was completed.
|
||||
"""
|
||||
if self._read_bytes is not None:
|
||||
if self._streaming_callback is not None and self._read_buffer_size:
|
||||
bytes_to_consume = min(self._read_bytes, self._read_buffer_size)
|
||||
self._read_bytes -= bytes_to_consume
|
||||
self._run_callback(self._streaming_callback,
|
||||
self._consume(bytes_to_consume))
|
||||
if self._read_buffer_size >= self._read_bytes:
|
||||
num_bytes = self._read_bytes
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_bytes = None
|
||||
self._run_callback(callback, self._consume(num_bytes))
|
||||
return True
|
||||
elif self._read_delimiter is not None:
|
||||
# Multi-byte delimiters (e.g. '\r\n') may straddle two
|
||||
# chunks in the read buffer, so we can't easily find them
|
||||
# without collapsing the buffer. However, since protocols
|
||||
# using delimited reads (as opposed to reads of a known
|
||||
# length) tend to be "line" oriented, the delimiter is likely
|
||||
# to be in the first few chunks. Merge the buffer gradually
|
||||
# since large merges are relatively expensive and get undone in
|
||||
# consume().
|
||||
loc = -1
|
||||
if self._read_buffer:
|
||||
loc = self._read_buffer[0].find(self._read_delimiter)
|
||||
while loc == -1 and len(self._read_buffer) > 1:
|
||||
# Grow by doubling, but don't split the second chunk just
|
||||
# because the first one is small.
|
||||
new_len = max(len(self._read_buffer[0]) * 2,
|
||||
(len(self._read_buffer[0]) +
|
||||
len(self._read_buffer[1])))
|
||||
_merge_prefix(self._read_buffer, new_len)
|
||||
loc = self._read_buffer[0].find(self._read_delimiter)
|
||||
if loc != -1:
|
||||
callback = self._read_callback
|
||||
delimiter_len = len(self._read_delimiter)
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_delimiter = None
|
||||
self._run_callback(callback,
|
||||
self._consume(loc + delimiter_len))
|
||||
return True
|
||||
elif self._read_regex is not None:
|
||||
m = None
|
||||
if self._read_buffer:
|
||||
m = self._read_regex.search(self._read_buffer[0])
|
||||
while m is None and len(self._read_buffer) > 1:
|
||||
# Grow by doubling, but don't split the second chunk just
|
||||
# because the first one is small.
|
||||
new_len = max(len(self._read_buffer[0]) * 2,
|
||||
(len(self._read_buffer[0]) +
|
||||
len(self._read_buffer[1])))
|
||||
_merge_prefix(self._read_buffer, new_len)
|
||||
m = self._read_regex.search(self._read_buffer[0])
|
||||
_merge_prefix(self._read_buffer, sys.maxint)
|
||||
m = self._read_regex.search(self._read_buffer[0])
|
||||
if m:
|
||||
callback = self._read_callback
|
||||
self._read_callback = None
|
||||
self._streaming_callback = None
|
||||
self._read_regex = None
|
||||
self._run_callback(callback, self._consume(m.end()))
|
||||
return True
|
||||
elif self._read_until_close:
|
||||
if self._streaming_callback is not None and self._read_buffer_size:
|
||||
self._run_callback(self._streaming_callback,
|
||||
self._consume(self._read_buffer_size))
|
||||
return False
|
||||
|
||||
def _handle_connect(self):
|
||||
err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
|
||||
if err != 0:
|
||||
# IOLoop implementations may vary: some of them return
|
||||
# an error state before the socket becomes writable, so
|
||||
# in that case a connection failure would be handled by the
|
||||
# error path in _handle_events instead of here.
|
||||
logging.warning("Connect error on fd %d: %s",
|
||||
self.socket.fileno(), errno.errorcode[err])
|
||||
self.close()
|
||||
return
|
||||
if self._connect_callback is not None:
|
||||
callback = self._connect_callback
|
||||
self._connect_callback = None
|
||||
self._run_callback(callback)
|
||||
self._connecting = False
|
||||
|
||||
def _handle_write(self):
|
||||
while self._write_buffer:
|
||||
try:
|
||||
if not self._write_buffer_frozen:
|
||||
# On windows, socket.send blows up if given a
|
||||
# write buffer that's too large, instead of just
|
||||
# returning the number of bytes it was able to
|
||||
# process. Therefore we must not call socket.send
|
||||
# with more than 128KB at a time.
|
||||
_merge_prefix(self._write_buffer, 128 * 1024)
|
||||
num_bytes = self.socket.send(self._write_buffer[0])
|
||||
if num_bytes == 0:
|
||||
# With OpenSSL, if we couldn't write the entire buffer,
|
||||
# the very same string object must be used on the
|
||||
# next call to send. Therefore we suppress
|
||||
# merging the write buffer after an incomplete send.
|
||||
# A cleaner solution would be to set
|
||||
# SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER, but this is
|
||||
# not yet accessible from python
|
||||
# (http://bugs.python.org/issue8240)
|
||||
self._write_buffer_frozen = True
|
||||
break
|
||||
self._write_buffer_frozen = False
|
||||
_merge_prefix(self._write_buffer, num_bytes)
|
||||
self._write_buffer.popleft()
|
||||
except socket.error, e:
|
||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
self._write_buffer_frozen = True
|
||||
break
|
||||
else:
|
||||
logging.warning("Write error on %d: %s",
|
||||
self.socket.fileno(), e)
|
||||
self.close()
|
||||
return
|
||||
if not self._write_buffer and self._write_callback:
|
||||
callback = self._write_callback
|
||||
self._write_callback = None
|
||||
self._run_callback(callback)
|
||||
|
||||
def _consume(self, loc):
|
||||
if loc == 0:
|
||||
return b("")
|
||||
_merge_prefix(self._read_buffer, loc)
|
||||
self._read_buffer_size -= loc
|
||||
return self._read_buffer.popleft()
|
||||
|
||||
def _check_closed(self):
|
||||
if not self.socket:
|
||||
raise IOError("Stream is closed")
|
||||
|
||||
def _maybe_add_error_listener(self):
|
||||
if self._state is None and self._pending_callbacks == 0:
|
||||
if self.socket is None:
|
||||
cb = self._close_callback
|
||||
if cb is not None:
|
||||
self._close_callback = None
|
||||
self._run_callback(cb)
|
||||
else:
|
||||
self._add_io_state(ioloop.IOLoop.READ)
|
||||
|
||||
def _add_io_state(self, state):
|
||||
"""Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler.
|
||||
|
||||
Implementation notes: Reads and writes have a fast path and a
|
||||
slow path. The fast path reads synchronously from socket
|
||||
buffers, while the slow path uses `_add_io_state` to schedule
|
||||
an IOLoop callback. Note that in both cases, the callback is
|
||||
run asynchronously with `_run_callback`.
|
||||
|
||||
To detect closed connections, we must have called
|
||||
`_add_io_state` at some point, but we want to delay this as
|
||||
much as possible so we don't have to set an `IOLoop.ERROR`
|
||||
listener that will be overwritten by the next slow-path
|
||||
operation. As long as there are callbacks scheduled for
|
||||
fast-path ops, those callbacks may do more reads.
|
||||
If a sequence of fast-path ops do not end in a slow-path op,
|
||||
(e.g. for an @asynchronous long-poll request), we must add
|
||||
the error handler. This is done in `_run_callback` and `write`
|
||||
(since the write callback is optional so we can have a
|
||||
fast-path write with no `_run_callback`)
|
||||
"""
|
||||
if self.socket is None:
|
||||
# connection has been closed, so there can be no future events
|
||||
return
|
||||
if self._state is None:
|
||||
self._state = ioloop.IOLoop.ERROR | state
|
||||
with stack_context.NullContext():
|
||||
self.io_loop.add_handler(
|
||||
self.socket.fileno(), self._handle_events, self._state)
|
||||
elif not self._state & state:
|
||||
self._state = self._state | state
|
||||
self.io_loop.update_handler(self.socket.fileno(), self._state)
|
||||
|
||||
|
||||
class SSLIOStream(IOStream):
|
||||
"""A utility class to write to and read from a non-blocking SSL socket.
|
||||
|
||||
If the socket passed to the constructor is already connected,
|
||||
it should be wrapped with::
|
||||
|
||||
ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs)
|
||||
|
||||
before constructing the SSLIOStream. Unconnected sockets will be
|
||||
wrapped when IOStream.connect is finished.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Creates an SSLIOStream.
|
||||
|
||||
If a dictionary is provided as keyword argument ssl_options,
|
||||
it will be used as additional keyword arguments to ssl.wrap_socket.
|
||||
"""
|
||||
self._ssl_options = kwargs.pop('ssl_options', {})
|
||||
super(SSLIOStream, self).__init__(*args, **kwargs)
|
||||
self._ssl_accepting = True
|
||||
self._handshake_reading = False
|
||||
self._handshake_writing = False
|
||||
|
||||
def reading(self):
|
||||
return self._handshake_reading or super(SSLIOStream, self).reading()
|
||||
|
||||
def writing(self):
|
||||
return self._handshake_writing or super(SSLIOStream, self).writing()
|
||||
|
||||
def _do_ssl_handshake(self):
|
||||
# Based on code from test_ssl.py in the python stdlib
|
||||
try:
|
||||
self._handshake_reading = False
|
||||
self._handshake_writing = False
|
||||
self.socket.do_handshake()
|
||||
except ssl.SSLError, err:
|
||||
if err.args[0] == ssl.SSL_ERROR_WANT_READ:
|
||||
self._handshake_reading = True
|
||||
return
|
||||
elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
|
||||
self._handshake_writing = True
|
||||
return
|
||||
elif err.args[0] in (ssl.SSL_ERROR_EOF,
|
||||
ssl.SSL_ERROR_ZERO_RETURN):
|
||||
return self.close()
|
||||
elif err.args[0] == ssl.SSL_ERROR_SSL:
|
||||
logging.warning("SSL Error on %d: %s", self.socket.fileno(), err)
|
||||
return self.close()
|
||||
raise
|
||||
except socket.error, err:
|
||||
if err.args[0] == errno.ECONNABORTED:
|
||||
return self.close()
|
||||
else:
|
||||
self._ssl_accepting = False
|
||||
super(SSLIOStream, self)._handle_connect()
|
||||
|
||||
def _handle_read(self):
|
||||
if self._ssl_accepting:
|
||||
self._do_ssl_handshake()
|
||||
return
|
||||
super(SSLIOStream, self)._handle_read()
|
||||
|
||||
def _handle_write(self):
|
||||
if self._ssl_accepting:
|
||||
self._do_ssl_handshake()
|
||||
return
|
||||
super(SSLIOStream, self)._handle_write()
|
||||
|
||||
def _handle_connect(self):
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
do_handshake_on_connect=False,
|
||||
**self._ssl_options)
|
||||
# Don't call the superclass's _handle_connect (which is responsible
|
||||
# for telling the application that the connection is complete)
|
||||
# until we've completed the SSL handshake (so certificates are
|
||||
# available, etc).
|
||||
|
||||
|
||||
def _read_from_socket(self):
|
||||
if self._ssl_accepting:
|
||||
# If the handshake hasn't finished yet, there can't be anything
|
||||
# to read (attempting to read may or may not raise an exception
|
||||
# depending on the SSL version)
|
||||
return None
|
||||
try:
|
||||
# SSLSocket objects have both a read() and recv() method,
|
||||
# while regular sockets only have recv().
|
||||
# The recv() method blocks (at least in python 2.6) if it is
|
||||
# called when there is nothing to read, so we have to use
|
||||
# read() instead.
|
||||
chunk = self.socket.read(self.read_chunk_size)
|
||||
except ssl.SSLError, e:
|
||||
# SSLError is a subclass of socket.error, so this except
|
||||
# block must come first.
|
||||
if e.args[0] == ssl.SSL_ERROR_WANT_READ:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
except socket.error, e:
|
||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
if not chunk:
|
||||
self.close()
|
||||
return None
|
||||
return chunk
|
||||
|
||||
def _merge_prefix(deque, size):
|
||||
"""Replace the first entries in a deque of strings with a single
|
||||
string of up to size bytes.
|
||||
|
||||
>>> d = collections.deque(['abc', 'de', 'fghi', 'j'])
|
||||
>>> _merge_prefix(d, 5); print d
|
||||
deque(['abcde', 'fghi', 'j'])
|
||||
|
||||
Strings will be split as necessary to reach the desired size.
|
||||
>>> _merge_prefix(d, 7); print d
|
||||
deque(['abcdefg', 'hi', 'j'])
|
||||
|
||||
>>> _merge_prefix(d, 3); print d
|
||||
deque(['abc', 'defg', 'hi', 'j'])
|
||||
|
||||
>>> _merge_prefix(d, 100); print d
|
||||
deque(['abcdefghij'])
|
||||
"""
|
||||
if len(deque) == 1 and len(deque[0]) <= size:
|
||||
return
|
||||
prefix = []
|
||||
remaining = size
|
||||
while deque and remaining > 0:
|
||||
chunk = deque.popleft()
|
||||
if len(chunk) > remaining:
|
||||
deque.appendleft(chunk[remaining:])
|
||||
chunk = chunk[:remaining]
|
||||
prefix.append(chunk)
|
||||
remaining -= len(chunk)
|
||||
# This data structure normally just contains byte strings, but
|
||||
# the unittest gets messy if it doesn't use the default str() type,
|
||||
# so do the merge based on the type of data that's actually present.
|
||||
if prefix:
|
||||
deque.appendleft(type(prefix[0])().join(prefix))
|
||||
if not deque:
|
||||
deque.appendleft(b(""))
|
||||
|
||||
def doctests():
|
||||
import doctest
|
||||
return doctest.DocTestSuite()
|
||||
472
libs/tornado/locale.py
Normal file
472
libs/tornado/locale.py
Normal file
@@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Translation methods for generating localized strings.
|
||||
|
||||
To load a locale and generate a translated string::
|
||||
|
||||
user_locale = locale.get("es_LA")
|
||||
print user_locale.translate("Sign out")
|
||||
|
||||
locale.get() returns the closest matching locale, not necessarily the
|
||||
specific locale you requested. You can support pluralization with
|
||||
additional arguments to translate(), e.g.::
|
||||
|
||||
people = [...]
|
||||
message = user_locale.translate(
|
||||
"%(list)s is online", "%(list)s are online", len(people))
|
||||
print message % {"list": user_locale.list(people)}
|
||||
|
||||
The first string is chosen if len(people) == 1, otherwise the second
|
||||
string is chosen.
|
||||
|
||||
Applications should call one of load_translations (which uses a simple
|
||||
CSV format) or load_gettext_translations (which uses the .mo format
|
||||
supported by gettext and related tools). If neither method is called,
|
||||
the locale.translate method will simply return the original string.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
_default_locale = "en_US"
|
||||
_translations = {}
|
||||
_supported_locales = frozenset([_default_locale])
|
||||
_use_gettext = False
|
||||
|
||||
def get(*locale_codes):
|
||||
"""Returns the closest match for the given locale codes.
|
||||
|
||||
We iterate over all given locale codes in order. If we have a tight
|
||||
or a loose match for the code (e.g., "en" for "en_US"), we return
|
||||
the locale. Otherwise we move to the next code in the list.
|
||||
|
||||
By default we return en_US if no translations are found for any of
|
||||
the specified locales. You can change the default locale with
|
||||
set_default_locale() below.
|
||||
"""
|
||||
return Locale.get_closest(*locale_codes)
|
||||
|
||||
|
||||
def set_default_locale(code):
|
||||
"""Sets the default locale, used in get_closest_locale().
|
||||
|
||||
The default locale is assumed to be the language used for all strings
|
||||
in the system. The translations loaded from disk are mappings from
|
||||
the default locale to the destination locale. Consequently, you don't
|
||||
need to create a translation file for the default locale.
|
||||
"""
|
||||
global _default_locale
|
||||
global _supported_locales
|
||||
_default_locale = code
|
||||
_supported_locales = frozenset(_translations.keys() + [_default_locale])
|
||||
|
||||
|
||||
def load_translations(directory):
|
||||
u"""Loads translations from CSV files in a directory.
|
||||
|
||||
Translations are strings with optional Python-style named placeholders
|
||||
(e.g., "My name is %(name)s") and their associated translations.
|
||||
|
||||
The directory should have translation files of the form LOCALE.csv,
|
||||
e.g. es_GT.csv. The CSV files should have two or three columns: string,
|
||||
translation, and an optional plural indicator. Plural indicators should
|
||||
be one of "plural" or "singular". A given string can have both singular
|
||||
and plural forms. For example "%(name)s liked this" may have a
|
||||
different verb conjugation depending on whether %(name)s is one
|
||||
name or a list of names. There should be two rows in the CSV file for
|
||||
that string, one with plural indicator "singular", and one "plural".
|
||||
For strings with no verbs that would change on translation, simply
|
||||
use "unknown" or the empty string (or don't include the column at all).
|
||||
|
||||
The file is read using the csv module in the default "excel" dialect.
|
||||
In this format there should not be spaces after the commas.
|
||||
|
||||
Example translation es_LA.csv:
|
||||
|
||||
"I love you","Te amo"
|
||||
"%(name)s liked this","A %(name)s les gust\u00f3 esto","plural"
|
||||
"%(name)s liked this","A %(name)s le gust\u00f3 esto","singular"
|
||||
|
||||
"""
|
||||
global _translations
|
||||
global _supported_locales
|
||||
_translations = {}
|
||||
for path in os.listdir(directory):
|
||||
if not path.endswith(".csv"): continue
|
||||
locale, extension = path.split(".")
|
||||
if not re.match("[a-z]+(_[A-Z]+)?$", locale):
|
||||
logging.error("Unrecognized locale %r (path: %s)", locale,
|
||||
os.path.join(directory, path))
|
||||
continue
|
||||
f = open(os.path.join(directory, path), "r")
|
||||
_translations[locale] = {}
|
||||
for i, row in enumerate(csv.reader(f)):
|
||||
if not row or len(row) < 2: continue
|
||||
row = [c.decode("utf-8").strip() for c in row]
|
||||
english, translation = row[:2]
|
||||
if len(row) > 2:
|
||||
plural = row[2] or "unknown"
|
||||
else:
|
||||
plural = "unknown"
|
||||
if plural not in ("plural", "singular", "unknown"):
|
||||
logging.error("Unrecognized plural indicator %r in %s line %d",
|
||||
plural, path, i + 1)
|
||||
continue
|
||||
_translations[locale].setdefault(plural, {})[english] = translation
|
||||
f.close()
|
||||
_supported_locales = frozenset(_translations.keys() + [_default_locale])
|
||||
logging.info("Supported locales: %s", sorted(_supported_locales))
|
||||
|
||||
def load_gettext_translations(directory, domain):
|
||||
"""Loads translations from gettext's locale tree
|
||||
|
||||
Locale tree is similar to system's /usr/share/locale, like:
|
||||
|
||||
{directory}/{lang}/LC_MESSAGES/{domain}.mo
|
||||
|
||||
Three steps are required to have you app translated:
|
||||
|
||||
1. Generate POT translation file
|
||||
xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc
|
||||
|
||||
2. Merge against existing POT file:
|
||||
msgmerge old.po cyclone.po > new.po
|
||||
|
||||
3. Compile:
|
||||
msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
|
||||
"""
|
||||
import gettext
|
||||
global _translations
|
||||
global _supported_locales
|
||||
global _use_gettext
|
||||
_translations = {}
|
||||
for lang in os.listdir(directory):
|
||||
if lang.startswith('.'): continue # skip .svn, etc
|
||||
if os.path.isfile(os.path.join(directory, lang)): continue
|
||||
try:
|
||||
os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo"))
|
||||
_translations[lang] = gettext.translation(domain, directory,
|
||||
languages=[lang])
|
||||
except Exception, e:
|
||||
logging.error("Cannot load translation for '%s': %s", lang, str(e))
|
||||
continue
|
||||
_supported_locales = frozenset(_translations.keys() + [_default_locale])
|
||||
_use_gettext = True
|
||||
logging.info("Supported locales: %s", sorted(_supported_locales))
|
||||
|
||||
|
||||
def get_supported_locales(cls):
|
||||
"""Returns a list of all the supported locale codes."""
|
||||
return _supported_locales
|
||||
|
||||
|
||||
class Locale(object):
|
||||
"""Object representing a locale.
|
||||
|
||||
After calling one of `load_translations` or `load_gettext_translations`,
|
||||
call `get` or `get_closest` to get a Locale object.
|
||||
"""
|
||||
@classmethod
|
||||
def get_closest(cls, *locale_codes):
|
||||
"""Returns the closest match for the given locale code."""
|
||||
for code in locale_codes:
|
||||
if not code: continue
|
||||
code = code.replace("-", "_")
|
||||
parts = code.split("_")
|
||||
if len(parts) > 2:
|
||||
continue
|
||||
elif len(parts) == 2:
|
||||
code = parts[0].lower() + "_" + parts[1].upper()
|
||||
if code in _supported_locales:
|
||||
return cls.get(code)
|
||||
if parts[0].lower() in _supported_locales:
|
||||
return cls.get(parts[0].lower())
|
||||
return cls.get(_default_locale)
|
||||
|
||||
@classmethod
|
||||
def get(cls, code):
|
||||
"""Returns the Locale for the given locale code.
|
||||
|
||||
If it is not supported, we raise an exception.
|
||||
"""
|
||||
if not hasattr(cls, "_cache"):
|
||||
cls._cache = {}
|
||||
if code not in cls._cache:
|
||||
assert code in _supported_locales
|
||||
translations = _translations.get(code, None)
|
||||
if translations is None:
|
||||
locale = CSVLocale(code, {})
|
||||
elif _use_gettext:
|
||||
locale = GettextLocale(code, translations)
|
||||
else:
|
||||
locale = CSVLocale(code, translations)
|
||||
cls._cache[code] = locale
|
||||
return cls._cache[code]
|
||||
|
||||
def __init__(self, code, translations):
|
||||
self.code = code
|
||||
self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
|
||||
self.rtl = False
|
||||
for prefix in ["fa", "ar", "he"]:
|
||||
if self.code.startswith(prefix):
|
||||
self.rtl = True
|
||||
break
|
||||
self.translations = translations
|
||||
|
||||
# Initialize strings for date formatting
|
||||
_ = self.translate
|
||||
self._months = [
|
||||
_("January"), _("February"), _("March"), _("April"),
|
||||
_("May"), _("June"), _("July"), _("August"),
|
||||
_("September"), _("October"), _("November"), _("December")]
|
||||
self._weekdays = [
|
||||
_("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
|
||||
_("Friday"), _("Saturday"), _("Sunday")]
|
||||
|
||||
def translate(self, message, plural_message=None, count=None):
|
||||
"""Returns the translation for the given message for this locale.
|
||||
|
||||
If plural_message is given, you must also provide count. We return
|
||||
plural_message when count != 1, and we return the singular form
|
||||
for the given message when count == 1.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
|
||||
full_format=False):
|
||||
"""Formats the given date (which should be GMT).
|
||||
|
||||
By default, we return a relative time (e.g., "2 minutes ago"). You
|
||||
can return an absolute date string with relative=False.
|
||||
|
||||
You can force a full format date ("July 10, 1980") with
|
||||
full_format=True.
|
||||
|
||||
This method is primarily intended for dates in the past.
|
||||
For dates in the future, we fall back to full format.
|
||||
"""
|
||||
if self.code.startswith("ru"):
|
||||
relative = False
|
||||
if type(date) in (int, long, float):
|
||||
date = datetime.datetime.utcfromtimestamp(date)
|
||||
now = datetime.datetime.utcnow()
|
||||
if date > now:
|
||||
if relative and (date - now).seconds < 60:
|
||||
# Due to click skew, things are some things slightly
|
||||
# in the future. Round timestamps in the immediate
|
||||
# future down to now in relative mode.
|
||||
date = now
|
||||
else:
|
||||
# Otherwise, future dates always use the full format.
|
||||
full_format = True
|
||||
local_date = date - datetime.timedelta(minutes=gmt_offset)
|
||||
local_now = now - datetime.timedelta(minutes=gmt_offset)
|
||||
local_yesterday = local_now - datetime.timedelta(hours=24)
|
||||
difference = now - date
|
||||
seconds = difference.seconds
|
||||
days = difference.days
|
||||
|
||||
_ = self.translate
|
||||
format = None
|
||||
if not full_format:
|
||||
if relative and days == 0:
|
||||
if seconds < 50:
|
||||
return _("1 second ago", "%(seconds)d seconds ago",
|
||||
seconds) % { "seconds": seconds }
|
||||
|
||||
if seconds < 50 * 60:
|
||||
minutes = round(seconds / 60.0)
|
||||
return _("1 minute ago", "%(minutes)d minutes ago",
|
||||
minutes) % { "minutes": minutes }
|
||||
|
||||
hours = round(seconds / (60.0 * 60))
|
||||
return _("1 hour ago", "%(hours)d hours ago",
|
||||
hours) % { "hours": hours }
|
||||
|
||||
if days == 0:
|
||||
format = _("%(time)s")
|
||||
elif days == 1 and local_date.day == local_yesterday.day and \
|
||||
relative:
|
||||
format = _("yesterday") if shorter else \
|
||||
_("yesterday at %(time)s")
|
||||
elif days < 5:
|
||||
format = _("%(weekday)s") if shorter else \
|
||||
_("%(weekday)s at %(time)s")
|
||||
elif days < 334: # 11mo, since confusing for same month last year
|
||||
format = _("%(month_name)s %(day)s") if shorter else \
|
||||
_("%(month_name)s %(day)s at %(time)s")
|
||||
|
||||
if format is None:
|
||||
format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
|
||||
_("%(month_name)s %(day)s, %(year)s at %(time)s")
|
||||
|
||||
tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
|
||||
if tfhour_clock:
|
||||
str_time = "%d:%02d" % (local_date.hour, local_date.minute)
|
||||
elif self.code == "zh_CN":
|
||||
str_time = "%s%d:%02d" % (
|
||||
(u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
|
||||
local_date.hour % 12 or 12, local_date.minute)
|
||||
else:
|
||||
str_time = "%d:%02d %s" % (
|
||||
local_date.hour % 12 or 12, local_date.minute,
|
||||
("am", "pm")[local_date.hour >= 12])
|
||||
|
||||
return format % {
|
||||
"month_name": self._months[local_date.month - 1],
|
||||
"weekday": self._weekdays[local_date.weekday()],
|
||||
"day": str(local_date.day),
|
||||
"year": str(local_date.year),
|
||||
"time": str_time
|
||||
}
|
||||
|
||||
def format_day(self, date, gmt_offset=0, dow=True):
|
||||
"""Formats the given date as a day of week.
|
||||
|
||||
Example: "Monday, January 22". You can remove the day of week with
|
||||
dow=False.
|
||||
"""
|
||||
local_date = date - datetime.timedelta(minutes=gmt_offset)
|
||||
_ = self.translate
|
||||
if dow:
|
||||
return _("%(weekday)s, %(month_name)s %(day)s") % {
|
||||
"month_name": self._months[local_date.month - 1],
|
||||
"weekday": self._weekdays[local_date.weekday()],
|
||||
"day": str(local_date.day),
|
||||
}
|
||||
else:
|
||||
return _("%(month_name)s %(day)s") % {
|
||||
"month_name": self._months[local_date.month - 1],
|
||||
"day": str(local_date.day),
|
||||
}
|
||||
|
||||
def list(self, parts):
|
||||
"""Returns a comma-separated list for the given list of parts.
|
||||
|
||||
The format is, e.g., "A, B and C", "A and B" or just "A" for lists
|
||||
of size 1.
|
||||
"""
|
||||
_ = self.translate
|
||||
if len(parts) == 0: return ""
|
||||
if len(parts) == 1: return parts[0]
|
||||
comma = u' \u0648 ' if self.code.startswith("fa") else u", "
|
||||
return _("%(commas)s and %(last)s") % {
|
||||
"commas": comma.join(parts[:-1]),
|
||||
"last": parts[len(parts) - 1],
|
||||
}
|
||||
|
||||
def friendly_number(self, value):
|
||||
"""Returns a comma-separated number for the given integer."""
|
||||
if self.code not in ("en", "en_US"):
|
||||
return str(value)
|
||||
value = str(value)
|
||||
parts = []
|
||||
while value:
|
||||
parts.append(value[-3:])
|
||||
value = value[:-3]
|
||||
return ",".join(reversed(parts))
|
||||
|
||||
class CSVLocale(Locale):
|
||||
"""Locale implementation using tornado's CSV translation format."""
|
||||
def translate(self, message, plural_message=None, count=None):
|
||||
if plural_message is not None:
|
||||
assert count is not None
|
||||
if count != 1:
|
||||
message = plural_message
|
||||
message_dict = self.translations.get("plural", {})
|
||||
else:
|
||||
message_dict = self.translations.get("singular", {})
|
||||
else:
|
||||
message_dict = self.translations.get("unknown", {})
|
||||
return message_dict.get(message, message)
|
||||
|
||||
class GettextLocale(Locale):
|
||||
"""Locale implementation using the gettext module."""
|
||||
def translate(self, message, plural_message=None, count=None):
|
||||
if plural_message is not None:
|
||||
assert count is not None
|
||||
return self.translations.ungettext(message, plural_message, count)
|
||||
else:
|
||||
return self.translations.ugettext(message)
|
||||
|
||||
LOCALE_NAMES = {
|
||||
"af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
|
||||
"am_ET": {"name_en": u"Amharic", "name": u'\u12a0\u121b\u122d\u129b'},
|
||||
"ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},
|
||||
"bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},
|
||||
"bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"},
|
||||
"bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
|
||||
"ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
|
||||
"cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
|
||||
"cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
|
||||
"da_DK": {"name_en": u"Danish", "name": u"Dansk"},
|
||||
"de_DE": {"name_en": u"German", "name": u"Deutsch"},
|
||||
"el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},
|
||||
"en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
|
||||
"en_US": {"name_en": u"English (US)", "name": u"English (US)"},
|
||||
"es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"},
|
||||
"es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
|
||||
"et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
|
||||
"eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
|
||||
"fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"},
|
||||
"fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
|
||||
"fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
|
||||
"fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
|
||||
"ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
|
||||
"gl_ES": {"name_en": u"Galician", "name": u"Galego"},
|
||||
"he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"},
|
||||
"hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"},
|
||||
"hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
|
||||
"hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
|
||||
"id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
|
||||
"is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
|
||||
"it_IT": {"name_en": u"Italian", "name": u"Italiano"},
|
||||
"ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"},
|
||||
"ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"},
|
||||
"lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
|
||||
"lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"},
|
||||
"mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"},
|
||||
"ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
|
||||
"ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
|
||||
"nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
|
||||
"nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
|
||||
"nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"},
|
||||
"pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},
|
||||
"pl_PL": {"name_en": u"Polish", "name": u"Polski"},
|
||||
"pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"},
|
||||
"pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"},
|
||||
"ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"},
|
||||
"ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},
|
||||
"sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
|
||||
"sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
|
||||
"sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
|
||||
"sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
|
||||
"sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
|
||||
"sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
|
||||
"ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},
|
||||
"te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},
|
||||
"th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
|
||||
"tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
|
||||
"tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"},
|
||||
"uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"},
|
||||
"vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"},
|
||||
"zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\u4e2d\u6587(\u7b80\u4f53)"},
|
||||
"zh_TW": {"name_en": u"Chinese (Traditional)", "name": u"\u4e2d\u6587(\u7e41\u9ad4)"},
|
||||
}
|
||||
314
libs/tornado/netutil.py
Normal file
314
libs/tornado/netutil.py
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Miscellaneous network utility code."""
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import stat
|
||||
|
||||
from tornado import process
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.iostream import IOStream, SSLIOStream
|
||||
from tornado.platform.auto import set_close_exec
|
||||
|
||||
try:
|
||||
import ssl # Python 2.6+
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
class TCPServer(object):
|
||||
r"""A non-blocking, single-threaded TCP server.
|
||||
|
||||
To use `TCPServer`, define a subclass which overrides the `handle_stream`
|
||||
method.
|
||||
|
||||
`TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
|
||||
To make this server serve SSL traffic, send the ssl_options dictionary
|
||||
argument with the arguments required for the `ssl.wrap_socket` method,
|
||||
including "certfile" and "keyfile"::
|
||||
|
||||
TCPServer(ssl_options={
|
||||
"certfile": os.path.join(data_dir, "mydomain.crt"),
|
||||
"keyfile": os.path.join(data_dir, "mydomain.key"),
|
||||
})
|
||||
|
||||
`TCPServer` initialization follows one of three patterns:
|
||||
|
||||
1. `listen`: simple single-process::
|
||||
|
||||
server = TCPServer()
|
||||
server.listen(8888)
|
||||
IOLoop.instance().start()
|
||||
|
||||
2. `bind`/`start`: simple multi-process::
|
||||
|
||||
server = TCPServer()
|
||||
server.bind(8888)
|
||||
server.start(0) # Forks multiple sub-processes
|
||||
IOLoop.instance().start()
|
||||
|
||||
When using this interface, an `IOLoop` must *not* be passed
|
||||
to the `TCPServer` constructor. `start` will always start
|
||||
the server on the default singleton `IOLoop`.
|
||||
|
||||
3. `add_sockets`: advanced multi-process::
|
||||
|
||||
sockets = bind_sockets(8888)
|
||||
tornado.process.fork_processes(0)
|
||||
server = TCPServer()
|
||||
server.add_sockets(sockets)
|
||||
IOLoop.instance().start()
|
||||
|
||||
The `add_sockets` interface is more complicated, but it can be
|
||||
used with `tornado.process.fork_processes` to give you more
|
||||
flexibility in when the fork happens. `add_sockets` can
|
||||
also be used in single-process servers if you want to create
|
||||
your listening sockets in some way other than
|
||||
`bind_sockets`.
|
||||
"""
|
||||
def __init__(self, io_loop=None, ssl_options=None):
|
||||
self.io_loop = io_loop
|
||||
self.ssl_options = ssl_options
|
||||
self._sockets = {} # fd -> socket object
|
||||
self._pending_sockets = []
|
||||
self._started = False
|
||||
|
||||
def listen(self, port, address=""):
|
||||
"""Starts accepting connections on the given port.
|
||||
|
||||
This method may be called more than once to listen on multiple ports.
|
||||
`listen` takes effect immediately; it is not necessary to call
|
||||
`TCPServer.start` afterwards. It is, however, necessary to start
|
||||
the `IOLoop`.
|
||||
"""
|
||||
sockets = bind_sockets(port, address=address)
|
||||
self.add_sockets(sockets)
|
||||
|
||||
def add_sockets(self, sockets):
|
||||
"""Makes this server start accepting connections on the given sockets.
|
||||
|
||||
The ``sockets`` parameter is a list of socket objects such as
|
||||
those returned by `bind_sockets`.
|
||||
`add_sockets` is typically used in combination with that
|
||||
method and `tornado.process.fork_processes` to provide greater
|
||||
control over the initialization of a multi-process server.
|
||||
"""
|
||||
if self.io_loop is None:
|
||||
self.io_loop = IOLoop.instance()
|
||||
|
||||
for sock in sockets:
|
||||
self._sockets[sock.fileno()] = sock
|
||||
add_accept_handler(sock, self._handle_connection,
|
||||
io_loop=self.io_loop)
|
||||
|
||||
def add_socket(self, socket):
|
||||
"""Singular version of `add_sockets`. Takes a single socket object."""
|
||||
self.add_sockets([socket])
|
||||
|
||||
def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128):
|
||||
"""Binds this server to the given port on the given address.
|
||||
|
||||
To start the server, call `start`. If you want to run this server
|
||||
in a single process, you can call `listen` as a shortcut to the
|
||||
sequence of `bind` and `start` calls.
|
||||
|
||||
Address may be either an IP address or hostname. If it's a hostname,
|
||||
the server will listen on all IP addresses associated with the
|
||||
name. Address may be an empty string or None to listen on all
|
||||
available interfaces. Family may be set to either ``socket.AF_INET``
|
||||
or ``socket.AF_INET6`` to restrict to ipv4 or ipv6 addresses, otherwise
|
||||
both will be used if available.
|
||||
|
||||
The ``backlog`` argument has the same meaning as for
|
||||
`socket.listen`.
|
||||
|
||||
This method may be called multiple times prior to `start` to listen
|
||||
on multiple ports or interfaces.
|
||||
"""
|
||||
sockets = bind_sockets(port, address=address, family=family,
|
||||
backlog=backlog)
|
||||
if self._started:
|
||||
self.add_sockets(sockets)
|
||||
else:
|
||||
self._pending_sockets.extend(sockets)
|
||||
|
||||
def start(self, num_processes=1):
|
||||
"""Starts this server in the IOLoop.
|
||||
|
||||
By default, we run the server in this process and do not fork any
|
||||
additional child process.
|
||||
|
||||
If num_processes is ``None`` or <= 0, we detect the number of cores
|
||||
available on this machine and fork that number of child
|
||||
processes. If num_processes is given and > 1, we fork that
|
||||
specific number of sub-processes.
|
||||
|
||||
Since we use processes and not threads, there is no shared memory
|
||||
between any server code.
|
||||
|
||||
Note that multiple processes are not compatible with the autoreload
|
||||
module (or the ``debug=True`` option to `tornado.web.Application`).
|
||||
When using multiple processes, no IOLoops can be created or
|
||||
referenced until after the call to ``TCPServer.start(n)``.
|
||||
"""
|
||||
assert not self._started
|
||||
self._started = True
|
||||
if num_processes != 1:
|
||||
process.fork_processes(num_processes)
|
||||
sockets = self._pending_sockets
|
||||
self._pending_sockets = []
|
||||
self.add_sockets(sockets)
|
||||
|
||||
def stop(self):
|
||||
"""Stops listening for new connections.
|
||||
|
||||
Requests currently in progress may still continue after the
|
||||
server is stopped.
|
||||
"""
|
||||
for fd, sock in self._sockets.iteritems():
|
||||
self.io_loop.remove_handler(fd)
|
||||
sock.close()
|
||||
|
||||
def handle_stream(self, stream, address):
|
||||
"""Override to handle a new `IOStream` from an incoming connection."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _handle_connection(self, connection, address):
|
||||
if self.ssl_options is not None:
|
||||
assert ssl, "Python 2.6+ and OpenSSL required for SSL"
|
||||
try:
|
||||
connection = ssl.wrap_socket(connection,
|
||||
server_side=True,
|
||||
do_handshake_on_connect=False,
|
||||
**self.ssl_options)
|
||||
except ssl.SSLError, err:
|
||||
if err.args[0] == ssl.SSL_ERROR_EOF:
|
||||
return connection.close()
|
||||
else:
|
||||
raise
|
||||
except socket.error, err:
|
||||
if err.args[0] == errno.ECONNABORTED:
|
||||
return connection.close()
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
if self.ssl_options is not None:
|
||||
stream = SSLIOStream(connection, io_loop=self.io_loop)
|
||||
else:
|
||||
stream = IOStream(connection, io_loop=self.io_loop)
|
||||
self.handle_stream(stream, address)
|
||||
except Exception:
|
||||
logging.error("Error in connection callback", exc_info=True)
|
||||
|
||||
|
||||
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
|
||||
"""Creates listening sockets bound to the given port and address.
|
||||
|
||||
Returns a list of socket objects (multiple sockets are returned if
|
||||
the given address maps to multiple IP addresses, which is most common
|
||||
for mixed IPv4 and IPv6 use).
|
||||
|
||||
Address may be either an IP address or hostname. If it's a hostname,
|
||||
the server will listen on all IP addresses associated with the
|
||||
name. Address may be an empty string or None to listen on all
|
||||
available interfaces. Family may be set to either socket.AF_INET
|
||||
or socket.AF_INET6 to restrict to ipv4 or ipv6 addresses, otherwise
|
||||
both will be used if available.
|
||||
|
||||
The ``backlog`` argument has the same meaning as for
|
||||
``socket.listen()``.
|
||||
"""
|
||||
sockets = []
|
||||
if address == "":
|
||||
address = None
|
||||
flags = socket.AI_PASSIVE
|
||||
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
|
||||
0, flags)):
|
||||
af, socktype, proto, canonname, sockaddr = res
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
set_close_exec(sock.fileno())
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if af == socket.AF_INET6:
|
||||
# On linux, ipv6 sockets accept ipv4 too by default,
|
||||
# but this makes it impossible to bind to both
|
||||
# 0.0.0.0 in ipv4 and :: in ipv6. On other systems,
|
||||
# separate sockets *must* be used to listen for both ipv4
|
||||
# and ipv6. For consistency, always disable ipv4 on our
|
||||
# ipv6 sockets and use a separate ipv4 socket when needed.
|
||||
#
|
||||
# Python 2.x on windows doesn't have IPPROTO_IPV6.
|
||||
if hasattr(socket, "IPPROTO_IPV6"):
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
||||
sock.setblocking(0)
|
||||
sock.bind(sockaddr)
|
||||
sock.listen(backlog)
|
||||
sockets.append(sock)
|
||||
return sockets
|
||||
|
||||
if hasattr(socket, 'AF_UNIX'):
|
||||
def bind_unix_socket(file, mode=0600, backlog=128):
|
||||
"""Creates a listening unix socket.
|
||||
|
||||
If a socket with the given name already exists, it will be deleted.
|
||||
If any other file with that name exists, an exception will be
|
||||
raised.
|
||||
|
||||
Returns a socket object (not a list of socket objects like
|
||||
`bind_sockets`)
|
||||
"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
set_close_exec(sock.fileno())
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setblocking(0)
|
||||
try:
|
||||
st = os.stat(file)
|
||||
except OSError, err:
|
||||
if err.errno != errno.ENOENT:
|
||||
raise
|
||||
else:
|
||||
if stat.S_ISSOCK(st.st_mode):
|
||||
os.remove(file)
|
||||
else:
|
||||
raise ValueError("File %s exists and is not a socket", file)
|
||||
sock.bind(file)
|
||||
os.chmod(file, mode)
|
||||
sock.listen(backlog)
|
||||
return sock
|
||||
|
||||
def add_accept_handler(sock, callback, io_loop=None):
|
||||
"""Adds an ``IOLoop`` event handler to accept new connections on ``sock``.
|
||||
|
||||
When a connection is accepted, ``callback(connection, address)`` will
|
||||
be run (``connection`` is a socket object, and ``address`` is the
|
||||
address of the other end of the connection). Note that this signature
|
||||
is different from the ``callback(fd, events)`` signature used for
|
||||
``IOLoop`` handlers.
|
||||
"""
|
||||
if io_loop is None:
|
||||
io_loop = IOLoop.instance()
|
||||
def accept_handler(fd, events):
|
||||
while True:
|
||||
try:
|
||||
connection, address = sock.accept()
|
||||
except socket.error, e:
|
||||
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
return
|
||||
raise
|
||||
callback(connection, address)
|
||||
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
|
||||
422
libs/tornado/options.py
Normal file
422
libs/tornado/options.py
Normal file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A command line parsing module that lets modules define their own options.
|
||||
|
||||
Each module defines its own options, e.g.::
|
||||
|
||||
from tornado.options import define, options
|
||||
|
||||
define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
|
||||
define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
|
||||
help="Main user memcache servers")
|
||||
|
||||
def connect():
|
||||
db = database.Connection(options.mysql_host)
|
||||
...
|
||||
|
||||
The main() method of your application does not need to be aware of all of
|
||||
the options used throughout your program; they are all automatically loaded
|
||||
when the modules are loaded. Your main() method can parse the command line
|
||||
or parse a config file with::
|
||||
|
||||
import tornado.options
|
||||
tornado.options.parse_config_file("/etc/server.conf")
|
||||
tornado.options.parse_command_line()
|
||||
|
||||
Command line formats are what you would expect ("--myoption=myvalue").
|
||||
Config files are just Python files. Global names become options, e.g.::
|
||||
|
||||
myoption = "myvalue"
|
||||
myotheroption = "myothervalue"
|
||||
|
||||
We support datetimes, timedeltas, ints, and floats (just pass a 'type'
|
||||
kwarg to define). We also accept multi-value options. See the documentation
|
||||
for define() below.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import logging.handlers
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from tornado.escape import _unicode
|
||||
|
||||
# For pretty log messages, if available
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
curses = None
|
||||
|
||||
|
||||
def define(name, default=None, type=None, help=None, metavar=None,
|
||||
multiple=False, group=None):
|
||||
"""Defines a new command line option.
|
||||
|
||||
If type is given (one of str, float, int, datetime, or timedelta)
|
||||
or can be inferred from the default, we parse the command line
|
||||
arguments based on the given type. If multiple is True, we accept
|
||||
comma-separated values, and the option value is always a list.
|
||||
|
||||
For multi-value integers, we also accept the syntax x:y, which
|
||||
turns into range(x, y) - very useful for long integer ranges.
|
||||
|
||||
help and metavar are used to construct the automatically generated
|
||||
command line help string. The help message is formatted like::
|
||||
|
||||
--name=METAVAR help string
|
||||
|
||||
group is used to group the defined options in logical groups. By default,
|
||||
command line options are grouped by the defined file.
|
||||
|
||||
Command line option names must be unique globally. They can be parsed
|
||||
from the command line with parse_command_line() or parsed from a
|
||||
config file with parse_config_file.
|
||||
"""
|
||||
if name in options:
|
||||
raise Error("Option %r already defined in %s", name,
|
||||
options[name].file_name)
|
||||
frame = sys._getframe(0)
|
||||
options_file = frame.f_code.co_filename
|
||||
file_name = frame.f_back.f_code.co_filename
|
||||
if file_name == options_file: file_name = ""
|
||||
if type is None:
|
||||
if not multiple and default is not None:
|
||||
type = default.__class__
|
||||
else:
|
||||
type = str
|
||||
if group:
|
||||
group_name = group
|
||||
else:
|
||||
group_name = file_name
|
||||
options[name] = _Option(name, file_name=file_name, default=default,
|
||||
type=type, help=help, metavar=metavar,
|
||||
multiple=multiple, group_name=group_name)
|
||||
|
||||
|
||||
def parse_command_line(args=None):
|
||||
"""Parses all options given on the command line.
|
||||
|
||||
We return all command line arguments that are not options as a list.
|
||||
"""
|
||||
if args is None: args = sys.argv
|
||||
remaining = []
|
||||
for i in xrange(1, len(args)):
|
||||
# All things after the last option are command line arguments
|
||||
if not args[i].startswith("-"):
|
||||
remaining = args[i:]
|
||||
break
|
||||
if args[i] == "--":
|
||||
remaining = args[i+1:]
|
||||
break
|
||||
arg = args[i].lstrip("-")
|
||||
name, equals, value = arg.partition("=")
|
||||
name = name.replace('-', '_')
|
||||
if not name in options:
|
||||
print_help()
|
||||
raise Error('Unrecognized command line option: %r' % name)
|
||||
option = options[name]
|
||||
if not equals:
|
||||
if option.type == bool:
|
||||
value = "true"
|
||||
else:
|
||||
raise Error('Option %r requires a value' % name)
|
||||
option.parse(value)
|
||||
if options.help:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
# Set up log level and pretty console logging by default
|
||||
if options.logging != 'none':
|
||||
logging.getLogger().setLevel(getattr(logging, options.logging.upper()))
|
||||
enable_pretty_logging()
|
||||
|
||||
return remaining
|
||||
|
||||
|
||||
def parse_config_file(path):
|
||||
"""Parses and loads the Python config file at the given path."""
|
||||
config = {}
|
||||
execfile(path, config, config)
|
||||
for name in config:
|
||||
if name in options:
|
||||
options[name].set(config[name])
|
||||
|
||||
|
||||
def print_help(file=sys.stdout):
|
||||
"""Prints all the command line options to stdout."""
|
||||
print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
|
||||
print >> file, ""
|
||||
print >> file, "Options:"
|
||||
by_group = {}
|
||||
for option in options.itervalues():
|
||||
by_group.setdefault(option.group_name, []).append(option)
|
||||
|
||||
for filename, o in sorted(by_group.items()):
|
||||
if filename: print >> file, filename
|
||||
o.sort(key=lambda option: option.name)
|
||||
for option in o:
|
||||
prefix = option.name
|
||||
if option.metavar:
|
||||
prefix += "=" + option.metavar
|
||||
print >> file, " --%-30s %s" % (prefix, option.help or "")
|
||||
print >> file
|
||||
|
||||
|
||||
class _Options(dict):
|
||||
"""Our global program options, an dictionary with object-like access."""
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
if not hasattr(cls, "_instance"):
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def __getattr__(self, name):
|
||||
if isinstance(self.get(name), _Option):
|
||||
return self[name].value()
|
||||
raise AttributeError("Unrecognized option %r" % name)
|
||||
|
||||
|
||||
class _Option(object):
|
||||
def __init__(self, name, default=None, type=str, help=None, metavar=None,
|
||||
multiple=False, file_name=None, group_name=None):
|
||||
if default is None and multiple:
|
||||
default = []
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.help = help
|
||||
self.metavar = metavar
|
||||
self.multiple = multiple
|
||||
self.file_name = file_name
|
||||
self.group_name = group_name
|
||||
self.default = default
|
||||
self._value = None
|
||||
|
||||
def value(self):
|
||||
return self.default if self._value is None else self._value
|
||||
|
||||
def parse(self, value):
|
||||
_parse = {
|
||||
datetime.datetime: self._parse_datetime,
|
||||
datetime.timedelta: self._parse_timedelta,
|
||||
bool: self._parse_bool,
|
||||
str: self._parse_string,
|
||||
}.get(self.type, self.type)
|
||||
if self.multiple:
|
||||
if self._value is None:
|
||||
self._value = []
|
||||
for part in value.split(","):
|
||||
if self.type in (int, long):
|
||||
# allow ranges of the form X:Y (inclusive at both ends)
|
||||
lo, _, hi = part.partition(":")
|
||||
lo = _parse(lo)
|
||||
hi = _parse(hi) if hi else lo
|
||||
self._value.extend(range(lo, hi+1))
|
||||
else:
|
||||
self._value.append(_parse(part))
|
||||
else:
|
||||
self._value = _parse(value)
|
||||
return self.value()
|
||||
|
||||
def set(self, value):
|
||||
if self.multiple:
|
||||
if not isinstance(value, list):
|
||||
raise Error("Option %r is required to be a list of %s" %
|
||||
(self.name, self.type.__name__))
|
||||
for item in value:
|
||||
if item != None and not isinstance(item, self.type):
|
||||
raise Error("Option %r is required to be a list of %s" %
|
||||
(self.name, self.type.__name__))
|
||||
else:
|
||||
if value != None and not isinstance(value, self.type):
|
||||
raise Error("Option %r is required to be a %s" %
|
||||
(self.name, self.type.__name__))
|
||||
self._value = value
|
||||
|
||||
# Supported date/time formats in our options
|
||||
_DATETIME_FORMATS = [
|
||||
"%a %b %d %H:%M:%S %Y",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y-%m-%dT%H:%M",
|
||||
"%Y%m%d %H:%M:%S",
|
||||
"%Y%m%d %H:%M",
|
||||
"%Y-%m-%d",
|
||||
"%Y%m%d",
|
||||
"%H:%M:%S",
|
||||
"%H:%M",
|
||||
]
|
||||
|
||||
def _parse_datetime(self, value):
|
||||
for format in self._DATETIME_FORMATS:
|
||||
try:
|
||||
return datetime.datetime.strptime(value, format)
|
||||
except ValueError:
|
||||
pass
|
||||
raise Error('Unrecognized date/time format: %r' % value)
|
||||
|
||||
_TIMEDELTA_ABBREVS = [
|
||||
('hours', ['h']),
|
||||
('minutes', ['m', 'min']),
|
||||
('seconds', ['s', 'sec']),
|
||||
('milliseconds', ['ms']),
|
||||
('microseconds', ['us']),
|
||||
('days', ['d']),
|
||||
('weeks', ['w']),
|
||||
]
|
||||
|
||||
_TIMEDELTA_ABBREV_DICT = dict(
|
||||
(abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS
|
||||
for abbrev in abbrevs)
|
||||
|
||||
_FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
|
||||
|
||||
_TIMEDELTA_PATTERN = re.compile(
|
||||
r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE)
|
||||
|
||||
def _parse_timedelta(self, value):
|
||||
try:
|
||||
sum = datetime.timedelta()
|
||||
start = 0
|
||||
while start < len(value):
|
||||
m = self._TIMEDELTA_PATTERN.match(value, start)
|
||||
if not m:
|
||||
raise Exception()
|
||||
num = float(m.group(1))
|
||||
units = m.group(2) or 'seconds'
|
||||
units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
|
||||
sum += datetime.timedelta(**{units: num})
|
||||
start = m.end()
|
||||
return sum
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
def _parse_bool(self, value):
|
||||
return value.lower() not in ("false", "0", "f")
|
||||
|
||||
def _parse_string(self, value):
|
||||
return _unicode(value)
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Exception raised by errors in the options module."""
|
||||
pass
|
||||
|
||||
|
||||
def enable_pretty_logging():
|
||||
"""Turns on formatted logging output as configured.
|
||||
|
||||
This is called automatically by `parse_command_line`.
|
||||
"""
|
||||
root_logger = logging.getLogger()
|
||||
if options.log_file_prefix:
|
||||
channel = logging.handlers.RotatingFileHandler(
|
||||
filename=options.log_file_prefix,
|
||||
maxBytes=options.log_file_max_size,
|
||||
backupCount=options.log_file_num_backups)
|
||||
channel.setFormatter(_LogFormatter(color=False))
|
||||
root_logger.addHandler(channel)
|
||||
|
||||
if (options.log_to_stderr or
|
||||
(options.log_to_stderr is None and not root_logger.handlers)):
|
||||
# Set up color if we are in a tty and curses is installed
|
||||
color = False
|
||||
if curses and sys.stderr.isatty():
|
||||
try:
|
||||
curses.setupterm()
|
||||
if curses.tigetnum("colors") > 0:
|
||||
color = True
|
||||
except Exception:
|
||||
pass
|
||||
channel = logging.StreamHandler()
|
||||
channel.setFormatter(_LogFormatter(color=color))
|
||||
root_logger.addHandler(channel)
|
||||
|
||||
|
||||
|
||||
class _LogFormatter(logging.Formatter):
|
||||
def __init__(self, color, *args, **kwargs):
|
||||
logging.Formatter.__init__(self, *args, **kwargs)
|
||||
self._color = color
|
||||
if color:
|
||||
# The curses module has some str/bytes confusion in
|
||||
# python3. Until version 3.2.3, most methods return
|
||||
# bytes, but only accept strings. In addition, we want to
|
||||
# output these strings with the logging module, which
|
||||
# works with unicode strings. The explicit calls to
|
||||
# unicode() below are harmless in python2 but will do the
|
||||
# right conversion in python 3.
|
||||
fg_color = (curses.tigetstr("setaf") or
|
||||
curses.tigetstr("setf") or "")
|
||||
if (3, 0) < sys.version_info < (3, 2, 3):
|
||||
fg_color = unicode(fg_color, "ascii")
|
||||
self._colors = {
|
||||
logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue
|
||||
"ascii"),
|
||||
logging.INFO: unicode(curses.tparm(fg_color, 2), # Green
|
||||
"ascii"),
|
||||
logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow
|
||||
"ascii"),
|
||||
logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red
|
||||
"ascii"),
|
||||
}
|
||||
self._normal = unicode(curses.tigetstr("sgr0"), "ascii")
|
||||
|
||||
def format(self, record):
|
||||
try:
|
||||
record.message = record.getMessage()
|
||||
except Exception, e:
|
||||
record.message = "Bad message (%r): %r" % (e, record.__dict__)
|
||||
record.asctime = time.strftime(
|
||||
"%y%m%d %H:%M:%S", self.converter(record.created))
|
||||
prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
|
||||
record.__dict__
|
||||
if self._color:
|
||||
prefix = (self._colors.get(record.levelno, self._normal) +
|
||||
prefix + self._normal)
|
||||
formatted = prefix + " " + record.message
|
||||
if record.exc_info:
|
||||
if not record.exc_text:
|
||||
record.exc_text = self.formatException(record.exc_info)
|
||||
if record.exc_text:
|
||||
formatted = formatted.rstrip() + "\n" + record.exc_text
|
||||
return formatted.replace("\n", "\n ")
|
||||
|
||||
|
||||
options = _Options.instance()
|
||||
|
||||
|
||||
# Default options
|
||||
define("help", type=bool, help="show this help information")
|
||||
define("logging", default="info",
|
||||
help=("Set the Python log level. If 'none', tornado won't touch the "
|
||||
"logging configuration."),
|
||||
metavar="debug|info|warning|error|none")
|
||||
define("log_to_stderr", type=bool, default=None,
|
||||
help=("Send log output to stderr (colorized if possible). "
|
||||
"By default use stderr if --log_file_prefix is not set and "
|
||||
"no other logging is configured."))
|
||||
define("log_file_prefix", type=str, default=None, metavar="PATH",
|
||||
help=("Path prefix for log files. "
|
||||
"Note that if you are running multiple tornado processes, "
|
||||
"log_file_prefix must be different for each of them (e.g. "
|
||||
"include the port number)"))
|
||||
define("log_file_max_size", type=int, default=100 * 1000 * 1000,
|
||||
help="max size of log files before rollover")
|
||||
define("log_file_num_backups", type=int, default=10,
|
||||
help="number of log files to keep")
|
||||
0
libs/tornado/platform/__init__.py
Normal file
0
libs/tornado/platform/__init__.py
Normal file
31
libs/tornado/platform/auto.py
Normal file
31
libs/tornado/platform/auto.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Implementation of platform-specific functionality.
|
||||
|
||||
For each function or class described in `tornado.platform.interface`,
|
||||
the appropriate platform-specific implementation exists in this module.
|
||||
Most code that needs access to this functionality should do e.g.::
|
||||
|
||||
from tornado.platform.auto import set_close_exec
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
if os.name == 'nt':
|
||||
from tornado.platform.windows import set_close_exec, Waker
|
||||
else:
|
||||
from tornado.platform.posix import set_close_exec, Waker
|
||||
57
libs/tornado/platform/interface.py
Normal file
57
libs/tornado/platform/interface.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Interfaces for platform-specific functionality.
|
||||
|
||||
This module exists primarily for documentation purposes and as base classes
|
||||
for other tornado.platform modules. Most code should import the appropriate
|
||||
implementation from `tornado.platform.auto`.
|
||||
"""
|
||||
|
||||
def set_close_exec(fd):
|
||||
"""Sets the close-on-exec bit (``FD_CLOEXEC``)for a file descriptor."""
|
||||
raise NotImplementedError()
|
||||
|
||||
class Waker(object):
|
||||
"""A socket-like object that can wake another thread from ``select()``.
|
||||
|
||||
The `~tornado.ioloop.IOLoop` will add the Waker's `fileno()` to
|
||||
its ``select`` (or ``epoll`` or ``kqueue``) calls. When another
|
||||
thread wants to wake up the loop, it calls `wake`. Once it has woken
|
||||
up, it will call `consume` to do any necessary per-wake cleanup. When
|
||||
the ``IOLoop`` is closed, it closes its waker too.
|
||||
"""
|
||||
def fileno(self):
|
||||
"""Returns a file descriptor for this waker.
|
||||
|
||||
Must be suitable for use with ``select()`` or equivalent on the
|
||||
local platform.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def wake(self):
|
||||
"""Triggers activity on the waker's file descriptor."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def consume(self):
|
||||
"""Called after the listen has woken up to do any necessary cleanup."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def close(self):
|
||||
"""Closes the waker's file descriptor(s)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
62
libs/tornado/platform/posix.py
Normal file
62
libs/tornado/platform/posix.py
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Posix implementations of platform-specific functionality."""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
|
||||
from tornado.platform import interface
|
||||
from tornado.util import b
|
||||
|
||||
def set_close_exec(fd):
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
|
||||
def _set_nonblocking(fd):
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
|
||||
class Waker(interface.Waker):
|
||||
def __init__(self):
|
||||
r, w = os.pipe()
|
||||
_set_nonblocking(r)
|
||||
_set_nonblocking(w)
|
||||
set_close_exec(r)
|
||||
set_close_exec(w)
|
||||
self.reader = os.fdopen(r, "rb", 0)
|
||||
self.writer = os.fdopen(w, "wb", 0)
|
||||
|
||||
def fileno(self):
|
||||
return self.reader.fileno()
|
||||
|
||||
def wake(self):
|
||||
try:
|
||||
self.writer.write(b("x"))
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def consume(self):
|
||||
try:
|
||||
while True:
|
||||
result = self.reader.read()
|
||||
if not result: break;
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.reader.close()
|
||||
self.writer.close()
|
||||
330
libs/tornado/platform/twisted.py
Normal file
330
libs/tornado/platform/twisted.py
Normal file
@@ -0,0 +1,330 @@
|
||||
# Author: Ovidiu Predescu
|
||||
# Date: July 2011
|
||||
#
|
||||
# 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.
|
||||
|
||||
# Note: This module's docs are not currently extracted automatically,
|
||||
# so changes must be made manually to twisted.rst
|
||||
# TODO: refactor doc build process to use an appropriate virtualenv
|
||||
"""A Twisted reactor built on the Tornado IOLoop.
|
||||
|
||||
This module lets you run applications and libraries written for
|
||||
Twisted in a Tornado application. To use it, simply call `install` at
|
||||
the beginning of the application::
|
||||
|
||||
import tornado.platform.twisted
|
||||
tornado.platform.twisted.install()
|
||||
from twisted.internet import reactor
|
||||
|
||||
When the app is ready to start, call `IOLoop.instance().start()`
|
||||
instead of `reactor.run()`. This will allow you to use a mixture of
|
||||
Twisted and Tornado code in the same process.
|
||||
|
||||
It is also possible to create a non-global reactor by calling
|
||||
`tornado.platform.twisted.TornadoReactor(io_loop)`. However, if
|
||||
the `IOLoop` and reactor are to be short-lived (such as those used in
|
||||
unit tests), additional cleanup may be required. Specifically, it is
|
||||
recommended to call::
|
||||
|
||||
reactor.fireSystemEvent('shutdown')
|
||||
reactor.disconnectAll()
|
||||
|
||||
before closing the `IOLoop`.
|
||||
|
||||
This module has been tested with Twisted versions 11.0.0 and 11.1.0.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, absolute_import
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import time
|
||||
|
||||
from twisted.internet.posixbase import PosixReactorBase
|
||||
from twisted.internet.interfaces import \
|
||||
IReactorFDSet, IDelayedCall, IReactorTime
|
||||
from twisted.python import failure, log
|
||||
from twisted.internet import error
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
import tornado
|
||||
import tornado.ioloop
|
||||
from tornado.stack_context import NullContext
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
|
||||
class TornadoDelayedCall(object):
|
||||
"""DelayedCall object for Tornado."""
|
||||
implements(IDelayedCall)
|
||||
|
||||
def __init__(self, reactor, seconds, f, *args, **kw):
|
||||
self._reactor = reactor
|
||||
self._func = functools.partial(f, *args, **kw)
|
||||
self._time = self._reactor.seconds() + seconds
|
||||
self._timeout = self._reactor._io_loop.add_timeout(self._time,
|
||||
self._called)
|
||||
self._active = True
|
||||
|
||||
def _called(self):
|
||||
self._active = False
|
||||
self._reactor._removeDelayedCall(self)
|
||||
try:
|
||||
self._func()
|
||||
except:
|
||||
logging.error("_called caught exception", exc_info=True)
|
||||
|
||||
def getTime(self):
|
||||
return self._time
|
||||
|
||||
def cancel(self):
|
||||
self._active = False
|
||||
self._reactor._io_loop.remove_timeout(self._timeout)
|
||||
self._reactor._removeDelayedCall(self)
|
||||
|
||||
def delay(self, seconds):
|
||||
self._reactor._io_loop.remove_timeout(self._timeout)
|
||||
self._time += seconds
|
||||
self._timeout = self._reactor._io_loop.add_timeout(self._time,
|
||||
self._called)
|
||||
|
||||
def reset(self, seconds):
|
||||
self._reactor._io_loop.remove_timeout(self._timeout)
|
||||
self._time = self._reactor.seconds() + seconds
|
||||
self._timeout = self._reactor._io_loop.add_timeout(self._time,
|
||||
self._called)
|
||||
|
||||
def active(self):
|
||||
return self._active
|
||||
|
||||
class TornadoReactor(PosixReactorBase):
|
||||
"""Twisted reactor built on the Tornado IOLoop.
|
||||
|
||||
Since it is intented to be used in applications where the top-level
|
||||
event loop is ``io_loop.start()`` rather than ``reactor.run()``,
|
||||
it is implemented a little differently than other Twisted reactors.
|
||||
We override `mainLoop` instead of `doIteration` and must implement
|
||||
timed call functionality on top of `IOLoop.add_timeout` rather than
|
||||
using the implementation in `PosixReactorBase`.
|
||||
"""
|
||||
implements(IReactorTime, IReactorFDSet)
|
||||
|
||||
def __init__(self, io_loop=None):
|
||||
if not io_loop:
|
||||
io_loop = tornado.ioloop.IOLoop.instance()
|
||||
self._io_loop = io_loop
|
||||
self._readers = {} # map of reader objects to fd
|
||||
self._writers = {} # map of writer objects to fd
|
||||
self._fds = {} # a map of fd to a (reader, writer) tuple
|
||||
self._delayedCalls = {}
|
||||
PosixReactorBase.__init__(self)
|
||||
|
||||
# IOLoop.start() bypasses some of the reactor initialization.
|
||||
# Fire off the necessary events if they weren't already triggered
|
||||
# by reactor.run().
|
||||
def start_if_necessary():
|
||||
if not self._started:
|
||||
self.fireSystemEvent('startup')
|
||||
self._io_loop.add_callback(start_if_necessary)
|
||||
|
||||
# IReactorTime
|
||||
def seconds(self):
|
||||
return time.time()
|
||||
|
||||
def callLater(self, seconds, f, *args, **kw):
|
||||
dc = TornadoDelayedCall(self, seconds, f, *args, **kw)
|
||||
self._delayedCalls[dc] = True
|
||||
return dc
|
||||
|
||||
def getDelayedCalls(self):
|
||||
return [x for x in self._delayedCalls if x._active]
|
||||
|
||||
def _removeDelayedCall(self, dc):
|
||||
if dc in self._delayedCalls:
|
||||
del self._delayedCalls[dc]
|
||||
|
||||
# IReactorThreads
|
||||
def callFromThread(self, f, *args, **kw):
|
||||
"""See `twisted.internet.interfaces.IReactorThreads.callFromThread`"""
|
||||
assert callable(f), "%s is not callable" % f
|
||||
p = functools.partial(f, *args, **kw)
|
||||
self._io_loop.add_callback(p)
|
||||
|
||||
# We don't need the waker code from the super class, Tornado uses
|
||||
# its own waker.
|
||||
def installWaker(self):
|
||||
pass
|
||||
|
||||
def wakeUp(self):
|
||||
pass
|
||||
|
||||
# IReactorFDSet
|
||||
def _invoke_callback(self, fd, events):
|
||||
(reader, writer) = self._fds[fd]
|
||||
if reader:
|
||||
err = None
|
||||
if reader.fileno() == -1:
|
||||
err = error.ConnectionLost()
|
||||
elif events & IOLoop.READ:
|
||||
err = log.callWithLogger(reader, reader.doRead)
|
||||
if err is None and events & IOLoop.ERROR:
|
||||
err = error.ConnectionLost()
|
||||
if err is not None:
|
||||
self.removeReader(reader)
|
||||
reader.readConnectionLost(failure.Failure(err))
|
||||
if writer:
|
||||
err = None
|
||||
if writer.fileno() == -1:
|
||||
err = error.ConnectionLost()
|
||||
elif events & IOLoop.WRITE:
|
||||
err = log.callWithLogger(writer, writer.doWrite)
|
||||
if err is None and events & IOLoop.ERROR:
|
||||
err = error.ConnectionLost()
|
||||
if err is not None:
|
||||
self.removeWriter(writer)
|
||||
writer.writeConnectionLost(failure.Failure(err))
|
||||
|
||||
def addReader(self, reader):
|
||||
"""Add a FileDescriptor for notification of data available to read."""
|
||||
if reader in self._readers:
|
||||
# Don't add the reader if it's already there
|
||||
return
|
||||
fd = reader.fileno()
|
||||
self._readers[reader] = fd
|
||||
if fd in self._fds:
|
||||
(_, writer) = self._fds[fd]
|
||||
self._fds[fd] = (reader, writer)
|
||||
if writer:
|
||||
# We already registered this fd for write events,
|
||||
# update it for read events as well.
|
||||
self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE)
|
||||
else:
|
||||
with NullContext():
|
||||
self._fds[fd] = (reader, None)
|
||||
self._io_loop.add_handler(fd, self._invoke_callback,
|
||||
IOLoop.READ)
|
||||
|
||||
def addWriter(self, writer):
|
||||
"""Add a FileDescriptor for notification of data available to write."""
|
||||
if writer in self._writers:
|
||||
return
|
||||
fd = writer.fileno()
|
||||
self._writers[writer] = fd
|
||||
if fd in self._fds:
|
||||
(reader, _) = self._fds[fd]
|
||||
self._fds[fd] = (reader, writer)
|
||||
if reader:
|
||||
# We already registered this fd for read events,
|
||||
# update it for write events as well.
|
||||
self._io_loop.update_handler(fd, IOLoop.READ | IOLoop.WRITE)
|
||||
else:
|
||||
with NullContext():
|
||||
self._fds[fd] = (None, writer)
|
||||
self._io_loop.add_handler(fd, self._invoke_callback,
|
||||
IOLoop.WRITE)
|
||||
|
||||
def removeReader(self, reader):
|
||||
"""Remove a Selectable for notification of data available to read."""
|
||||
if reader in self._readers:
|
||||
fd = self._readers.pop(reader)
|
||||
(_, writer) = self._fds[fd]
|
||||
if writer:
|
||||
# We have a writer so we need to update the IOLoop for
|
||||
# write events only.
|
||||
self._fds[fd] = (None, writer)
|
||||
self._io_loop.update_handler(fd, IOLoop.WRITE)
|
||||
else:
|
||||
# Since we have no writer registered, we remove the
|
||||
# entry from _fds and unregister the handler from the
|
||||
# IOLoop
|
||||
del self._fds[fd]
|
||||
self._io_loop.remove_handler(fd)
|
||||
|
||||
def removeWriter(self, writer):
|
||||
"""Remove a Selectable for notification of data available to write."""
|
||||
if writer in self._writers:
|
||||
fd = self._writers.pop(writer)
|
||||
(reader, _) = self._fds[fd]
|
||||
if reader:
|
||||
# We have a reader so we need to update the IOLoop for
|
||||
# read events only.
|
||||
self._fds[fd] = (reader, None)
|
||||
self._io_loop.update_handler(fd, IOLoop.READ)
|
||||
else:
|
||||
# Since we have no reader registered, we remove the
|
||||
# entry from the _fds and unregister the handler from
|
||||
# the IOLoop.
|
||||
del self._fds[fd]
|
||||
self._io_loop.remove_handler(fd)
|
||||
|
||||
def removeAll(self):
|
||||
return self._removeAll(self._readers, self._writers)
|
||||
|
||||
def getReaders(self):
|
||||
return self._readers.keys()
|
||||
|
||||
def getWriters(self):
|
||||
return self._writers.keys()
|
||||
|
||||
# The following functions are mainly used in twisted-style test cases;
|
||||
# it is expected that most users of the TornadoReactor will call
|
||||
# IOLoop.start() instead of Reactor.run().
|
||||
def stop(self):
|
||||
PosixReactorBase.stop(self)
|
||||
self._io_loop.stop()
|
||||
|
||||
def crash(self):
|
||||
PosixReactorBase.crash(self)
|
||||
self._io_loop.stop()
|
||||
|
||||
def doIteration(self, delay):
|
||||
raise NotImplementedError("doIteration")
|
||||
|
||||
def mainLoop(self):
|
||||
self._io_loop.start()
|
||||
if self._stopped:
|
||||
self.fireSystemEvent("shutdown")
|
||||
|
||||
class _TestReactor(TornadoReactor):
|
||||
"""Subclass of TornadoReactor for use in unittests.
|
||||
|
||||
This can't go in the test.py file because of import-order dependencies
|
||||
with the Twisted reactor test builder.
|
||||
"""
|
||||
def __init__(self):
|
||||
# always use a new ioloop
|
||||
super(_TestReactor, self).__init__(IOLoop())
|
||||
|
||||
def listenTCP(self, port, factory, backlog=50, interface=''):
|
||||
# default to localhost to avoid firewall prompts on the mac
|
||||
if not interface:
|
||||
interface = '127.0.0.1'
|
||||
return super(_TestReactor, self).listenTCP(
|
||||
port, factory, backlog=backlog, interface=interface)
|
||||
|
||||
def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
|
||||
if not interface:
|
||||
interface = '127.0.0.1'
|
||||
return super(_TestReactor, self).listenUDP(
|
||||
port, protocol, interface=interface, maxPacketSize=maxPacketSize)
|
||||
|
||||
|
||||
|
||||
def install(io_loop=None):
|
||||
"""Install this package as the default Twisted reactor."""
|
||||
if not io_loop:
|
||||
io_loop = tornado.ioloop.IOLoop.instance()
|
||||
reactor = TornadoReactor(io_loop)
|
||||
from twisted.internet.main import installReactor
|
||||
installReactor(reactor)
|
||||
return reactor
|
||||
97
libs/tornado/platform/windows.py
Normal file
97
libs/tornado/platform/windows.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# NOTE: win32 support is currently experimental, and not recommended
|
||||
# for production use.
|
||||
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import socket
|
||||
import errno
|
||||
|
||||
from tornado.platform import interface
|
||||
from tornado.util import b
|
||||
|
||||
# See: http://msdn.microsoft.com/en-us/library/ms724935(VS.85).aspx
|
||||
SetHandleInformation = ctypes.windll.kernel32.SetHandleInformation
|
||||
SetHandleInformation.argtypes = (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)
|
||||
SetHandleInformation.restype = ctypes.wintypes.BOOL
|
||||
|
||||
HANDLE_FLAG_INHERIT = 0x00000001
|
||||
|
||||
|
||||
def set_close_exec(fd):
|
||||
success = SetHandleInformation(fd, HANDLE_FLAG_INHERIT, 0)
|
||||
if not success:
|
||||
raise ctypes.GetLastError()
|
||||
|
||||
|
||||
class Waker(interface.Waker):
|
||||
"""Create an OS independent asynchronous pipe"""
|
||||
def __init__(self):
|
||||
# Based on Zope async.py: http://svn.zope.org/zc.ngi/trunk/src/zc/ngi/async.py
|
||||
|
||||
self.writer = socket.socket()
|
||||
# Disable buffering -- pulling the trigger sends 1 byte,
|
||||
# and we want that sent immediately, to wake up ASAP.
|
||||
self.writer.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
count = 0
|
||||
while 1:
|
||||
count += 1
|
||||
# Bind to a local port; for efficiency, let the OS pick
|
||||
# a free port for us.
|
||||
# Unfortunately, stress tests showed that we may not
|
||||
# be able to connect to that port ("Address already in
|
||||
# use") despite that the OS picked it. This appears
|
||||
# to be a race bug in the Windows socket implementation.
|
||||
# So we loop until a connect() succeeds (almost always
|
||||
# on the first try). See the long thread at
|
||||
# http://mail.zope.org/pipermail/zope/2005-July/160433.html
|
||||
# for hideous details.
|
||||
a = socket.socket()
|
||||
a.bind(("127.0.0.1", 0))
|
||||
connect_address = a.getsockname() # assigned (host, port) pair
|
||||
a.listen(1)
|
||||
try:
|
||||
self.writer.connect(connect_address)
|
||||
break # success
|
||||
except socket.error, detail:
|
||||
if detail[0] != errno.WSAEADDRINUSE:
|
||||
# "Address already in use" is the only error
|
||||
# I've seen on two WinXP Pro SP2 boxes, under
|
||||
# Pythons 2.3.5 and 2.4.1.
|
||||
raise
|
||||
# (10048, 'Address already in use')
|
||||
# assert count <= 2 # never triggered in Tim's tests
|
||||
if count >= 10: # I've never seen it go above 2
|
||||
a.close()
|
||||
self.writer.close()
|
||||
raise socket.error("Cannot bind trigger!")
|
||||
# Close `a` and try again. Note: I originally put a short
|
||||
# sleep() here, but it didn't appear to help or hurt.
|
||||
a.close()
|
||||
|
||||
self.reader, addr = a.accept()
|
||||
self.reader.setblocking(0)
|
||||
self.writer.setblocking(0)
|
||||
a.close()
|
||||
self.reader_fd = self.reader.fileno()
|
||||
|
||||
def fileno(self):
|
||||
return self.reader.fileno()
|
||||
|
||||
def wake(self):
|
||||
try:
|
||||
self.writer.send(b("x"))
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def consume(self):
|
||||
try:
|
||||
while True:
|
||||
result = self.reader.recv(1024)
|
||||
if not result: break
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.reader.close()
|
||||
self.writer.close()
|
||||
149
libs/tornado/process.py
Normal file
149
libs/tornado/process.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2011 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.
|
||||
|
||||
"""Utilities for working with multiple processes."""
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from binascii import hexlify
|
||||
|
||||
from tornado import ioloop
|
||||
|
||||
try:
|
||||
import multiprocessing # Python 2.6+
|
||||
except ImportError:
|
||||
multiprocessing = None
|
||||
|
||||
def cpu_count():
|
||||
"""Returns the number of processors on this machine."""
|
||||
if multiprocessing is not None:
|
||||
try:
|
||||
return multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
pass
|
||||
try:
|
||||
return os.sysconf("SC_NPROCESSORS_CONF")
|
||||
except ValueError:
|
||||
pass
|
||||
logging.error("Could not detect number of processors; assuming 1")
|
||||
return 1
|
||||
|
||||
def _reseed_random():
|
||||
if 'random' not in sys.modules:
|
||||
return
|
||||
import random
|
||||
# If os.urandom is available, this method does the same thing as
|
||||
# random.seed (at least as of python 2.6). If os.urandom is not
|
||||
# available, we mix in the pid in addition to a timestamp.
|
||||
try:
|
||||
seed = long(hexlify(os.urandom(16)), 16)
|
||||
except NotImplementedError:
|
||||
seed = int(time.time() * 1000) ^ os.getpid()
|
||||
random.seed(seed)
|
||||
|
||||
|
||||
_task_id = None
|
||||
|
||||
def fork_processes(num_processes, max_restarts=100):
|
||||
"""Starts multiple worker processes.
|
||||
|
||||
If ``num_processes`` is None or <= 0, we detect the number of cores
|
||||
available on this machine and fork that number of child
|
||||
processes. If ``num_processes`` is given and > 0, we fork that
|
||||
specific number of sub-processes.
|
||||
|
||||
Since we use processes and not threads, there is no shared memory
|
||||
between any server code.
|
||||
|
||||
Note that multiple processes are not compatible with the autoreload
|
||||
module (or the debug=True option to `tornado.web.Application`).
|
||||
When using multiple processes, no IOLoops can be created or
|
||||
referenced until after the call to ``fork_processes``.
|
||||
|
||||
In each child process, ``fork_processes`` returns its *task id*, a
|
||||
number between 0 and ``num_processes``. Processes that exit
|
||||
abnormally (due to a signal or non-zero exit status) are restarted
|
||||
with the same id (up to ``max_restarts`` times). In the parent
|
||||
process, ``fork_processes`` returns None if all child processes
|
||||
have exited normally, but will otherwise only exit by throwing an
|
||||
exception.
|
||||
"""
|
||||
global _task_id
|
||||
assert _task_id is None
|
||||
if num_processes is None or num_processes <= 0:
|
||||
num_processes = cpu_count()
|
||||
if ioloop.IOLoop.initialized():
|
||||
raise RuntimeError("Cannot run in multiple processes: IOLoop instance "
|
||||
"has already been initialized. You cannot call "
|
||||
"IOLoop.instance() before calling start_processes()")
|
||||
logging.info("Starting %d processes", num_processes)
|
||||
children = {}
|
||||
def start_child(i):
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# child process
|
||||
_reseed_random()
|
||||
global _task_id
|
||||
_task_id = i
|
||||
return i
|
||||
else:
|
||||
children[pid] = i
|
||||
return None
|
||||
for i in range(num_processes):
|
||||
id = start_child(i)
|
||||
if id is not None: return id
|
||||
num_restarts = 0
|
||||
while children:
|
||||
try:
|
||||
pid, status = os.wait()
|
||||
except OSError, e:
|
||||
if e.errno == errno.EINTR:
|
||||
continue
|
||||
raise
|
||||
if pid not in children:
|
||||
continue
|
||||
id = children.pop(pid)
|
||||
if os.WIFSIGNALED(status):
|
||||
logging.warning("child %d (pid %d) killed by signal %d, restarting",
|
||||
id, pid, os.WTERMSIG(status))
|
||||
elif os.WEXITSTATUS(status) != 0:
|
||||
logging.warning("child %d (pid %d) exited with status %d, restarting",
|
||||
id, pid, os.WEXITSTATUS(status))
|
||||
else:
|
||||
logging.info("child %d (pid %d) exited normally", id, pid)
|
||||
continue
|
||||
num_restarts += 1
|
||||
if num_restarts > max_restarts:
|
||||
raise RuntimeError("Too many child restarts, giving up")
|
||||
new_id = start_child(id)
|
||||
if new_id is not None: return new_id
|
||||
# All child processes exited cleanly, so exit the master process
|
||||
# instead of just returning to right after the call to
|
||||
# fork_processes (which will probably just start up another IOLoop
|
||||
# unless the caller checks the return value).
|
||||
sys.exit(0)
|
||||
|
||||
def task_id():
|
||||
"""Returns the current task id, if any.
|
||||
|
||||
Returns None if this process was not created by `fork_processes`.
|
||||
"""
|
||||
global _task_id
|
||||
return _task_id
|
||||
509
libs/tornado/simple_httpclient.py
Normal file
509
libs/tornado/simple_httpclient.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import with_statement
|
||||
|
||||
from tornado.escape import utf8, _unicode, native_str
|
||||
from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main
|
||||
from tornado.httputil import HTTPHeaders
|
||||
from tornado.iostream import IOStream, SSLIOStream
|
||||
from tornado import stack_context
|
||||
from tornado.util import b
|
||||
|
||||
import base64
|
||||
import collections
|
||||
import contextlib
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urlparse
|
||||
import zlib
|
||||
|
||||
try:
|
||||
from io import BytesIO # python 3
|
||||
except ImportError:
|
||||
from cStringIO import StringIO as BytesIO # python 2
|
||||
|
||||
try:
|
||||
import ssl # python 2.6+
|
||||
except ImportError:
|
||||
ssl = None
|
||||
|
||||
_DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt'
|
||||
|
||||
class SimpleAsyncHTTPClient(AsyncHTTPClient):
|
||||
"""Non-blocking HTTP client with no external dependencies.
|
||||
|
||||
This class implements an HTTP 1.1 client on top of Tornado's IOStreams.
|
||||
It does not currently implement all applicable parts of the HTTP
|
||||
specification, but it does enough to work with major web service APIs
|
||||
(mostly tested against the Twitter API so far).
|
||||
|
||||
This class has not been tested extensively in production and
|
||||
should be considered somewhat experimental as of the release of
|
||||
tornado 1.2. It is intended to become the default AsyncHTTPClient
|
||||
implementation in a future release. It may either be used
|
||||
directly, or to facilitate testing of this class with an existing
|
||||
application, setting the environment variable
|
||||
USE_SIMPLE_HTTPCLIENT=1 will cause this class to transparently
|
||||
replace tornado.httpclient.AsyncHTTPClient.
|
||||
|
||||
Some features found in the curl-based AsyncHTTPClient are not yet
|
||||
supported. In particular, proxies are not supported, connections
|
||||
are not reused, and callers cannot select the network interface to be
|
||||
used.
|
||||
|
||||
Python 2.6 or higher is required for HTTPS support. Users of Python 2.5
|
||||
should use the curl-based AsyncHTTPClient if HTTPS support is required.
|
||||
|
||||
"""
|
||||
def initialize(self, io_loop=None, max_clients=10,
|
||||
max_simultaneous_connections=None,
|
||||
hostname_mapping=None, max_buffer_size=104857600):
|
||||
"""Creates a AsyncHTTPClient.
|
||||
|
||||
Only a single AsyncHTTPClient instance exists per IOLoop
|
||||
in order to provide limitations on the number of pending connections.
|
||||
force_instance=True may be used to suppress this behavior.
|
||||
|
||||
max_clients is the number of concurrent requests that can be in
|
||||
progress. max_simultaneous_connections has no effect and is accepted
|
||||
only for compatibility with the curl-based AsyncHTTPClient. Note
|
||||
that these arguments are only used when the client is first created,
|
||||
and will be ignored when an existing client is reused.
|
||||
|
||||
hostname_mapping is a dictionary mapping hostnames to IP addresses.
|
||||
It can be used to make local DNS changes when modifying system-wide
|
||||
settings like /etc/hosts is not possible or desirable (e.g. in
|
||||
unittests).
|
||||
|
||||
max_buffer_size is the number of bytes that can be read by IOStream. It
|
||||
defaults to 100mb.
|
||||
"""
|
||||
self.io_loop = io_loop
|
||||
self.max_clients = max_clients
|
||||
self.queue = collections.deque()
|
||||
self.active = {}
|
||||
self.hostname_mapping = hostname_mapping
|
||||
self.max_buffer_size = max_buffer_size
|
||||
|
||||
def fetch(self, request, callback, **kwargs):
|
||||
if not isinstance(request, HTTPRequest):
|
||||
request = HTTPRequest(url=request, **kwargs)
|
||||
if not isinstance(request.headers, HTTPHeaders):
|
||||
request.headers = HTTPHeaders(request.headers)
|
||||
callback = stack_context.wrap(callback)
|
||||
self.queue.append((request, callback))
|
||||
self._process_queue()
|
||||
if self.queue:
|
||||
logging.debug("max_clients limit reached, request queued. "
|
||||
"%d active, %d queued requests." % (
|
||||
len(self.active), len(self.queue)))
|
||||
|
||||
def _process_queue(self):
|
||||
with stack_context.NullContext():
|
||||
while self.queue and len(self.active) < self.max_clients:
|
||||
request, callback = self.queue.popleft()
|
||||
key = object()
|
||||
self.active[key] = (request, callback)
|
||||
_HTTPConnection(self.io_loop, self, request,
|
||||
functools.partial(self._release_fetch, key),
|
||||
callback,
|
||||
self.max_buffer_size)
|
||||
|
||||
def _release_fetch(self, key):
|
||||
del self.active[key]
|
||||
self._process_queue()
|
||||
|
||||
|
||||
|
||||
class _HTTPConnection(object):
|
||||
_SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE"])
|
||||
|
||||
def __init__(self, io_loop, client, request, release_callback,
|
||||
final_callback, max_buffer_size):
|
||||
self.start_time = time.time()
|
||||
self.io_loop = io_loop
|
||||
self.client = client
|
||||
self.request = request
|
||||
self.release_callback = release_callback
|
||||
self.final_callback = final_callback
|
||||
self.code = None
|
||||
self.headers = None
|
||||
self.chunks = None
|
||||
self._decompressor = None
|
||||
# Timeout handle returned by IOLoop.add_timeout
|
||||
self._timeout = None
|
||||
with stack_context.StackContext(self.cleanup):
|
||||
parsed = urlparse.urlsplit(_unicode(self.request.url))
|
||||
if ssl is None and parsed.scheme == "https":
|
||||
raise ValueError("HTTPS requires either python2.6+ or "
|
||||
"curl_httpclient")
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise ValueError("Unsupported url scheme: %s" %
|
||||
self.request.url)
|
||||
# urlsplit results have hostname and port results, but they
|
||||
# didn't support ipv6 literals until python 2.7.
|
||||
netloc = parsed.netloc
|
||||
if "@" in netloc:
|
||||
userpass, _, netloc = netloc.rpartition("@")
|
||||
match = re.match(r'^(.+):(\d+)$', netloc)
|
||||
if match:
|
||||
host = match.group(1)
|
||||
port = int(match.group(2))
|
||||
else:
|
||||
host = netloc
|
||||
port = 443 if parsed.scheme == "https" else 80
|
||||
if re.match(r'^\[.*\]$', host):
|
||||
# raw ipv6 addresses in urls are enclosed in brackets
|
||||
host = host[1:-1]
|
||||
if self.client.hostname_mapping is not None:
|
||||
host = self.client.hostname_mapping.get(host, host)
|
||||
|
||||
if request.allow_ipv6:
|
||||
af = socket.AF_UNSPEC
|
||||
else:
|
||||
# We only try the first IP we get from getaddrinfo,
|
||||
# so restrict to ipv4 by default.
|
||||
af = socket.AF_INET
|
||||
|
||||
addrinfo = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM,
|
||||
0, 0)
|
||||
af, socktype, proto, canonname, sockaddr = addrinfo[0]
|
||||
|
||||
if parsed.scheme == "https":
|
||||
ssl_options = {}
|
||||
if request.validate_cert:
|
||||
ssl_options["cert_reqs"] = ssl.CERT_REQUIRED
|
||||
if request.ca_certs is not None:
|
||||
ssl_options["ca_certs"] = request.ca_certs
|
||||
else:
|
||||
ssl_options["ca_certs"] = _DEFAULT_CA_CERTS
|
||||
if request.client_key is not None:
|
||||
ssl_options["keyfile"] = request.client_key
|
||||
if request.client_cert is not None:
|
||||
ssl_options["certfile"] = request.client_cert
|
||||
|
||||
# SSL interoperability is tricky. We want to disable
|
||||
# SSLv2 for security reasons; it wasn't disabled by default
|
||||
# until openssl 1.0. The best way to do this is to use
|
||||
# the SSL_OP_NO_SSLv2, but that wasn't exposed to python
|
||||
# until 3.2. Python 2.7 adds the ciphers argument, which
|
||||
# can also be used to disable SSLv2. As a last resort
|
||||
# on python 2.6, we set ssl_version to SSLv3. This is
|
||||
# more narrow than we'd like since it also breaks
|
||||
# compatibility with servers configured for TLSv1 only,
|
||||
# but nearly all servers support SSLv3:
|
||||
# http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html
|
||||
if sys.version_info >= (2,7):
|
||||
ssl_options["ciphers"] = "DEFAULT:!SSLv2"
|
||||
else:
|
||||
# This is really only necessary for pre-1.0 versions
|
||||
# of openssl, but python 2.6 doesn't expose version
|
||||
# information.
|
||||
ssl_options["ssl_version"] = ssl.PROTOCOL_SSLv3
|
||||
|
||||
self.stream = SSLIOStream(socket.socket(af, socktype, proto),
|
||||
io_loop=self.io_loop,
|
||||
ssl_options=ssl_options,
|
||||
max_buffer_size=max_buffer_size)
|
||||
else:
|
||||
self.stream = IOStream(socket.socket(af, socktype, proto),
|
||||
io_loop=self.io_loop,
|
||||
max_buffer_size=max_buffer_size)
|
||||
timeout = min(request.connect_timeout, request.request_timeout)
|
||||
if timeout:
|
||||
self._timeout = self.io_loop.add_timeout(
|
||||
self.start_time + timeout,
|
||||
self._on_timeout)
|
||||
self.stream.set_close_callback(self._on_close)
|
||||
self.stream.connect(sockaddr,
|
||||
functools.partial(self._on_connect, parsed))
|
||||
|
||||
def _on_timeout(self):
|
||||
self._timeout = None
|
||||
self._run_callback(HTTPResponse(self.request, 599,
|
||||
request_time=time.time() - self.start_time,
|
||||
error=HTTPError(599, "Timeout")))
|
||||
self.stream.close()
|
||||
|
||||
def _on_connect(self, parsed):
|
||||
if self._timeout is not None:
|
||||
self.io_loop.remove_timeout(self._timeout)
|
||||
self._timeout = None
|
||||
if self.request.request_timeout:
|
||||
self._timeout = self.io_loop.add_timeout(
|
||||
self.start_time + self.request.request_timeout,
|
||||
self._on_timeout)
|
||||
if (self.request.validate_cert and
|
||||
isinstance(self.stream, SSLIOStream)):
|
||||
match_hostname(self.stream.socket.getpeercert(),
|
||||
parsed.hostname)
|
||||
if (self.request.method not in self._SUPPORTED_METHODS and
|
||||
not self.request.allow_nonstandard_methods):
|
||||
raise KeyError("unknown method %s" % self.request.method)
|
||||
for key in ('network_interface',
|
||||
'proxy_host', 'proxy_port',
|
||||
'proxy_username', 'proxy_password'):
|
||||
if getattr(self.request, key, None):
|
||||
raise NotImplementedError('%s not supported' % key)
|
||||
if "Host" not in self.request.headers:
|
||||
self.request.headers["Host"] = parsed.netloc
|
||||
username, password = None, None
|
||||
if parsed.username is not None:
|
||||
username, password = parsed.username, parsed.password
|
||||
elif self.request.auth_username is not None:
|
||||
username = self.request.auth_username
|
||||
password = self.request.auth_password or ''
|
||||
if username is not None:
|
||||
auth = utf8(username) + b(":") + utf8(password)
|
||||
self.request.headers["Authorization"] = (b("Basic ") +
|
||||
base64.b64encode(auth))
|
||||
if self.request.user_agent:
|
||||
self.request.headers["User-Agent"] = self.request.user_agent
|
||||
if not self.request.allow_nonstandard_methods:
|
||||
if self.request.method in ("POST", "PUT"):
|
||||
assert self.request.body is not None
|
||||
else:
|
||||
assert self.request.body is None
|
||||
if self.request.body is not None:
|
||||
self.request.headers["Content-Length"] = str(len(
|
||||
self.request.body))
|
||||
if (self.request.method == "POST" and
|
||||
"Content-Type" not in self.request.headers):
|
||||
self.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
if self.request.use_gzip:
|
||||
self.request.headers["Accept-Encoding"] = "gzip"
|
||||
req_path = ((parsed.path or '/') +
|
||||
(('?' + parsed.query) if parsed.query else ''))
|
||||
request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method,
|
||||
req_path))]
|
||||
for k, v in self.request.headers.get_all():
|
||||
line = utf8(k) + b(": ") + utf8(v)
|
||||
if b('\n') in line:
|
||||
raise ValueError('Newline in header: ' + repr(line))
|
||||
request_lines.append(line)
|
||||
self.stream.write(b("\r\n").join(request_lines) + b("\r\n\r\n"))
|
||||
if self.request.body is not None:
|
||||
self.stream.write(self.request.body)
|
||||
self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers)
|
||||
|
||||
def _release(self):
|
||||
if self.release_callback is not None:
|
||||
release_callback = self.release_callback
|
||||
self.release_callback = None
|
||||
release_callback()
|
||||
|
||||
def _run_callback(self, response):
|
||||
self._release()
|
||||
if self.final_callback is not None:
|
||||
final_callback = self.final_callback
|
||||
self.final_callback = None
|
||||
final_callback(response)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def cleanup(self):
|
||||
try:
|
||||
yield
|
||||
except Exception, e:
|
||||
logging.warning("uncaught exception", exc_info=True)
|
||||
self._run_callback(HTTPResponse(self.request, 599, error=e,
|
||||
request_time=time.time() - self.start_time,
|
||||
))
|
||||
|
||||
def _on_close(self):
|
||||
self._run_callback(HTTPResponse(
|
||||
self.request, 599,
|
||||
request_time=time.time() - self.start_time,
|
||||
error=HTTPError(599, "Connection closed")))
|
||||
|
||||
def _on_headers(self, data):
|
||||
data = native_str(data.decode("latin1"))
|
||||
first_line, _, header_data = data.partition("\n")
|
||||
match = re.match("HTTP/1.[01] ([0-9]+)", first_line)
|
||||
assert match
|
||||
self.code = int(match.group(1))
|
||||
self.headers = HTTPHeaders.parse(header_data)
|
||||
|
||||
if "Content-Length" in self.headers:
|
||||
if "," in self.headers["Content-Length"]:
|
||||
# Proxies sometimes cause Content-Length headers to get
|
||||
# duplicated. If all the values are identical then we can
|
||||
# use them but if they differ it's an error.
|
||||
pieces = re.split(r',\s*', self.headers["Content-Length"])
|
||||
if any(i != pieces[0] for i in pieces):
|
||||
raise ValueError("Multiple unequal Content-Lengths: %r" %
|
||||
self.headers["Content-Length"])
|
||||
self.headers["Content-Length"] = pieces[0]
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
else:
|
||||
content_length = None
|
||||
|
||||
if self.request.header_callback is not None:
|
||||
for k, v in self.headers.get_all():
|
||||
self.request.header_callback("%s: %s\r\n" % (k, v))
|
||||
|
||||
if self.request.method == "HEAD":
|
||||
# HEAD requests never have content, even though they may have
|
||||
# content-length headers
|
||||
self._on_body(b(""))
|
||||
return
|
||||
if 100 <= self.code < 200 or self.code in (204, 304):
|
||||
# These response codes never have bodies
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
|
||||
assert "Transfer-Encoding" not in self.headers
|
||||
assert content_length in (None, 0)
|
||||
self._on_body(b(""))
|
||||
return
|
||||
|
||||
if (self.request.use_gzip and
|
||||
self.headers.get("Content-Encoding") == "gzip"):
|
||||
# Magic parameter makes zlib module understand gzip header
|
||||
# http://stackoverflow.com/questions/1838699/how-can-i-decompress-a-gzip-stream-with-zlib
|
||||
self._decompressor = zlib.decompressobj(16+zlib.MAX_WBITS)
|
||||
if self.headers.get("Transfer-Encoding") == "chunked":
|
||||
self.chunks = []
|
||||
self.stream.read_until(b("\r\n"), self._on_chunk_length)
|
||||
elif content_length is not None:
|
||||
self.stream.read_bytes(content_length, self._on_body)
|
||||
else:
|
||||
self.stream.read_until_close(self._on_body)
|
||||
|
||||
def _on_body(self, data):
|
||||
if self._timeout is not None:
|
||||
self.io_loop.remove_timeout(self._timeout)
|
||||
self._timeout = None
|
||||
original_request = getattr(self.request, "original_request",
|
||||
self.request)
|
||||
if (self.request.follow_redirects and
|
||||
self.request.max_redirects > 0 and
|
||||
self.code in (301, 302, 303, 307)):
|
||||
new_request = copy.copy(self.request)
|
||||
new_request.url = urlparse.urljoin(self.request.url,
|
||||
self.headers["Location"])
|
||||
new_request.max_redirects -= 1
|
||||
del new_request.headers["Host"]
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
|
||||
# client SHOULD make a GET request
|
||||
if self.code == 303:
|
||||
new_request.method = "GET"
|
||||
new_request.body = None
|
||||
for h in ["Content-Length", "Content-Type",
|
||||
"Content-Encoding", "Transfer-Encoding"]:
|
||||
try:
|
||||
del self.request.headers[h]
|
||||
except KeyError:
|
||||
pass
|
||||
new_request.original_request = original_request
|
||||
final_callback = self.final_callback
|
||||
self.final_callback = None
|
||||
self._release()
|
||||
self.client.fetch(new_request, final_callback)
|
||||
self.stream.close()
|
||||
return
|
||||
if self._decompressor:
|
||||
data = self._decompressor.decompress(data)
|
||||
if self.request.streaming_callback:
|
||||
if self.chunks is None:
|
||||
# if chunks is not None, we already called streaming_callback
|
||||
# in _on_chunk_data
|
||||
self.request.streaming_callback(data)
|
||||
buffer = BytesIO()
|
||||
else:
|
||||
buffer = BytesIO(data) # TODO: don't require one big string?
|
||||
response = HTTPResponse(original_request,
|
||||
self.code, headers=self.headers,
|
||||
request_time=time.time() - self.start_time,
|
||||
buffer=buffer,
|
||||
effective_url=self.request.url)
|
||||
self._run_callback(response)
|
||||
self.stream.close()
|
||||
|
||||
def _on_chunk_length(self, data):
|
||||
# TODO: "chunk extensions" http://tools.ietf.org/html/rfc2616#section-3.6.1
|
||||
length = int(data.strip(), 16)
|
||||
if length == 0:
|
||||
# all the data has been decompressed, so we don't need to
|
||||
# decompress again in _on_body
|
||||
self._decompressor = None
|
||||
self._on_body(b('').join(self.chunks))
|
||||
else:
|
||||
self.stream.read_bytes(length + 2, # chunk ends with \r\n
|
||||
self._on_chunk_data)
|
||||
|
||||
def _on_chunk_data(self, data):
|
||||
assert data[-2:] == b("\r\n")
|
||||
chunk = data[:-2]
|
||||
if self._decompressor:
|
||||
chunk = self._decompressor.decompress(chunk)
|
||||
if self.request.streaming_callback is not None:
|
||||
self.request.streaming_callback(chunk)
|
||||
else:
|
||||
self.chunks.append(chunk)
|
||||
self.stream.read_until(b("\r\n"), self._on_chunk_length)
|
||||
|
||||
|
||||
# match_hostname was added to the standard library ssl module in python 3.2.
|
||||
# The following code was backported for older releases and copied from
|
||||
# https://bitbucket.org/brandon/backports.ssl_match_hostname
|
||||
class CertificateError(ValueError):
|
||||
pass
|
||||
|
||||
def _dnsname_to_pat(dn):
|
||||
pats = []
|
||||
for frag in dn.split(r'.'):
|
||||
if frag == '*':
|
||||
# When '*' is a fragment by itself, it matches a non-empty dotless
|
||||
# fragment.
|
||||
pats.append('[^.]+')
|
||||
else:
|
||||
# Otherwise, '*' matches any dotless fragment.
|
||||
frag = re.escape(frag)
|
||||
pats.append(frag.replace(r'\*', '[^.]*'))
|
||||
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
||||
|
||||
def match_hostname(cert, hostname):
|
||||
"""Verify that *cert* (in decoded format as returned by
|
||||
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
|
||||
are mostly followed, but IP addresses are not accepted for *hostname*.
|
||||
|
||||
CertificateError is raised on failure. On success, the function
|
||||
returns nothing.
|
||||
"""
|
||||
if not cert:
|
||||
raise ValueError("empty or no certificate")
|
||||
dnsnames = []
|
||||
san = cert.get('subjectAltName', ())
|
||||
for key, value in san:
|
||||
if key == 'DNS':
|
||||
if _dnsname_to_pat(value).match(hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if not san:
|
||||
# The subject is only checked when subjectAltName is empty
|
||||
for sub in cert.get('subject', ()):
|
||||
for key, value in sub:
|
||||
# XXX according to RFC 2818, the most specific Common Name
|
||||
# must be used.
|
||||
if key == 'commonName':
|
||||
if _dnsname_to_pat(value).match(hostname):
|
||||
return
|
||||
dnsnames.append(value)
|
||||
if len(dnsnames) > 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match either of %s"
|
||||
% (hostname, ', '.join(map(repr, dnsnames))))
|
||||
elif len(dnsnames) == 1:
|
||||
raise CertificateError("hostname %r "
|
||||
"doesn't match %r"
|
||||
% (hostname, dnsnames[0]))
|
||||
else:
|
||||
raise CertificateError("no appropriate commonName or "
|
||||
"subjectAltName fields were found")
|
||||
|
||||
if __name__ == "__main__":
|
||||
AsyncHTTPClient.configure(SimpleAsyncHTTPClient)
|
||||
main()
|
||||
244
libs/tornado/stack_context.py
Normal file
244
libs/tornado/stack_context.py
Normal file
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2010 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.
|
||||
|
||||
'''StackContext allows applications to maintain threadlocal-like state
|
||||
that follows execution as it moves to other execution contexts.
|
||||
|
||||
The motivating examples are to eliminate the need for explicit
|
||||
async_callback wrappers (as in tornado.web.RequestHandler), and to
|
||||
allow some additional context to be kept for logging.
|
||||
|
||||
This is slightly magic, but it's an extension of the idea that an exception
|
||||
handler is a kind of stack-local state and when that stack is suspended
|
||||
and resumed in a new context that state needs to be preserved. StackContext
|
||||
shifts the burden of restoring that state from each call site (e.g.
|
||||
wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms
|
||||
that transfer control from one context to another (e.g. AsyncHTTPClient
|
||||
itself, IOLoop, thread pools, etc).
|
||||
|
||||
Example usage::
|
||||
|
||||
@contextlib.contextmanager
|
||||
def die_on_error():
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
logging.error("exception in asynchronous operation",exc_info=True)
|
||||
sys.exit(1)
|
||||
|
||||
with StackContext(die_on_error):
|
||||
# Any exception thrown here *or in callback and its desendents*
|
||||
# will cause the process to exit instead of spinning endlessly
|
||||
# in the ioloop.
|
||||
http_client.fetch(url, callback)
|
||||
ioloop.start()
|
||||
|
||||
Most applications shouln't have to work with `StackContext` directly.
|
||||
Here are a few rules of thumb for when it's necessary:
|
||||
|
||||
* If you're writing an asynchronous library that doesn't rely on a
|
||||
stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
|
||||
(for example, if you're writing a thread pool), use
|
||||
`stack_context.wrap()` before any asynchronous operations to capture the
|
||||
stack context from where the operation was started.
|
||||
|
||||
* If you're writing an asynchronous library that has some shared
|
||||
resources (such as a connection pool), create those shared resources
|
||||
within a ``with stack_context.NullContext():`` block. This will prevent
|
||||
``StackContexts`` from leaking from one request to another.
|
||||
|
||||
* If you want to write something like an exception handler that will
|
||||
persist across asynchronous calls, create a new `StackContext` (or
|
||||
`ExceptionStackContext`), and make your asynchronous calls in a ``with``
|
||||
block that references your `StackContext`.
|
||||
'''
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import itertools
|
||||
import sys
|
||||
import threading
|
||||
|
||||
class _State(threading.local):
|
||||
def __init__(self):
|
||||
self.contexts = ()
|
||||
_state = _State()
|
||||
|
||||
class StackContext(object):
|
||||
'''Establishes the given context as a StackContext that will be transferred.
|
||||
|
||||
Note that the parameter is a callable that returns a context
|
||||
manager, not the context itself. That is, where for a
|
||||
non-transferable context manager you would say::
|
||||
|
||||
with my_context():
|
||||
|
||||
StackContext takes the function itself rather than its result::
|
||||
|
||||
with StackContext(my_context):
|
||||
'''
|
||||
def __init__(self, context_factory):
|
||||
self.context_factory = context_factory
|
||||
|
||||
# Note that some of this code is duplicated in ExceptionStackContext
|
||||
# below. ExceptionStackContext is more common and doesn't need
|
||||
# the full generality of this class.
|
||||
def __enter__(self):
|
||||
self.old_contexts = _state.contexts
|
||||
# _state.contexts is a tuple of (class, arg) pairs
|
||||
_state.contexts = (self.old_contexts +
|
||||
((StackContext, self.context_factory),))
|
||||
try:
|
||||
self.context = self.context_factory()
|
||||
self.context.__enter__()
|
||||
except Exception:
|
||||
_state.contexts = self.old_contexts
|
||||
raise
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
try:
|
||||
return self.context.__exit__(type, value, traceback)
|
||||
finally:
|
||||
_state.contexts = self.old_contexts
|
||||
|
||||
class ExceptionStackContext(object):
|
||||
'''Specialization of StackContext for exception handling.
|
||||
|
||||
The supplied exception_handler function will be called in the
|
||||
event of an uncaught exception in this context. The semantics are
|
||||
similar to a try/finally clause, and intended use cases are to log
|
||||
an error, close a socket, or similar cleanup actions. The
|
||||
exc_info triple (type, value, traceback) will be passed to the
|
||||
exception_handler function.
|
||||
|
||||
If the exception handler returns true, the exception will be
|
||||
consumed and will not be propagated to other exception handlers.
|
||||
'''
|
||||
def __init__(self, exception_handler):
|
||||
self.exception_handler = exception_handler
|
||||
|
||||
def __enter__(self):
|
||||
self.old_contexts = _state.contexts
|
||||
_state.contexts = (self.old_contexts +
|
||||
((ExceptionStackContext, self.exception_handler),))
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
try:
|
||||
if type is not None:
|
||||
return self.exception_handler(type, value, traceback)
|
||||
finally:
|
||||
_state.contexts = self.old_contexts
|
||||
|
||||
class NullContext(object):
|
||||
'''Resets the StackContext.
|
||||
|
||||
Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
|
||||
where the stack that caused the creating is not relevant to future
|
||||
operations.
|
||||
'''
|
||||
def __enter__(self):
|
||||
self.old_contexts = _state.contexts
|
||||
_state.contexts = ()
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
_state.contexts = self.old_contexts
|
||||
|
||||
class _StackContextWrapper(functools.partial):
|
||||
pass
|
||||
|
||||
def wrap(fn):
|
||||
'''Returns a callable object that will restore the current StackContext
|
||||
when executed.
|
||||
|
||||
Use this whenever saving a callback to be executed later in a
|
||||
different execution context (either in a different thread or
|
||||
asynchronously in the same thread).
|
||||
'''
|
||||
if fn is None or fn.__class__ is _StackContextWrapper:
|
||||
return fn
|
||||
# functools.wraps doesn't appear to work on functools.partial objects
|
||||
#@functools.wraps(fn)
|
||||
def wrapped(callback, contexts, *args, **kwargs):
|
||||
if contexts is _state.contexts or not contexts:
|
||||
callback(*args, **kwargs)
|
||||
return
|
||||
if not _state.contexts:
|
||||
new_contexts = [cls(arg) for (cls, arg) in contexts]
|
||||
# If we're moving down the stack, _state.contexts is a prefix
|
||||
# of contexts. For each element of contexts not in that prefix,
|
||||
# create a new StackContext object.
|
||||
# If we're moving up the stack (or to an entirely different stack),
|
||||
# _state.contexts will have elements not in contexts. Use
|
||||
# NullContext to clear the state and then recreate from contexts.
|
||||
elif (len(_state.contexts) > len(contexts) or
|
||||
any(a[1] is not b[1]
|
||||
for a, b in itertools.izip(_state.contexts, contexts))):
|
||||
# contexts have been removed or changed, so start over
|
||||
new_contexts = ([NullContext()] +
|
||||
[cls(arg) for (cls,arg) in contexts])
|
||||
else:
|
||||
new_contexts = [cls(arg)
|
||||
for (cls, arg) in contexts[len(_state.contexts):]]
|
||||
if len(new_contexts) > 1:
|
||||
with _nested(*new_contexts):
|
||||
callback(*args, **kwargs)
|
||||
elif new_contexts:
|
||||
with new_contexts[0]:
|
||||
callback(*args, **kwargs)
|
||||
else:
|
||||
callback(*args, **kwargs)
|
||||
if _state.contexts:
|
||||
return _StackContextWrapper(wrapped, fn, _state.contexts)
|
||||
else:
|
||||
return _StackContextWrapper(fn)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _nested(*managers):
|
||||
"""Support multiple context managers in a single with-statement.
|
||||
|
||||
Copied from the python 2.6 standard library. It's no longer present
|
||||
in python 3 because the with statement natively supports multiple
|
||||
context managers, but that doesn't help if the list of context
|
||||
managers is not known until runtime.
|
||||
"""
|
||||
exits = []
|
||||
vars = []
|
||||
exc = (None, None, None)
|
||||
try:
|
||||
for mgr in managers:
|
||||
exit = mgr.__exit__
|
||||
enter = mgr.__enter__
|
||||
vars.append(enter())
|
||||
exits.append(exit)
|
||||
yield vars
|
||||
except:
|
||||
exc = sys.exc_info()
|
||||
finally:
|
||||
while exits:
|
||||
exit = exits.pop()
|
||||
try:
|
||||
if exit(*exc):
|
||||
exc = (None, None, None)
|
||||
except:
|
||||
exc = sys.exc_info()
|
||||
if exc != (None, None, None):
|
||||
# Don't rely on sys.exc_info() still containing
|
||||
# the right information. Another exception may
|
||||
# have been raised and caught by an exit method
|
||||
raise exc[0], exc[1], exc[2]
|
||||
|
||||
826
libs/tornado/template.py
Normal file
826
libs/tornado/template.py
Normal file
@@ -0,0 +1,826 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""A simple template system that compiles templates to Python code.
|
||||
|
||||
Basic usage looks like::
|
||||
|
||||
t = template.Template("<html>{{ myvalue }}</html>")
|
||||
print t.generate(myvalue="XXX")
|
||||
|
||||
Loader is a class that loads templates from a root directory and caches
|
||||
the compiled templates::
|
||||
|
||||
loader = template.Loader("/home/btaylor")
|
||||
print loader.load("test.html").generate(myvalue="XXX")
|
||||
|
||||
We compile all templates to raw Python. Error-reporting is currently... uh,
|
||||
interesting. Syntax for the templates::
|
||||
|
||||
### base.html
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}Default title{% end %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
{% for student in students %}
|
||||
{% block student %}
|
||||
<li>{{ escape(student.name) }}</li>
|
||||
{% end %}
|
||||
{% end %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
### bold.html
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}A bolder title{% end %}
|
||||
|
||||
{% block student %}
|
||||
<li><span style="bold">{{ escape(student.name) }}</span></li>
|
||||
{% end %}
|
||||
|
||||
Unlike most other template systems, we do not put any restrictions on the
|
||||
expressions you can include in your statements. if and for blocks get
|
||||
translated exactly into Python, you can do complex expressions like::
|
||||
|
||||
{% for student in [p for p in people if p.student and p.age > 23] %}
|
||||
<li>{{ escape(student.name) }}</li>
|
||||
{% end %}
|
||||
|
||||
Translating directly to Python means you can apply functions to expressions
|
||||
easily, like the escape() function in the examples above. You can pass
|
||||
functions in to your template just like any other variable::
|
||||
|
||||
### Python code
|
||||
def add(x, y):
|
||||
return x + y
|
||||
template.execute(add=add)
|
||||
|
||||
### The template
|
||||
{{ add(1, 2) }}
|
||||
|
||||
We provide the functions escape(), url_escape(), json_encode(), and squeeze()
|
||||
to all templates by default.
|
||||
|
||||
Typical applications do not create `Template` or `Loader` instances by
|
||||
hand, but instead use the `render` and `render_string` methods of
|
||||
`tornado.web.RequestHandler`, which load templates automatically based
|
||||
on the ``template_path`` `Application` setting.
|
||||
|
||||
Syntax Reference
|
||||
----------------
|
||||
|
||||
Template expressions are surrounded by double curly braces: ``{{ ... }}``.
|
||||
The contents may be any python expression, which will be escaped according
|
||||
to the current autoescape setting and inserted into the output. Other
|
||||
template directives use ``{% %}``. These tags may be escaped as ``{{!``
|
||||
and ``{%!`` if you need to include a literal ``{{`` or ``{%`` in the output.
|
||||
|
||||
To comment out a section so that it is omitted from the output, surround it
|
||||
with ``{# ... #}``.
|
||||
|
||||
``{% apply *function* %}...{% end %}``
|
||||
Applies a function to the output of all template code between ``apply``
|
||||
and ``end``::
|
||||
|
||||
{% apply linkify %}{{name}} said: {{message}}{% end %}
|
||||
|
||||
``{% autoescape *function* %}``
|
||||
Sets the autoescape mode for the current file. This does not affect
|
||||
other files, even those referenced by ``{% include %}``. Note that
|
||||
autoescaping can also be configured globally, at the `Application`
|
||||
or `Loader`.::
|
||||
|
||||
{% autoescape xhtml_escape %}
|
||||
{% autoescape None %}
|
||||
|
||||
``{% block *name* %}...{% end %}``
|
||||
Indicates a named, replaceable block for use with ``{% extends %}``.
|
||||
Blocks in the parent template will be replaced with the contents of
|
||||
the same-named block in a child template.::
|
||||
|
||||
<!-- base.html -->
|
||||
<title>{% block title %}Default title{% end %}</title>
|
||||
|
||||
<!-- mypage.html -->
|
||||
{% extends "base.html" %}
|
||||
{% block title %}My page title{% end %}
|
||||
|
||||
``{% comment ... %}``
|
||||
A comment which will be removed from the template output. Note that
|
||||
there is no ``{% end %}`` tag; the comment goes from the word ``comment``
|
||||
to the closing ``%}`` tag.
|
||||
|
||||
``{% extends *filename* %}``
|
||||
Inherit from another template. Templates that use ``extends`` should
|
||||
contain one or more ``block`` tags to replace content from the parent
|
||||
template. Anything in the child template not contained in a ``block``
|
||||
tag will be ignored. For an example, see the ``{% block %}`` tag.
|
||||
|
||||
``{% for *var* in *expr* %}...{% end %}``
|
||||
Same as the python ``for`` statement.
|
||||
|
||||
``{% from *x* import *y* %}``
|
||||
Same as the python ``import`` statement.
|
||||
|
||||
``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}``
|
||||
Conditional statement - outputs the first section whose condition is
|
||||
true. (The ``elif`` and ``else`` sections are optional)
|
||||
|
||||
``{% import *module* %}``
|
||||
Same as the python ``import`` statement.
|
||||
|
||||
``{% include *filename* %}``
|
||||
Includes another template file. The included file can see all the local
|
||||
variables as if it were copied directly to the point of the ``include``
|
||||
directive (the ``{% autoescape %}`` directive is an exception).
|
||||
Alternately, ``{% module Template(filename, **kwargs) %}`` may be used
|
||||
to include another template with an isolated namespace.
|
||||
|
||||
``{% module *expr* %}``
|
||||
Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is
|
||||
not escaped::
|
||||
|
||||
{% module Template("foo.html", arg=42) %}
|
||||
|
||||
``{% raw *expr* %}``
|
||||
Outputs the result of the given expression without autoescaping.
|
||||
|
||||
``{% set *x* = *y* %}``
|
||||
Sets a local variable.
|
||||
|
||||
``{% try %}...{% except %}...{% finally %}...{% end %}``
|
||||
Same as the python ``try`` statement.
|
||||
|
||||
``{% while *condition* %}... {% end %}``
|
||||
Same as the python ``while`` statement.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import cStringIO
|
||||
import datetime
|
||||
import linecache
|
||||
import logging
|
||||
import os.path
|
||||
import posixpath
|
||||
import re
|
||||
import threading
|
||||
|
||||
from tornado import escape
|
||||
from tornado.util import bytes_type, ObjectDict
|
||||
|
||||
_DEFAULT_AUTOESCAPE = "xhtml_escape"
|
||||
_UNSET = object()
|
||||
|
||||
class Template(object):
|
||||
"""A compiled template.
|
||||
|
||||
We compile into Python from the given template_string. You can generate
|
||||
the template from variables with generate().
|
||||
"""
|
||||
def __init__(self, template_string, name="<string>", loader=None,
|
||||
compress_whitespace=None, autoescape=_UNSET):
|
||||
self.name = name
|
||||
if compress_whitespace is None:
|
||||
compress_whitespace = name.endswith(".html") or \
|
||||
name.endswith(".js")
|
||||
if autoescape is not _UNSET:
|
||||
self.autoescape = autoescape
|
||||
elif loader:
|
||||
self.autoescape = loader.autoescape
|
||||
else:
|
||||
self.autoescape = _DEFAULT_AUTOESCAPE
|
||||
self.namespace = loader.namespace if loader else {}
|
||||
reader = _TemplateReader(name, escape.native_str(template_string))
|
||||
self.file = _File(self, _parse(reader, self))
|
||||
self.code = self._generate_python(loader, compress_whitespace)
|
||||
self.loader = loader
|
||||
try:
|
||||
# Under python2.5, the fake filename used here must match
|
||||
# the module name used in __name__ below.
|
||||
self.compiled = compile(
|
||||
escape.to_unicode(self.code),
|
||||
"%s.generated.py" % self.name.replace('.','_'),
|
||||
"exec")
|
||||
except Exception:
|
||||
formatted_code = _format_code(self.code).rstrip()
|
||||
logging.error("%s code:\n%s", self.name, formatted_code)
|
||||
raise
|
||||
|
||||
def generate(self, **kwargs):
|
||||
"""Generate this template with the given arguments."""
|
||||
namespace = {
|
||||
"escape": escape.xhtml_escape,
|
||||
"xhtml_escape": escape.xhtml_escape,
|
||||
"url_escape": escape.url_escape,
|
||||
"json_encode": escape.json_encode,
|
||||
"squeeze": escape.squeeze,
|
||||
"linkify": escape.linkify,
|
||||
"datetime": datetime,
|
||||
"_utf8": escape.utf8, # for internal use
|
||||
"_string_types": (unicode, bytes_type),
|
||||
# __name__ and __loader__ allow the traceback mechanism to find
|
||||
# the generated source code.
|
||||
"__name__": self.name.replace('.', '_'),
|
||||
"__loader__": ObjectDict(get_source=lambda name: self.code),
|
||||
}
|
||||
namespace.update(self.namespace)
|
||||
namespace.update(kwargs)
|
||||
exec self.compiled in namespace
|
||||
execute = namespace["_execute"]
|
||||
# Clear the traceback module's cache of source data now that
|
||||
# we've generated a new template (mainly for this module's
|
||||
# unittests, where different tests reuse the same name).
|
||||
linecache.clearcache()
|
||||
try:
|
||||
return execute()
|
||||
except Exception:
|
||||
formatted_code = _format_code(self.code).rstrip()
|
||||
logging.error("%s code:\n%s", self.name, formatted_code)
|
||||
raise
|
||||
|
||||
def _generate_python(self, loader, compress_whitespace):
|
||||
buffer = cStringIO.StringIO()
|
||||
try:
|
||||
# named_blocks maps from names to _NamedBlock objects
|
||||
named_blocks = {}
|
||||
ancestors = self._get_ancestors(loader)
|
||||
ancestors.reverse()
|
||||
for ancestor in ancestors:
|
||||
ancestor.find_named_blocks(loader, named_blocks)
|
||||
self.file.find_named_blocks(loader, named_blocks)
|
||||
writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,
|
||||
compress_whitespace)
|
||||
ancestors[0].generate(writer)
|
||||
return buffer.getvalue()
|
||||
finally:
|
||||
buffer.close()
|
||||
|
||||
def _get_ancestors(self, loader):
|
||||
ancestors = [self.file]
|
||||
for chunk in self.file.body.chunks:
|
||||
if isinstance(chunk, _ExtendsBlock):
|
||||
if not loader:
|
||||
raise ParseError("{% extends %} block found, but no "
|
||||
"template loader")
|
||||
template = loader.load(chunk.name, self.name)
|
||||
ancestors.extend(template._get_ancestors(loader))
|
||||
return ancestors
|
||||
|
||||
|
||||
class BaseLoader(object):
|
||||
"""Base class for template loaders."""
|
||||
def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None):
|
||||
"""Creates a template loader.
|
||||
|
||||
root_directory may be the empty string if this loader does not
|
||||
use the filesystem.
|
||||
|
||||
autoescape must be either None or a string naming a function
|
||||
in the template namespace, such as "xhtml_escape".
|
||||
"""
|
||||
self.autoescape = autoescape
|
||||
self.namespace = namespace or {}
|
||||
self.templates = {}
|
||||
# self.lock protects self.templates. It's a reentrant lock
|
||||
# because templates may load other templates via `include` or
|
||||
# `extends`. Note that thanks to the GIL this code would be safe
|
||||
# even without the lock, but could lead to wasted work as multiple
|
||||
# threads tried to compile the same template simultaneously.
|
||||
self.lock = threading.RLock()
|
||||
|
||||
def reset(self):
|
||||
"""Resets the cache of compiled templates."""
|
||||
with self.lock:
|
||||
self.templates = {}
|
||||
|
||||
def resolve_path(self, name, parent_path=None):
|
||||
"""Converts a possibly-relative path to absolute (used internally)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def load(self, name, parent_path=None):
|
||||
"""Loads a template."""
|
||||
name = self.resolve_path(name, parent_path=parent_path)
|
||||
with self.lock:
|
||||
if name not in self.templates:
|
||||
self.templates[name] = self._create_template(name)
|
||||
return self.templates[name]
|
||||
|
||||
def _create_template(self, name):
|
||||
raise NotImplementedError()
|
||||
|
||||
class Loader(BaseLoader):
|
||||
"""A template loader that loads from a single root directory.
|
||||
|
||||
You must use a template loader to use template constructs like
|
||||
{% extends %} and {% include %}. Loader caches all templates after
|
||||
they are loaded the first time.
|
||||
"""
|
||||
def __init__(self, root_directory, **kwargs):
|
||||
super(Loader, self).__init__(**kwargs)
|
||||
self.root = os.path.abspath(root_directory)
|
||||
|
||||
def resolve_path(self, name, parent_path=None):
|
||||
if parent_path and not parent_path.startswith("<") and \
|
||||
not parent_path.startswith("/") and \
|
||||
not name.startswith("/"):
|
||||
current_path = os.path.join(self.root, parent_path)
|
||||
file_dir = os.path.dirname(os.path.abspath(current_path))
|
||||
relative_path = os.path.abspath(os.path.join(file_dir, name))
|
||||
if relative_path.startswith(self.root):
|
||||
name = relative_path[len(self.root) + 1:]
|
||||
return name
|
||||
|
||||
def _create_template(self, name):
|
||||
path = os.path.join(self.root, name)
|
||||
f = open(path, "r")
|
||||
template = Template(f.read(), name=name, loader=self)
|
||||
f.close()
|
||||
return template
|
||||
|
||||
|
||||
class DictLoader(BaseLoader):
|
||||
"""A template loader that loads from a dictionary."""
|
||||
def __init__(self, dict, **kwargs):
|
||||
super(DictLoader, self).__init__(**kwargs)
|
||||
self.dict = dict
|
||||
|
||||
def resolve_path(self, name, parent_path=None):
|
||||
if parent_path and not parent_path.startswith("<") and \
|
||||
not parent_path.startswith("/") and \
|
||||
not name.startswith("/"):
|
||||
file_dir = posixpath.dirname(parent_path)
|
||||
name = posixpath.normpath(posixpath.join(file_dir, name))
|
||||
return name
|
||||
|
||||
def _create_template(self, name):
|
||||
return Template(self.dict[name], name=name, loader=self)
|
||||
|
||||
|
||||
class _Node(object):
|
||||
def each_child(self):
|
||||
return ()
|
||||
|
||||
def generate(self, writer):
|
||||
raise NotImplementedError()
|
||||
|
||||
def find_named_blocks(self, loader, named_blocks):
|
||||
for child in self.each_child():
|
||||
child.find_named_blocks(loader, named_blocks)
|
||||
|
||||
|
||||
class _File(_Node):
|
||||
def __init__(self, template, body):
|
||||
self.template = template
|
||||
self.body = body
|
||||
self.line = 0
|
||||
|
||||
def generate(self, writer):
|
||||
writer.write_line("def _execute():", self.line)
|
||||
with writer.indent():
|
||||
writer.write_line("_buffer = []", self.line)
|
||||
writer.write_line("_append = _buffer.append", self.line)
|
||||
self.body.generate(writer)
|
||||
writer.write_line("return _utf8('').join(_buffer)", self.line)
|
||||
|
||||
def each_child(self):
|
||||
return (self.body,)
|
||||
|
||||
|
||||
|
||||
class _ChunkList(_Node):
|
||||
def __init__(self, chunks):
|
||||
self.chunks = chunks
|
||||
|
||||
def generate(self, writer):
|
||||
for chunk in self.chunks:
|
||||
chunk.generate(writer)
|
||||
|
||||
def each_child(self):
|
||||
return self.chunks
|
||||
|
||||
|
||||
class _NamedBlock(_Node):
|
||||
def __init__(self, name, body, template, line):
|
||||
self.name = name
|
||||
self.body = body
|
||||
self.template = template
|
||||
self.line = line
|
||||
|
||||
def each_child(self):
|
||||
return (self.body,)
|
||||
|
||||
def generate(self, writer):
|
||||
block = writer.named_blocks[self.name]
|
||||
with writer.include(block.template, self.line):
|
||||
block.body.generate(writer)
|
||||
|
||||
def find_named_blocks(self, loader, named_blocks):
|
||||
named_blocks[self.name] = self
|
||||
_Node.find_named_blocks(self, loader, named_blocks)
|
||||
|
||||
|
||||
class _ExtendsBlock(_Node):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
class _IncludeBlock(_Node):
|
||||
def __init__(self, name, reader, line):
|
||||
self.name = name
|
||||
self.template_name = reader.name
|
||||
self.line = line
|
||||
|
||||
def find_named_blocks(self, loader, named_blocks):
|
||||
included = loader.load(self.name, self.template_name)
|
||||
included.file.find_named_blocks(loader, named_blocks)
|
||||
|
||||
def generate(self, writer):
|
||||
included = writer.loader.load(self.name, self.template_name)
|
||||
with writer.include(included, self.line):
|
||||
included.file.body.generate(writer)
|
||||
|
||||
|
||||
class _ApplyBlock(_Node):
|
||||
def __init__(self, method, line, body=None):
|
||||
self.method = method
|
||||
self.line = line
|
||||
self.body = body
|
||||
|
||||
def each_child(self):
|
||||
return (self.body,)
|
||||
|
||||
def generate(self, writer):
|
||||
method_name = "apply%d" % writer.apply_counter
|
||||
writer.apply_counter += 1
|
||||
writer.write_line("def %s():" % method_name, self.line)
|
||||
with writer.indent():
|
||||
writer.write_line("_buffer = []", self.line)
|
||||
writer.write_line("_append = _buffer.append", self.line)
|
||||
self.body.generate(writer)
|
||||
writer.write_line("return _utf8('').join(_buffer)", self.line)
|
||||
writer.write_line("_append(%s(%s()))" % (
|
||||
self.method, method_name), self.line)
|
||||
|
||||
|
||||
class _ControlBlock(_Node):
|
||||
def __init__(self, statement, line, body=None):
|
||||
self.statement = statement
|
||||
self.line = line
|
||||
self.body = body
|
||||
|
||||
def each_child(self):
|
||||
return (self.body,)
|
||||
|
||||
def generate(self, writer):
|
||||
writer.write_line("%s:" % self.statement, self.line)
|
||||
with writer.indent():
|
||||
self.body.generate(writer)
|
||||
|
||||
|
||||
class _IntermediateControlBlock(_Node):
|
||||
def __init__(self, statement, line):
|
||||
self.statement = statement
|
||||
self.line = line
|
||||
|
||||
def generate(self, writer):
|
||||
writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1)
|
||||
|
||||
|
||||
class _Statement(_Node):
|
||||
def __init__(self, statement, line):
|
||||
self.statement = statement
|
||||
self.line = line
|
||||
|
||||
def generate(self, writer):
|
||||
writer.write_line(self.statement, self.line)
|
||||
|
||||
|
||||
class _Expression(_Node):
|
||||
def __init__(self, expression, line, raw=False):
|
||||
self.expression = expression
|
||||
self.line = line
|
||||
self.raw = raw
|
||||
|
||||
def generate(self, writer):
|
||||
writer.write_line("_tmp = %s" % self.expression, self.line)
|
||||
writer.write_line("if isinstance(_tmp, _string_types):"
|
||||
" _tmp = _utf8(_tmp)", self.line)
|
||||
writer.write_line("else: _tmp = _utf8(str(_tmp))", self.line)
|
||||
if not self.raw and writer.current_template.autoescape is not None:
|
||||
# In python3 functions like xhtml_escape return unicode,
|
||||
# so we have to convert to utf8 again.
|
||||
writer.write_line("_tmp = _utf8(%s(_tmp))" %
|
||||
writer.current_template.autoescape, self.line)
|
||||
writer.write_line("_append(_tmp)", self.line)
|
||||
|
||||
class _Module(_Expression):
|
||||
def __init__(self, expression, line):
|
||||
super(_Module, self).__init__("_modules." + expression, line,
|
||||
raw=True)
|
||||
|
||||
class _Text(_Node):
|
||||
def __init__(self, value, line):
|
||||
self.value = value
|
||||
self.line = line
|
||||
|
||||
def generate(self, writer):
|
||||
value = self.value
|
||||
|
||||
# Compress lots of white space to a single character. If the whitespace
|
||||
# breaks a line, have it continue to break a line, but just with a
|
||||
# single \n character
|
||||
if writer.compress_whitespace and "<pre>" not in value:
|
||||
value = re.sub(r"([\t ]+)", " ", value)
|
||||
value = re.sub(r"(\s*\n\s*)", "\n", value)
|
||||
|
||||
if value:
|
||||
writer.write_line('_append(%r)' % escape.utf8(value), self.line)
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
"""Raised for template syntax errors."""
|
||||
pass
|
||||
|
||||
|
||||
class _CodeWriter(object):
|
||||
def __init__(self, file, named_blocks, loader, current_template,
|
||||
compress_whitespace):
|
||||
self.file = file
|
||||
self.named_blocks = named_blocks
|
||||
self.loader = loader
|
||||
self.current_template = current_template
|
||||
self.compress_whitespace = compress_whitespace
|
||||
self.apply_counter = 0
|
||||
self.include_stack = []
|
||||
self._indent = 0
|
||||
|
||||
def indent_size(self):
|
||||
return self._indent
|
||||
|
||||
def indent(self):
|
||||
class Indenter(object):
|
||||
def __enter__(_):
|
||||
self._indent += 1
|
||||
return self
|
||||
|
||||
def __exit__(_, *args):
|
||||
assert self._indent > 0
|
||||
self._indent -= 1
|
||||
|
||||
return Indenter()
|
||||
|
||||
def include(self, template, line):
|
||||
self.include_stack.append((self.current_template, line))
|
||||
self.current_template = template
|
||||
|
||||
class IncludeTemplate(object):
|
||||
def __enter__(_):
|
||||
return self
|
||||
|
||||
def __exit__(_, *args):
|
||||
self.current_template = self.include_stack.pop()[0]
|
||||
|
||||
return IncludeTemplate()
|
||||
|
||||
def write_line(self, line, line_number, indent=None):
|
||||
if indent == None:
|
||||
indent = self._indent
|
||||
line_comment = ' # %s:%d' % (self.current_template.name, line_number)
|
||||
if self.include_stack:
|
||||
ancestors = ["%s:%d" % (tmpl.name, lineno)
|
||||
for (tmpl, lineno) in self.include_stack]
|
||||
line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
|
||||
print >> self.file, " "*indent + line + line_comment
|
||||
|
||||
|
||||
class _TemplateReader(object):
|
||||
def __init__(self, name, text):
|
||||
self.name = name
|
||||
self.text = text
|
||||
self.line = 1
|
||||
self.pos = 0
|
||||
|
||||
def find(self, needle, start=0, end=None):
|
||||
assert start >= 0, start
|
||||
pos = self.pos
|
||||
start += pos
|
||||
if end is None:
|
||||
index = self.text.find(needle, start)
|
||||
else:
|
||||
end += pos
|
||||
assert end >= start
|
||||
index = self.text.find(needle, start, end)
|
||||
if index != -1:
|
||||
index -= pos
|
||||
return index
|
||||
|
||||
def consume(self, count=None):
|
||||
if count is None:
|
||||
count = len(self.text) - self.pos
|
||||
newpos = self.pos + count
|
||||
self.line += self.text.count("\n", self.pos, newpos)
|
||||
s = self.text[self.pos:newpos]
|
||||
self.pos = newpos
|
||||
return s
|
||||
|
||||
def remaining(self):
|
||||
return len(self.text) - self.pos
|
||||
|
||||
def __len__(self):
|
||||
return self.remaining()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if type(key) is slice:
|
||||
size = len(self)
|
||||
start, stop, step = key.indices(size)
|
||||
if start is None: start = self.pos
|
||||
else: start += self.pos
|
||||
if stop is not None: stop += self.pos
|
||||
return self.text[slice(start, stop, step)]
|
||||
elif key < 0:
|
||||
return self.text[key]
|
||||
else:
|
||||
return self.text[self.pos + key]
|
||||
|
||||
def __str__(self):
|
||||
return self.text[self.pos:]
|
||||
|
||||
|
||||
def _format_code(code):
|
||||
lines = code.splitlines()
|
||||
format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
|
||||
return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
|
||||
|
||||
|
||||
def _parse(reader, template, in_block=None):
|
||||
body = _ChunkList([])
|
||||
while True:
|
||||
# Find next template directive
|
||||
curly = 0
|
||||
while True:
|
||||
curly = reader.find("{", curly)
|
||||
if curly == -1 or curly + 1 == reader.remaining():
|
||||
# EOF
|
||||
if in_block:
|
||||
raise ParseError("Missing {%% end %%} block for %s" %
|
||||
in_block)
|
||||
body.chunks.append(_Text(reader.consume(), reader.line))
|
||||
return body
|
||||
# If the first curly brace is not the start of a special token,
|
||||
# start searching from the character after it
|
||||
if reader[curly + 1] not in ("{", "%", "#"):
|
||||
curly += 1
|
||||
continue
|
||||
# When there are more than 2 curlies in a row, use the
|
||||
# innermost ones. This is useful when generating languages
|
||||
# like latex where curlies are also meaningful
|
||||
if (curly + 2 < reader.remaining() and
|
||||
reader[curly + 1] == '{' and reader[curly + 2] == '{'):
|
||||
curly += 1
|
||||
continue
|
||||
break
|
||||
|
||||
# Append any text before the special token
|
||||
if curly > 0:
|
||||
cons = reader.consume(curly)
|
||||
body.chunks.append(_Text(cons, reader.line))
|
||||
|
||||
start_brace = reader.consume(2)
|
||||
line = reader.line
|
||||
|
||||
# Template directives may be escaped as "{{!" or "{%!".
|
||||
# In this case output the braces and consume the "!".
|
||||
# This is especially useful in conjunction with jquery templates,
|
||||
# which also use double braces.
|
||||
if reader.remaining() and reader[0] == "!":
|
||||
reader.consume(1)
|
||||
body.chunks.append(_Text(start_brace, line))
|
||||
continue
|
||||
|
||||
# Comment
|
||||
if start_brace == "{#":
|
||||
end = reader.find("#}")
|
||||
if end == -1:
|
||||
raise ParseError("Missing end expression #} on line %d" % line)
|
||||
contents = reader.consume(end).strip()
|
||||
reader.consume(2)
|
||||
continue
|
||||
|
||||
# Expression
|
||||
if start_brace == "{{":
|
||||
end = reader.find("}}")
|
||||
if end == -1:
|
||||
raise ParseError("Missing end expression }} on line %d" % line)
|
||||
contents = reader.consume(end).strip()
|
||||
reader.consume(2)
|
||||
if not contents:
|
||||
raise ParseError("Empty expression on line %d" % line)
|
||||
body.chunks.append(_Expression(contents, line))
|
||||
continue
|
||||
|
||||
# Block
|
||||
assert start_brace == "{%", start_brace
|
||||
end = reader.find("%}")
|
||||
if end == -1:
|
||||
raise ParseError("Missing end block %%} on line %d" % line)
|
||||
contents = reader.consume(end).strip()
|
||||
reader.consume(2)
|
||||
if not contents:
|
||||
raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
|
||||
|
||||
operator, space, suffix = contents.partition(" ")
|
||||
suffix = suffix.strip()
|
||||
|
||||
# Intermediate ("else", "elif", etc) blocks
|
||||
intermediate_blocks = {
|
||||
"else": set(["if", "for", "while"]),
|
||||
"elif": set(["if"]),
|
||||
"except": set(["try"]),
|
||||
"finally": set(["try"]),
|
||||
}
|
||||
allowed_parents = intermediate_blocks.get(operator)
|
||||
if allowed_parents is not None:
|
||||
if not in_block:
|
||||
raise ParseError("%s outside %s block" %
|
||||
(operator, allowed_parents))
|
||||
if in_block not in allowed_parents:
|
||||
raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
|
||||
body.chunks.append(_IntermediateControlBlock(contents, line))
|
||||
continue
|
||||
|
||||
# End tag
|
||||
elif operator == "end":
|
||||
if not in_block:
|
||||
raise ParseError("Extra {%% end %%} block on line %d" % line)
|
||||
return body
|
||||
|
||||
elif operator in ("extends", "include", "set", "import", "from",
|
||||
"comment", "autoescape", "raw", "module"):
|
||||
if operator == "comment":
|
||||
continue
|
||||
if operator == "extends":
|
||||
suffix = suffix.strip('"').strip("'")
|
||||
if not suffix:
|
||||
raise ParseError("extends missing file path on line %d" % line)
|
||||
block = _ExtendsBlock(suffix)
|
||||
elif operator in ("import", "from"):
|
||||
if not suffix:
|
||||
raise ParseError("import missing statement on line %d" % line)
|
||||
block = _Statement(contents, line)
|
||||
elif operator == "include":
|
||||
suffix = suffix.strip('"').strip("'")
|
||||
if not suffix:
|
||||
raise ParseError("include missing file path on line %d" % line)
|
||||
block = _IncludeBlock(suffix, reader, line)
|
||||
elif operator == "set":
|
||||
if not suffix:
|
||||
raise ParseError("set missing statement on line %d" % line)
|
||||
block = _Statement(suffix, line)
|
||||
elif operator == "autoescape":
|
||||
fn = suffix.strip()
|
||||
if fn == "None": fn = None
|
||||
template.autoescape = fn
|
||||
continue
|
||||
elif operator == "raw":
|
||||
block = _Expression(suffix, line, raw=True)
|
||||
elif operator == "module":
|
||||
block = _Module(suffix, line)
|
||||
body.chunks.append(block)
|
||||
continue
|
||||
|
||||
elif operator in ("apply", "block", "try", "if", "for", "while"):
|
||||
# parse inner body recursively
|
||||
block_body = _parse(reader, template, operator)
|
||||
if operator == "apply":
|
||||
if not suffix:
|
||||
raise ParseError("apply missing method name on line %d" % line)
|
||||
block = _ApplyBlock(suffix, line, block_body)
|
||||
elif operator == "block":
|
||||
if not suffix:
|
||||
raise ParseError("block missing name on line %d" % line)
|
||||
block = _NamedBlock(suffix, block_body, template, line)
|
||||
else:
|
||||
block = _ControlBlock(contents, line, block_body)
|
||||
body.chunks.append(block)
|
||||
continue
|
||||
|
||||
else:
|
||||
raise ParseError("unknown operator: %r" % operator)
|
||||
382
libs/tornado/testing.py
Normal file
382
libs/tornado/testing.py
Normal file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python
|
||||
"""Support classes for automated testing.
|
||||
|
||||
This module contains three parts:
|
||||
|
||||
* `AsyncTestCase`/`AsyncHTTPTestCase`: Subclasses of unittest.TestCase
|
||||
with additional support for testing asynchronous (IOLoop-based) code.
|
||||
|
||||
* `LogTrapTestCase`: Subclass of unittest.TestCase that discards log output
|
||||
from tests that pass and only produces output for failing tests.
|
||||
|
||||
* `main()`: A simple test runner (wrapper around unittest.main()) with support
|
||||
for the tornado.autoreload module to rerun the tests when code changes.
|
||||
|
||||
These components may be used together or independently. In particular,
|
||||
it is safe to combine AsyncTestCase and LogTrapTestCase via multiple
|
||||
inheritance. See the docstrings for each class/function below for more
|
||||
information.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from cStringIO import StringIO
|
||||
try:
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.ioloop import IOLoop
|
||||
except ImportError:
|
||||
# These modules are not importable on app engine. Parts of this module
|
||||
# won't work, but e.g. LogTrapTestCase and main() will.
|
||||
AsyncHTTPClient = None
|
||||
HTTPServer = None
|
||||
IOLoop = None
|
||||
from tornado.stack_context import StackContext, NullContext
|
||||
import contextlib
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
_next_port = 10000
|
||||
def get_unused_port():
|
||||
"""Returns a (hopefully) unused port number."""
|
||||
global _next_port
|
||||
port = _next_port
|
||||
_next_port = _next_port + 1
|
||||
return port
|
||||
|
||||
class AsyncTestCase(unittest.TestCase):
|
||||
"""TestCase subclass for testing IOLoop-based asynchronous code.
|
||||
|
||||
The unittest framework is synchronous, so the test must be complete
|
||||
by the time the test method returns. This method provides the stop()
|
||||
and wait() methods for this purpose. The test method itself must call
|
||||
self.wait(), and asynchronous callbacks should call self.stop() to signal
|
||||
completion.
|
||||
|
||||
By default, a new IOLoop is constructed for each test and is available
|
||||
as self.io_loop. This IOLoop should be used in the construction of
|
||||
HTTP clients/servers, etc. If the code being tested requires a
|
||||
global IOLoop, subclasses should override get_new_ioloop to return it.
|
||||
|
||||
The IOLoop's start and stop methods should not be called directly.
|
||||
Instead, use self.stop self.wait. Arguments passed to self.stop are
|
||||
returned from self.wait. It is possible to have multiple
|
||||
wait/stop cycles in the same test.
|
||||
|
||||
Example::
|
||||
|
||||
# This test uses an asynchronous style similar to most async
|
||||
# application code.
|
||||
class MyTestCase(AsyncTestCase):
|
||||
def test_http_fetch(self):
|
||||
client = AsyncHTTPClient(self.io_loop)
|
||||
client.fetch("http://www.tornadoweb.org/", self.handle_fetch)
|
||||
self.wait()
|
||||
|
||||
def handle_fetch(self, response):
|
||||
# Test contents of response (failures and exceptions here
|
||||
# will cause self.wait() to throw an exception and end the
|
||||
# test).
|
||||
# Exceptions thrown here are magically propagated to
|
||||
# self.wait() in test_http_fetch() via stack_context.
|
||||
self.assertIn("FriendFeed", response.body)
|
||||
self.stop()
|
||||
|
||||
# This test uses the argument passing between self.stop and self.wait
|
||||
# for a simpler, more synchronous style.
|
||||
# This style is recommended over the preceding example because it
|
||||
# keeps the assertions in the test method itself, and is therefore
|
||||
# less sensitive to the subtleties of stack_context.
|
||||
class MyTestCase2(AsyncTestCase):
|
||||
def test_http_fetch(self):
|
||||
client = AsyncHTTPClient(self.io_loop)
|
||||
client.fetch("http://www.tornadoweb.org/", self.stop)
|
||||
response = self.wait()
|
||||
# Test contents of response
|
||||
self.assertIn("FriendFeed", response.body)
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AsyncTestCase, self).__init__(*args, **kwargs)
|
||||
self.__stopped = False
|
||||
self.__running = False
|
||||
self.__failure = None
|
||||
self.__stop_args = None
|
||||
|
||||
def setUp(self):
|
||||
super(AsyncTestCase, self).setUp()
|
||||
self.io_loop = self.get_new_ioloop()
|
||||
|
||||
def tearDown(self):
|
||||
if (not IOLoop.initialized() or
|
||||
self.io_loop is not IOLoop.instance()):
|
||||
# Try to clean up any file descriptors left open in the ioloop.
|
||||
# This avoids leaks, especially when tests are run repeatedly
|
||||
# in the same process with autoreload (because curl does not
|
||||
# set FD_CLOEXEC on its file descriptors)
|
||||
self.io_loop.close(all_fds=True)
|
||||
super(AsyncTestCase, self).tearDown()
|
||||
|
||||
def get_new_ioloop(self):
|
||||
'''Creates a new IOLoop for this test. May be overridden in
|
||||
subclasses for tests that require a specific IOLoop (usually
|
||||
the singleton).
|
||||
'''
|
||||
return IOLoop()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _stack_context(self):
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
self.__failure = sys.exc_info()
|
||||
self.stop()
|
||||
|
||||
def run(self, result=None):
|
||||
with StackContext(self._stack_context):
|
||||
super(AsyncTestCase, self).run(result)
|
||||
|
||||
def stop(self, _arg=None, **kwargs):
|
||||
'''Stops the ioloop, causing one pending (or future) call to wait()
|
||||
to return.
|
||||
|
||||
Keyword arguments or a single positional argument passed to stop() are
|
||||
saved and will be returned by wait().
|
||||
'''
|
||||
assert _arg is None or not kwargs
|
||||
self.__stop_args = kwargs or _arg
|
||||
if self.__running:
|
||||
self.io_loop.stop()
|
||||
self.__running = False
|
||||
self.__stopped = True
|
||||
|
||||
def wait(self, condition=None, timeout=5):
|
||||
"""Runs the IOLoop until stop is called or timeout has passed.
|
||||
|
||||
In the event of a timeout, an exception will be thrown.
|
||||
|
||||
If condition is not None, the IOLoop will be restarted after stop()
|
||||
until condition() returns true.
|
||||
"""
|
||||
if not self.__stopped:
|
||||
if timeout:
|
||||
def timeout_func():
|
||||
try:
|
||||
raise self.failureException(
|
||||
'Async operation timed out after %d seconds' %
|
||||
timeout)
|
||||
except Exception:
|
||||
self.__failure = sys.exc_info()
|
||||
self.stop()
|
||||
self.io_loop.add_timeout(time.time() + timeout, timeout_func)
|
||||
while True:
|
||||
self.__running = True
|
||||
with NullContext():
|
||||
# Wipe out the StackContext that was established in
|
||||
# self.run() so that all callbacks executed inside the
|
||||
# IOLoop will re-run it.
|
||||
self.io_loop.start()
|
||||
if (self.__failure is not None or
|
||||
condition is None or condition()):
|
||||
break
|
||||
assert self.__stopped
|
||||
self.__stopped = False
|
||||
if self.__failure is not None:
|
||||
# 2to3 isn't smart enough to convert three-argument raise
|
||||
# statements correctly in some cases.
|
||||
if isinstance(self.__failure[1], self.__failure[0]):
|
||||
raise self.__failure[1], None, self.__failure[2]
|
||||
else:
|
||||
raise self.__failure[0], self.__failure[1], self.__failure[2]
|
||||
result = self.__stop_args
|
||||
self.__stop_args = None
|
||||
return result
|
||||
|
||||
|
||||
class AsyncHTTPTestCase(AsyncTestCase):
|
||||
'''A test case that starts up an HTTP server.
|
||||
|
||||
Subclasses must override get_app(), which returns the
|
||||
tornado.web.Application (or other HTTPServer callback) to be tested.
|
||||
Tests will typically use the provided self.http_client to fetch
|
||||
URLs from this server.
|
||||
|
||||
Example::
|
||||
|
||||
class MyHTTPTest(AsyncHTTPTestCase):
|
||||
def get_app(self):
|
||||
return Application([('/', MyHandler)...])
|
||||
|
||||
def test_homepage(self):
|
||||
# The following two lines are equivalent to
|
||||
# response = self.fetch('/')
|
||||
# but are shown in full here to demonstrate explicit use
|
||||
# of self.stop and self.wait.
|
||||
self.http_client.fetch(self.get_url('/'), self.stop)
|
||||
response = self.wait()
|
||||
# test contents of response
|
||||
'''
|
||||
def setUp(self):
|
||||
super(AsyncHTTPTestCase, self).setUp()
|
||||
self.__port = None
|
||||
|
||||
self.http_client = AsyncHTTPClient(io_loop=self.io_loop)
|
||||
self._app = self.get_app()
|
||||
self.http_server = HTTPServer(self._app, io_loop=self.io_loop,
|
||||
**self.get_httpserver_options())
|
||||
self.http_server.listen(self.get_http_port(), address="127.0.0.1")
|
||||
|
||||
def get_app(self):
|
||||
"""Should be overridden by subclasses to return a
|
||||
tornado.web.Application or other HTTPServer callback.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def fetch(self, path, **kwargs):
|
||||
"""Convenience method to synchronously fetch a url.
|
||||
|
||||
The given path will be appended to the local server's host and port.
|
||||
Any additional kwargs will be passed directly to
|
||||
AsyncHTTPClient.fetch (and so could be used to pass method="POST",
|
||||
body="...", etc).
|
||||
"""
|
||||
self.http_client.fetch(self.get_url(path), self.stop, **kwargs)
|
||||
return self.wait()
|
||||
|
||||
def get_httpserver_options(self):
|
||||
"""May be overridden by subclasses to return additional
|
||||
keyword arguments for HTTPServer.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_http_port(self):
|
||||
"""Returns the port used by the HTTPServer.
|
||||
|
||||
A new port is chosen for each test.
|
||||
"""
|
||||
if self.__port is None:
|
||||
self.__port = get_unused_port()
|
||||
return self.__port
|
||||
|
||||
def get_url(self, path):
|
||||
"""Returns an absolute url for the given path on the test server."""
|
||||
return 'http://localhost:%s%s' % (self.get_http_port(), path)
|
||||
|
||||
def tearDown(self):
|
||||
self.http_server.stop()
|
||||
self.http_client.close()
|
||||
super(AsyncHTTPTestCase, self).tearDown()
|
||||
|
||||
class LogTrapTestCase(unittest.TestCase):
|
||||
"""A test case that captures and discards all logging output
|
||||
if the test passes.
|
||||
|
||||
Some libraries can produce a lot of logging output even when
|
||||
the test succeeds, so this class can be useful to minimize the noise.
|
||||
Simply use it as a base class for your test case. It is safe to combine
|
||||
with AsyncTestCase via multiple inheritance
|
||||
("class MyTestCase(AsyncHTTPTestCase, LogTrapTestCase):")
|
||||
|
||||
This class assumes that only one log handler is configured and that
|
||||
it is a StreamHandler. This is true for both logging.basicConfig
|
||||
and the "pretty logging" configured by tornado.options.
|
||||
"""
|
||||
def run(self, result=None):
|
||||
logger = logging.getLogger()
|
||||
if len(logger.handlers) > 1:
|
||||
# Multiple handlers have been defined. It gets messy to handle
|
||||
# this, especially since the handlers may have different
|
||||
# formatters. Just leave the logging alone in this case.
|
||||
super(LogTrapTestCase, self).run(result)
|
||||
return
|
||||
if not logger.handlers:
|
||||
logging.basicConfig()
|
||||
self.assertEqual(len(logger.handlers), 1)
|
||||
handler = logger.handlers[0]
|
||||
assert isinstance(handler, logging.StreamHandler)
|
||||
old_stream = handler.stream
|
||||
try:
|
||||
handler.stream = StringIO()
|
||||
logging.info("RUNNING TEST: " + str(self))
|
||||
old_error_count = len(result.failures) + len(result.errors)
|
||||
super(LogTrapTestCase, self).run(result)
|
||||
new_error_count = len(result.failures) + len(result.errors)
|
||||
if new_error_count != old_error_count:
|
||||
old_stream.write(handler.stream.getvalue())
|
||||
finally:
|
||||
handler.stream = old_stream
|
||||
|
||||
def main():
|
||||
"""A simple test runner.
|
||||
|
||||
This test runner is essentially equivalent to `unittest.main` from
|
||||
the standard library, but adds support for tornado-style option
|
||||
parsing and log formatting.
|
||||
|
||||
The easiest way to run a test is via the command line::
|
||||
|
||||
python -m tornado.testing tornado.test.stack_context_test
|
||||
|
||||
See the standard library unittest module for ways in which tests can
|
||||
be specified.
|
||||
|
||||
Projects with many tests may wish to define a test script like
|
||||
tornado/test/runtests.py. This script should define a method all()
|
||||
which returns a test suite and then call tornado.testing.main().
|
||||
Note that even when a test script is used, the all() test suite may
|
||||
be overridden by naming a single test on the command line::
|
||||
|
||||
# Runs all tests
|
||||
tornado/test/runtests.py
|
||||
# Runs one test
|
||||
tornado/test/runtests.py tornado.test.stack_context_test
|
||||
|
||||
"""
|
||||
from tornado.options import define, options, parse_command_line
|
||||
|
||||
define('autoreload', type=bool, default=False,
|
||||
help="DEPRECATED: use tornado.autoreload.main instead")
|
||||
define('httpclient', type=str, default=None)
|
||||
define('exception_on_interrupt', type=bool, default=True,
|
||||
help=("If true (default), ctrl-c raises a KeyboardInterrupt "
|
||||
"exception. This prints a stack trace but cannot interrupt "
|
||||
"certain operations. If false, the process is more reliably "
|
||||
"killed, but does not print a stack trace."))
|
||||
argv = [sys.argv[0]] + parse_command_line(sys.argv)
|
||||
|
||||
if options.httpclient:
|
||||
from tornado.httpclient import AsyncHTTPClient
|
||||
AsyncHTTPClient.configure(options.httpclient)
|
||||
|
||||
if not options.exception_on_interrupt:
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if __name__ == '__main__' and len(argv) == 1:
|
||||
print >> sys.stderr, "No tests specified"
|
||||
sys.exit(1)
|
||||
try:
|
||||
# In order to be able to run tests by their fully-qualified name
|
||||
# on the command line without importing all tests here,
|
||||
# module must be set to None. Python 3.2's unittest.main ignores
|
||||
# defaultTest if no module is given (it tries to do its own
|
||||
# test discovery, which is incompatible with auto2to3), so don't
|
||||
# set module if we're not asking for a specific test.
|
||||
if len(argv) > 1:
|
||||
unittest.main(module=None, argv=argv)
|
||||
else:
|
||||
unittest.main(defaultTest="all", argv=argv)
|
||||
except SystemExit, e:
|
||||
if e.code == 0:
|
||||
logging.info('PASS')
|
||||
else:
|
||||
logging.error('FAIL')
|
||||
if not options.autoreload:
|
||||
raise
|
||||
if options.autoreload:
|
||||
import tornado.autoreload
|
||||
tornado.autoreload.wait()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
47
libs/tornado/util.py
Normal file
47
libs/tornado/util.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Miscellaneous utility functions."""
|
||||
|
||||
class ObjectDict(dict):
|
||||
"""Makes a dictionary behave like an object."""
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self[name] = value
|
||||
|
||||
|
||||
def import_object(name):
|
||||
"""Imports an object by name.
|
||||
|
||||
import_object('x.y.z') is equivalent to 'from x.y import z'.
|
||||
|
||||
>>> import tornado.escape
|
||||
>>> import_object('tornado.escape') is tornado.escape
|
||||
True
|
||||
>>> import_object('tornado.escape.utf8') is tornado.escape.utf8
|
||||
True
|
||||
"""
|
||||
parts = name.split('.')
|
||||
obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
|
||||
return getattr(obj, parts[-1])
|
||||
|
||||
# Fake byte literal support: In python 2.6+, you can say b"foo" to get
|
||||
# a byte literal (str in 2.x, bytes in 3.x). There's no way to do this
|
||||
# in a way that supports 2.5, though, so we need a function wrapper
|
||||
# to convert our string literals. b() should only be applied to literal
|
||||
# latin1 strings. Once we drop support for 2.5, we can remove this function
|
||||
# and just use byte literals.
|
||||
if str is unicode:
|
||||
def b(s):
|
||||
return s.encode('latin1')
|
||||
bytes_type = bytes
|
||||
else:
|
||||
def b(s):
|
||||
return s
|
||||
bytes_type = str
|
||||
|
||||
def doctests():
|
||||
import doctest
|
||||
return doctest.DocTestSuite()
|
||||
1985
libs/tornado/web.py
Normal file
1985
libs/tornado/web.py
Normal file
File diff suppressed because it is too large
Load Diff
650
libs/tornado/websocket.py
Normal file
650
libs/tornado/websocket.py
Normal file
@@ -0,0 +1,650 @@
|
||||
"""Server-side implementation of the WebSocket protocol.
|
||||
|
||||
`WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
|
||||
communication between the browser and server.
|
||||
|
||||
.. warning::
|
||||
|
||||
The WebSocket protocol was recently finalized as `RFC 6455
|
||||
<http://tools.ietf.org/html/rfc6455>`_ and is not yet supported in
|
||||
all browsers. Refer to http://caniuse.com/websockets for details
|
||||
on compatibility. In addition, during development the protocol
|
||||
went through several incompatible versions, and some browsers only
|
||||
support older versions. By default this module only supports the
|
||||
latest version of the protocol, but optional support for an older
|
||||
version (known as "draft 76" or "hixie-76") can be enabled by
|
||||
overriding `WebSocketHandler.allow_draft76` (see that method's
|
||||
documentation for caveats).
|
||||
"""
|
||||
# Author: Jacob Kristhammar, 2010
|
||||
|
||||
import array
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
import struct
|
||||
import time
|
||||
import base64
|
||||
import tornado.escape
|
||||
import tornado.web
|
||||
|
||||
from tornado.util import bytes_type, b
|
||||
|
||||
class WebSocketHandler(tornado.web.RequestHandler):
|
||||
"""Subclass this class to create a basic WebSocket handler.
|
||||
|
||||
Override on_message to handle incoming messages. You can also override
|
||||
open and on_close to handle opened and closed connections.
|
||||
|
||||
See http://dev.w3.org/html5/websockets/ for details on the
|
||||
JavaScript interface. The protocol is specified at
|
||||
http://tools.ietf.org/html/rfc6455.
|
||||
|
||||
Here is an example Web Socket handler that echos back all received messages
|
||||
back to the client::
|
||||
|
||||
class EchoWebSocket(websocket.WebSocketHandler):
|
||||
def open(self):
|
||||
print "WebSocket opened"
|
||||
|
||||
def on_message(self, message):
|
||||
self.write_message(u"You said: " + message)
|
||||
|
||||
def on_close(self):
|
||||
print "WebSocket closed"
|
||||
|
||||
Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
|
||||
but after the handshake, the protocol is message-based. Consequently,
|
||||
most of the Tornado HTTP facilities are not available in handlers of this
|
||||
type. The only communication methods available to you are write_message()
|
||||
and close(). Likewise, your request handler class should
|
||||
implement open() method rather than get() or post().
|
||||
|
||||
If you map the handler above to "/websocket" in your application, you can
|
||||
invoke it in JavaScript with::
|
||||
|
||||
var ws = new WebSocket("ws://localhost:8888/websocket");
|
||||
ws.onopen = function() {
|
||||
ws.send("Hello, world");
|
||||
};
|
||||
ws.onmessage = function (evt) {
|
||||
alert(evt.data);
|
||||
};
|
||||
|
||||
This script pops up an alert box that says "You said: Hello, world".
|
||||
"""
|
||||
def __init__(self, application, request, **kwargs):
|
||||
tornado.web.RequestHandler.__init__(self, application, request,
|
||||
**kwargs)
|
||||
self.stream = request.connection.stream
|
||||
self.ws_connection = None
|
||||
|
||||
def _execute(self, transforms, *args, **kwargs):
|
||||
self.open_args = args
|
||||
self.open_kwargs = kwargs
|
||||
|
||||
# Websocket only supports GET method
|
||||
if self.request.method != 'GET':
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 405 Method Not Allowed\r\n\r\n"
|
||||
))
|
||||
self.stream.close()
|
||||
return
|
||||
|
||||
# Upgrade header should be present and should be equal to WebSocket
|
||||
if self.request.headers.get("Upgrade", "").lower() != 'websocket':
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 400 Bad Request\r\n\r\n"
|
||||
"Can \"Upgrade\" only to \"WebSocket\"."
|
||||
))
|
||||
self.stream.close()
|
||||
return
|
||||
|
||||
# Connection header should be upgrade. Some proxy servers/load balancers
|
||||
# might mess with it.
|
||||
headers = self.request.headers
|
||||
connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
|
||||
if 'upgrade' not in connection:
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 400 Bad Request\r\n\r\n"
|
||||
"\"Connection\" must be \"Upgrade\"."
|
||||
))
|
||||
self.stream.close()
|
||||
return
|
||||
|
||||
# The difference between version 8 and 13 is that in 8 the
|
||||
# client sends a "Sec-Websocket-Origin" header and in 13 it's
|
||||
# simply "Origin".
|
||||
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
|
||||
self.ws_connection = WebSocketProtocol13(self)
|
||||
self.ws_connection.accept_connection()
|
||||
elif (self.allow_draft76() and
|
||||
"Sec-WebSocket-Version" not in self.request.headers):
|
||||
self.ws_connection = WebSocketProtocol76(self)
|
||||
self.ws_connection.accept_connection()
|
||||
else:
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 426 Upgrade Required\r\n"
|
||||
"Sec-WebSocket-Version: 8\r\n\r\n"))
|
||||
self.stream.close()
|
||||
|
||||
def write_message(self, message, binary=False):
|
||||
"""Sends the given message to the client of this Web Socket.
|
||||
|
||||
The message may be either a string or a dict (which will be
|
||||
encoded as json). If the ``binary`` argument is false, the
|
||||
message will be sent as utf8; in binary mode any byte string
|
||||
is allowed.
|
||||
"""
|
||||
if isinstance(message, dict):
|
||||
message = tornado.escape.json_encode(message)
|
||||
self.ws_connection.write_message(message, binary=binary)
|
||||
|
||||
def select_subprotocol(self, subprotocols):
|
||||
"""Invoked when a new WebSocket requests specific subprotocols.
|
||||
|
||||
``subprotocols`` is a list of strings identifying the
|
||||
subprotocols proposed by the client. This method may be
|
||||
overridden to return one of those strings to select it, or
|
||||
``None`` to not select a subprotocol. Failure to select a
|
||||
subprotocol does not automatically abort the connection,
|
||||
although clients may close the connection if none of their
|
||||
proposed subprotocols was selected.
|
||||
"""
|
||||
return None
|
||||
|
||||
def open(self):
|
||||
"""Invoked when a new WebSocket is opened.
|
||||
|
||||
The arguments to `open` are extracted from the `tornado.web.URLSpec`
|
||||
regular expression, just like the arguments to
|
||||
`tornado.web.RequestHandler.get`.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_message(self, message):
|
||||
"""Handle incoming messages on the WebSocket
|
||||
|
||||
This method must be overridden.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_close(self):
|
||||
"""Invoked when the WebSocket is closed."""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""Closes this Web Socket.
|
||||
|
||||
Once the close handshake is successful the socket will be closed.
|
||||
"""
|
||||
self.ws_connection.close()
|
||||
|
||||
def allow_draft76(self):
|
||||
"""Override to enable support for the older "draft76" protocol.
|
||||
|
||||
The draft76 version of the websocket protocol is disabled by
|
||||
default due to security concerns, but it can be enabled by
|
||||
overriding this method to return True.
|
||||
|
||||
Connections using the draft76 protocol do not support the
|
||||
``binary=True`` flag to `write_message`.
|
||||
|
||||
Support for the draft76 protocol is deprecated and will be
|
||||
removed in a future version of Tornado.
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_websocket_scheme(self):
|
||||
"""Return the url scheme used for this request, either "ws" or "wss".
|
||||
|
||||
This is normally decided by HTTPServer, but applications
|
||||
may wish to override this if they are using an SSL proxy
|
||||
that does not provide the X-Scheme header as understood
|
||||
by HTTPServer.
|
||||
|
||||
Note that this is only used by the draft76 protocol.
|
||||
"""
|
||||
return "wss" if self.request.protocol == "https" else "ws"
|
||||
|
||||
def async_callback(self, callback, *args, **kwargs):
|
||||
"""Wrap callbacks with this if they are used on asynchronous requests.
|
||||
|
||||
Catches exceptions properly and closes this WebSocket if an exception
|
||||
is uncaught. (Note that this is usually unnecessary thanks to
|
||||
`tornado.stack_context`)
|
||||
"""
|
||||
return self.ws_connection.async_callback(callback, *args, **kwargs)
|
||||
|
||||
def _not_supported(self, *args, **kwargs):
|
||||
raise Exception("Method not supported for Web Sockets")
|
||||
|
||||
def on_connection_close(self):
|
||||
if self.ws_connection:
|
||||
self.ws_connection.on_connection_close()
|
||||
self.ws_connection = None
|
||||
self.on_close()
|
||||
|
||||
|
||||
for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
|
||||
"set_status", "flush", "finish"]:
|
||||
setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
|
||||
|
||||
|
||||
class WebSocketProtocol(object):
|
||||
"""Base class for WebSocket protocol versions.
|
||||
"""
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.request = handler.request
|
||||
self.stream = handler.stream
|
||||
self.client_terminated = False
|
||||
self.server_terminated = False
|
||||
|
||||
def async_callback(self, callback, *args, **kwargs):
|
||||
"""Wrap callbacks with this if they are used on asynchronous requests.
|
||||
|
||||
Catches exceptions properly and closes this WebSocket if an exception
|
||||
is uncaught.
|
||||
"""
|
||||
if args or kwargs:
|
||||
callback = functools.partial(callback, *args, **kwargs)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return callback(*args, **kwargs)
|
||||
except Exception:
|
||||
logging.error("Uncaught exception in %s",
|
||||
self.request.path, exc_info=True)
|
||||
self._abort()
|
||||
return wrapper
|
||||
|
||||
def on_connection_close(self):
|
||||
self._abort()
|
||||
|
||||
def _abort(self):
|
||||
"""Instantly aborts the WebSocket connection by closing the socket"""
|
||||
self.client_terminated = True
|
||||
self.server_terminated = True
|
||||
self.stream.close() # forcibly tear down the connection
|
||||
self.close() # let the subclass cleanup
|
||||
|
||||
|
||||
class WebSocketProtocol76(WebSocketProtocol):
|
||||
"""Implementation of the WebSockets protocol, version hixie-76.
|
||||
|
||||
This class provides basic functionality to process WebSockets requests as
|
||||
specified in
|
||||
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
"""
|
||||
def __init__(self, handler):
|
||||
WebSocketProtocol.__init__(self, handler)
|
||||
self.challenge = None
|
||||
self._waiting = None
|
||||
|
||||
def accept_connection(self):
|
||||
try:
|
||||
self._handle_websocket_headers()
|
||||
except ValueError:
|
||||
logging.debug("Malformed WebSocket request received")
|
||||
self._abort()
|
||||
return
|
||||
|
||||
scheme = self.handler.get_websocket_scheme()
|
||||
|
||||
# draft76 only allows a single subprotocol
|
||||
subprotocol_header = ''
|
||||
subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
|
||||
if subprotocol:
|
||||
selected = self.handler.select_subprotocol([subprotocol])
|
||||
if selected:
|
||||
assert selected == subprotocol
|
||||
subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
|
||||
|
||||
# Write the initial headers before attempting to read the challenge.
|
||||
# This is necessary when using proxies (such as HAProxy), which
|
||||
# need to see the Upgrade headers before passing through the
|
||||
# non-HTTP traffic that follows.
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
|
||||
"Upgrade: WebSocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Server: TornadoServer/%(version)s\r\n"
|
||||
"Sec-WebSocket-Origin: %(origin)s\r\n"
|
||||
"Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
|
||||
"%(subprotocol)s"
|
||||
"\r\n" % (dict(
|
||||
version=tornado.version,
|
||||
origin=self.request.headers["Origin"],
|
||||
scheme=scheme,
|
||||
host=self.request.host,
|
||||
uri=self.request.uri,
|
||||
subprotocol=subprotocol_header))))
|
||||
self.stream.read_bytes(8, self._handle_challenge)
|
||||
|
||||
def challenge_response(self, challenge):
|
||||
"""Generates the challenge response that's needed in the handshake
|
||||
|
||||
The challenge parameter should be the raw bytes as sent from the
|
||||
client.
|
||||
"""
|
||||
key_1 = self.request.headers.get("Sec-Websocket-Key1")
|
||||
key_2 = self.request.headers.get("Sec-Websocket-Key2")
|
||||
try:
|
||||
part_1 = self._calculate_part(key_1)
|
||||
part_2 = self._calculate_part(key_2)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid Keys/Challenge")
|
||||
return self._generate_challenge_response(part_1, part_2, challenge)
|
||||
|
||||
def _handle_challenge(self, challenge):
|
||||
try:
|
||||
challenge_response = self.challenge_response(challenge)
|
||||
except ValueError:
|
||||
logging.debug("Malformed key data in WebSocket request")
|
||||
self._abort()
|
||||
return
|
||||
self._write_response(challenge_response)
|
||||
|
||||
def _write_response(self, challenge):
|
||||
self.stream.write(challenge)
|
||||
self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
|
||||
self._receive_message()
|
||||
|
||||
def _handle_websocket_headers(self):
|
||||
"""Verifies all invariant- and required headers
|
||||
|
||||
If a header is missing or have an incorrect value ValueError will be
|
||||
raised
|
||||
"""
|
||||
fields = ("Origin", "Host", "Sec-Websocket-Key1",
|
||||
"Sec-Websocket-Key2")
|
||||
if not all(map(lambda f: self.request.headers.get(f), fields)):
|
||||
raise ValueError("Missing/Invalid WebSocket headers")
|
||||
|
||||
def _calculate_part(self, key):
|
||||
"""Processes the key headers and calculates their key value.
|
||||
|
||||
Raises ValueError when feed invalid key."""
|
||||
number = int(''.join(c for c in key if c.isdigit()))
|
||||
spaces = len([c for c in key if c.isspace()])
|
||||
try:
|
||||
key_number = number // spaces
|
||||
except (ValueError, ZeroDivisionError):
|
||||
raise ValueError
|
||||
return struct.pack(">I", key_number)
|
||||
|
||||
def _generate_challenge_response(self, part_1, part_2, part_3):
|
||||
m = hashlib.md5()
|
||||
m.update(part_1)
|
||||
m.update(part_2)
|
||||
m.update(part_3)
|
||||
return m.digest()
|
||||
|
||||
def _receive_message(self):
|
||||
self.stream.read_bytes(1, self._on_frame_type)
|
||||
|
||||
def _on_frame_type(self, byte):
|
||||
frame_type = ord(byte)
|
||||
if frame_type == 0x00:
|
||||
self.stream.read_until(b("\xff"), self._on_end_delimiter)
|
||||
elif frame_type == 0xff:
|
||||
self.stream.read_bytes(1, self._on_length_indicator)
|
||||
else:
|
||||
self._abort()
|
||||
|
||||
def _on_end_delimiter(self, frame):
|
||||
if not self.client_terminated:
|
||||
self.async_callback(self.handler.on_message)(
|
||||
frame[:-1].decode("utf-8", "replace"))
|
||||
if not self.client_terminated:
|
||||
self._receive_message()
|
||||
|
||||
def _on_length_indicator(self, byte):
|
||||
if ord(byte) != 0x00:
|
||||
self._abort()
|
||||
return
|
||||
self.client_terminated = True
|
||||
self.close()
|
||||
|
||||
def write_message(self, message, binary=False):
|
||||
"""Sends the given message to the client of this Web Socket."""
|
||||
if binary:
|
||||
raise ValueError(
|
||||
"Binary messages not supported by this version of websockets")
|
||||
if isinstance(message, unicode):
|
||||
message = message.encode("utf-8")
|
||||
assert isinstance(message, bytes_type)
|
||||
self.stream.write(b("\x00") + message + b("\xff"))
|
||||
|
||||
def close(self):
|
||||
"""Closes the WebSocket connection."""
|
||||
if not self.server_terminated:
|
||||
if not self.stream.closed():
|
||||
self.stream.write("\xff\x00")
|
||||
self.server_terminated = True
|
||||
if self.client_terminated:
|
||||
if self._waiting is not None:
|
||||
self.stream.io_loop.remove_timeout(self._waiting)
|
||||
self._waiting = None
|
||||
self.stream.close()
|
||||
elif self._waiting is None:
|
||||
self._waiting = self.stream.io_loop.add_timeout(
|
||||
time.time() + 5, self._abort)
|
||||
|
||||
|
||||
class WebSocketProtocol13(WebSocketProtocol):
|
||||
"""Implementation of the WebSocket protocol from RFC 6455.
|
||||
|
||||
This class supports versions 7 and 8 of the protocol in addition to the
|
||||
final version 13.
|
||||
"""
|
||||
def __init__(self, handler):
|
||||
WebSocketProtocol.__init__(self, handler)
|
||||
self._final_frame = False
|
||||
self._frame_opcode = None
|
||||
self._frame_mask = None
|
||||
self._frame_length = None
|
||||
self._fragmented_message_buffer = None
|
||||
self._fragmented_message_opcode = None
|
||||
self._waiting = None
|
||||
|
||||
def accept_connection(self):
|
||||
try:
|
||||
self._handle_websocket_headers()
|
||||
self._accept_connection()
|
||||
except ValueError:
|
||||
logging.debug("Malformed WebSocket request received")
|
||||
self._abort()
|
||||
return
|
||||
|
||||
def _handle_websocket_headers(self):
|
||||
"""Verifies all invariant- and required headers
|
||||
|
||||
If a header is missing or have an incorrect value ValueError will be
|
||||
raised
|
||||
"""
|
||||
fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
|
||||
if not all(map(lambda f: self.request.headers.get(f), fields)):
|
||||
raise ValueError("Missing/Invalid WebSocket headers")
|
||||
|
||||
def _challenge_response(self):
|
||||
sha1 = hashlib.sha1()
|
||||
sha1.update(tornado.escape.utf8(
|
||||
self.request.headers.get("Sec-Websocket-Key")))
|
||||
sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
|
||||
return tornado.escape.native_str(base64.b64encode(sha1.digest()))
|
||||
|
||||
def _accept_connection(self):
|
||||
subprotocol_header = ''
|
||||
subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '')
|
||||
subprotocols = [s.strip() for s in subprotocols.split(',')]
|
||||
if subprotocols:
|
||||
selected = self.handler.select_subprotocol(subprotocols)
|
||||
if selected:
|
||||
assert selected in subprotocols
|
||||
subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
|
||||
|
||||
self.stream.write(tornado.escape.utf8(
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: %s\r\n"
|
||||
"%s"
|
||||
"\r\n" % (self._challenge_response(), subprotocol_header)))
|
||||
|
||||
self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
|
||||
self._receive_frame()
|
||||
|
||||
def _write_frame(self, fin, opcode, data):
|
||||
if fin:
|
||||
finbit = 0x80
|
||||
else:
|
||||
finbit = 0
|
||||
frame = struct.pack("B", finbit | opcode)
|
||||
l = len(data)
|
||||
if l < 126:
|
||||
frame += struct.pack("B", l)
|
||||
elif l <= 0xFFFF:
|
||||
frame += struct.pack("!BH", 126, l)
|
||||
else:
|
||||
frame += struct.pack("!BQ", 127, l)
|
||||
frame += data
|
||||
self.stream.write(frame)
|
||||
|
||||
def write_message(self, message, binary=False):
|
||||
"""Sends the given message to the client of this Web Socket."""
|
||||
if binary:
|
||||
opcode = 0x2
|
||||
else:
|
||||
opcode = 0x1
|
||||
message = tornado.escape.utf8(message)
|
||||
assert isinstance(message, bytes_type)
|
||||
self._write_frame(True, opcode, message)
|
||||
|
||||
def _receive_frame(self):
|
||||
self.stream.read_bytes(2, self._on_frame_start)
|
||||
|
||||
def _on_frame_start(self, data):
|
||||
header, payloadlen = struct.unpack("BB", data)
|
||||
self._final_frame = header & 0x80
|
||||
reserved_bits = header & 0x70
|
||||
self._frame_opcode = header & 0xf
|
||||
self._frame_opcode_is_control = self._frame_opcode & 0x8
|
||||
if reserved_bits:
|
||||
# client is using as-yet-undefined extensions; abort
|
||||
self._abort()
|
||||
return
|
||||
if not (payloadlen & 0x80):
|
||||
# Unmasked frame -> abort connection
|
||||
self._abort()
|
||||
return
|
||||
payloadlen = payloadlen & 0x7f
|
||||
if self._frame_opcode_is_control and payloadlen >= 126:
|
||||
# control frames must have payload < 126
|
||||
self._abort()
|
||||
return
|
||||
if payloadlen < 126:
|
||||
self._frame_length = payloadlen
|
||||
self.stream.read_bytes(4, self._on_masking_key)
|
||||
elif payloadlen == 126:
|
||||
self.stream.read_bytes(2, self._on_frame_length_16)
|
||||
elif payloadlen == 127:
|
||||
self.stream.read_bytes(8, self._on_frame_length_64)
|
||||
|
||||
def _on_frame_length_16(self, data):
|
||||
self._frame_length = struct.unpack("!H", data)[0];
|
||||
self.stream.read_bytes(4, self._on_masking_key);
|
||||
|
||||
def _on_frame_length_64(self, data):
|
||||
self._frame_length = struct.unpack("!Q", data)[0];
|
||||
self.stream.read_bytes(4, self._on_masking_key);
|
||||
|
||||
def _on_masking_key(self, data):
|
||||
self._frame_mask = array.array("B", data)
|
||||
self.stream.read_bytes(self._frame_length, self._on_frame_data)
|
||||
|
||||
def _on_frame_data(self, data):
|
||||
unmasked = array.array("B", data)
|
||||
for i in xrange(len(data)):
|
||||
unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4]
|
||||
|
||||
if self._frame_opcode_is_control:
|
||||
# control frames may be interleaved with a series of fragmented
|
||||
# data frames, so control frames must not interact with
|
||||
# self._fragmented_*
|
||||
if not self._final_frame:
|
||||
# control frames must not be fragmented
|
||||
self._abort()
|
||||
return
|
||||
opcode = self._frame_opcode
|
||||
elif self._frame_opcode == 0: # continuation frame
|
||||
if self._fragmented_message_buffer is None:
|
||||
# nothing to continue
|
||||
self._abort()
|
||||
return
|
||||
self._fragmented_message_buffer += unmasked
|
||||
if self._final_frame:
|
||||
opcode = self._fragmented_message_opcode
|
||||
unmasked = self._fragmented_message_buffer
|
||||
self._fragmented_message_buffer = None
|
||||
else: # start of new data message
|
||||
if self._fragmented_message_buffer is not None:
|
||||
# can't start new message until the old one is finished
|
||||
self._abort()
|
||||
return
|
||||
if self._final_frame:
|
||||
opcode = self._frame_opcode
|
||||
else:
|
||||
self._fragmented_message_opcode = self._frame_opcode
|
||||
self._fragmented_message_buffer = unmasked
|
||||
|
||||
if self._final_frame:
|
||||
self._handle_message(opcode, unmasked.tostring())
|
||||
|
||||
if not self.client_terminated:
|
||||
self._receive_frame()
|
||||
|
||||
|
||||
def _handle_message(self, opcode, data):
|
||||
if self.client_terminated: return
|
||||
|
||||
if opcode == 0x1:
|
||||
# UTF-8 data
|
||||
try:
|
||||
decoded = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
self._abort()
|
||||
return
|
||||
self.async_callback(self.handler.on_message)(decoded)
|
||||
elif opcode == 0x2:
|
||||
# Binary data
|
||||
self.async_callback(self.handler.on_message)(data)
|
||||
elif opcode == 0x8:
|
||||
# Close
|
||||
self.client_terminated = True
|
||||
self.close()
|
||||
elif opcode == 0x9:
|
||||
# Ping
|
||||
self._write_frame(True, 0xA, data)
|
||||
elif opcode == 0xA:
|
||||
# Pong
|
||||
pass
|
||||
else:
|
||||
self._abort()
|
||||
|
||||
def close(self):
|
||||
"""Closes the WebSocket connection."""
|
||||
if not self.server_terminated:
|
||||
if not self.stream.closed():
|
||||
self._write_frame(True, 0x8, b(""))
|
||||
self.server_terminated = True
|
||||
if self.client_terminated:
|
||||
if self._waiting is not None:
|
||||
self.stream.io_loop.remove_timeout(self._waiting)
|
||||
self._waiting = None
|
||||
self.stream.close()
|
||||
elif self._waiting is None:
|
||||
# Give the client a few seconds to complete a clean shutdown,
|
||||
# otherwise just close the connection.
|
||||
self._waiting = self.stream.io_loop.add_timeout(
|
||||
time.time() + 5, self._abort)
|
||||
296
libs/tornado/wsgi.py
Normal file
296
libs/tornado/wsgi.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""WSGI support for the Tornado web framework.
|
||||
|
||||
WSGI is the Python standard for web servers, and allows for interoperability
|
||||
between Tornado and other Python web frameworks and servers. This module
|
||||
provides WSGI support in two ways:
|
||||
|
||||
* `WSGIApplication` is a version of `tornado.web.Application` that can run
|
||||
inside a WSGI server. This is useful for running a Tornado app on another
|
||||
HTTP server, such as Google App Engine. See the `WSGIApplication` class
|
||||
documentation for limitations that apply.
|
||||
* `WSGIContainer` lets you run other WSGI applications and frameworks on the
|
||||
Tornado HTTP server. For example, with this class you can mix Django
|
||||
and Tornado handlers in a single server.
|
||||
"""
|
||||
|
||||
import Cookie
|
||||
import cgi
|
||||
import httplib
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import tornado
|
||||
import urllib
|
||||
|
||||
from tornado import escape
|
||||
from tornado import httputil
|
||||
from tornado import web
|
||||
from tornado.escape import native_str, utf8
|
||||
from tornado.util import b
|
||||
|
||||
try:
|
||||
from io import BytesIO # python 3
|
||||
except ImportError:
|
||||
from cStringIO import StringIO as BytesIO # python 2
|
||||
|
||||
class WSGIApplication(web.Application):
|
||||
"""A WSGI equivalent of `tornado.web.Application`.
|
||||
|
||||
WSGIApplication is very similar to web.Application, except no
|
||||
asynchronous methods are supported (since WSGI does not support
|
||||
non-blocking requests properly). If you call self.flush() or other
|
||||
asynchronous methods in your request handlers running in a
|
||||
WSGIApplication, we throw an exception.
|
||||
|
||||
Example usage::
|
||||
|
||||
import tornado.web
|
||||
import tornado.wsgi
|
||||
import wsgiref.simple_server
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler):
|
||||
def get(self):
|
||||
self.write("Hello, world")
|
||||
|
||||
if __name__ == "__main__":
|
||||
application = tornado.wsgi.WSGIApplication([
|
||||
(r"/", MainHandler),
|
||||
])
|
||||
server = wsgiref.simple_server.make_server('', 8888, application)
|
||||
server.serve_forever()
|
||||
|
||||
See the 'appengine' demo for an example of using this module to run
|
||||
a Tornado app on Google AppEngine.
|
||||
|
||||
Since no asynchronous methods are available for WSGI applications, the
|
||||
httpclient and auth modules are both not available for WSGI applications.
|
||||
We support the same interface, but handlers running in a WSGIApplication
|
||||
do not support flush() or asynchronous methods.
|
||||
"""
|
||||
def __init__(self, handlers=None, default_host="", **settings):
|
||||
web.Application.__init__(self, handlers, default_host, transforms=[],
|
||||
wsgi=True, **settings)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
handler = web.Application.__call__(self, HTTPRequest(environ))
|
||||
assert handler._finished
|
||||
status = str(handler._status_code) + " " + \
|
||||
httplib.responses[handler._status_code]
|
||||
headers = handler._headers.items()
|
||||
for cookie_dict in getattr(handler, "_new_cookies", []):
|
||||
for cookie in cookie_dict.values():
|
||||
headers.append(("Set-Cookie", cookie.OutputString(None)))
|
||||
start_response(status,
|
||||
[(native_str(k), native_str(v)) for (k,v) in headers])
|
||||
return handler._write_buffer
|
||||
|
||||
|
||||
class HTTPRequest(object):
|
||||
"""Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
|
||||
def __init__(self, environ):
|
||||
"""Parses the given WSGI environ to construct the request."""
|
||||
self.method = environ["REQUEST_METHOD"]
|
||||
self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
|
||||
self.path += urllib.quote(environ.get("PATH_INFO", ""))
|
||||
self.uri = self.path
|
||||
self.arguments = {}
|
||||
self.query = environ.get("QUERY_STRING", "")
|
||||
if self.query:
|
||||
self.uri += "?" + self.query
|
||||
arguments = cgi.parse_qs(self.query)
|
||||
for name, values in arguments.iteritems():
|
||||
values = [v for v in values if v]
|
||||
if values: self.arguments[name] = values
|
||||
self.version = "HTTP/1.1"
|
||||
self.headers = httputil.HTTPHeaders()
|
||||
if environ.get("CONTENT_TYPE"):
|
||||
self.headers["Content-Type"] = environ["CONTENT_TYPE"]
|
||||
if environ.get("CONTENT_LENGTH"):
|
||||
self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
|
||||
for key in environ:
|
||||
if key.startswith("HTTP_"):
|
||||
self.headers[key[5:].replace("_", "-")] = environ[key]
|
||||
if self.headers.get("Content-Length"):
|
||||
self.body = environ["wsgi.input"].read(
|
||||
int(self.headers["Content-Length"]))
|
||||
else:
|
||||
self.body = ""
|
||||
self.protocol = environ["wsgi.url_scheme"]
|
||||
self.remote_ip = environ.get("REMOTE_ADDR", "")
|
||||
if environ.get("HTTP_HOST"):
|
||||
self.host = environ["HTTP_HOST"]
|
||||
else:
|
||||
self.host = environ["SERVER_NAME"]
|
||||
|
||||
# Parse request body
|
||||
self.files = {}
|
||||
content_type = self.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/x-www-form-urlencoded"):
|
||||
for name, values in cgi.parse_qs(self.body).iteritems():
|
||||
self.arguments.setdefault(name, []).extend(values)
|
||||
elif content_type.startswith("multipart/form-data"):
|
||||
if 'boundary=' in content_type:
|
||||
boundary = content_type.split('boundary=',1)[1]
|
||||
if boundary:
|
||||
httputil.parse_multipart_form_data(
|
||||
utf8(boundary), self.body, self.arguments, self.files)
|
||||
else:
|
||||
logging.warning("Invalid multipart/form-data")
|
||||
|
||||
self._start_time = time.time()
|
||||
self._finish_time = None
|
||||
|
||||
def supports_http_1_1(self):
|
||||
"""Returns True if this request supports HTTP/1.1 semantics"""
|
||||
return self.version == "HTTP/1.1"
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
"""A dictionary of Cookie.Morsel objects."""
|
||||
if not hasattr(self, "_cookies"):
|
||||
self._cookies = Cookie.SimpleCookie()
|
||||
if "Cookie" in self.headers:
|
||||
try:
|
||||
self._cookies.load(
|
||||
native_str(self.headers["Cookie"]))
|
||||
except Exception:
|
||||
self._cookies = None
|
||||
return self._cookies
|
||||
|
||||
def full_url(self):
|
||||
"""Reconstructs the full URL for this request."""
|
||||
return self.protocol + "://" + self.host + self.uri
|
||||
|
||||
def request_time(self):
|
||||
"""Returns the amount of time it took for this request to execute."""
|
||||
if self._finish_time is None:
|
||||
return time.time() - self._start_time
|
||||
else:
|
||||
return self._finish_time - self._start_time
|
||||
|
||||
|
||||
class WSGIContainer(object):
|
||||
r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
|
||||
|
||||
Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
|
||||
run it. For example::
|
||||
|
||||
def simple_app(environ, start_response):
|
||||
status = "200 OK"
|
||||
response_headers = [("Content-type", "text/plain")]
|
||||
start_response(status, response_headers)
|
||||
return ["Hello world!\n"]
|
||||
|
||||
container = tornado.wsgi.WSGIContainer(simple_app)
|
||||
http_server = tornado.httpserver.HTTPServer(container)
|
||||
http_server.listen(8888)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
This class is intended to let other frameworks (Django, web.py, etc)
|
||||
run on the Tornado HTTP server and I/O loop.
|
||||
|
||||
The `tornado.web.FallbackHandler` class is often useful for mixing
|
||||
Tornado and WSGI apps in the same server. See
|
||||
https://github.com/bdarnell/django-tornado-demo for a complete example.
|
||||
"""
|
||||
def __init__(self, wsgi_application):
|
||||
self.wsgi_application = wsgi_application
|
||||
|
||||
def __call__(self, request):
|
||||
data = {}
|
||||
response = []
|
||||
def start_response(status, response_headers, exc_info=None):
|
||||
data["status"] = status
|
||||
data["headers"] = response_headers
|
||||
return response.append
|
||||
app_response = self.wsgi_application(
|
||||
WSGIContainer.environ(request), start_response)
|
||||
response.extend(app_response)
|
||||
body = b("").join(response)
|
||||
if hasattr(app_response, "close"):
|
||||
app_response.close()
|
||||
if not data: raise Exception("WSGI app did not call start_response")
|
||||
|
||||
status_code = int(data["status"].split()[0])
|
||||
headers = data["headers"]
|
||||
header_set = set(k.lower() for (k,v) in headers)
|
||||
body = escape.utf8(body)
|
||||
if "content-length" not in header_set:
|
||||
headers.append(("Content-Length", str(len(body))))
|
||||
if "content-type" not in header_set:
|
||||
headers.append(("Content-Type", "text/html; charset=UTF-8"))
|
||||
if "server" not in header_set:
|
||||
headers.append(("Server", "TornadoServer/%s" % tornado.version))
|
||||
|
||||
parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
|
||||
for key, value in headers:
|
||||
parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
|
||||
parts.append(b("\r\n"))
|
||||
parts.append(body)
|
||||
request.write(b("").join(parts))
|
||||
request.finish()
|
||||
self._log(status_code, request)
|
||||
|
||||
@staticmethod
|
||||
def environ(request):
|
||||
"""Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
|
||||
"""
|
||||
hostport = request.host.split(":")
|
||||
if len(hostport) == 2:
|
||||
host = hostport[0]
|
||||
port = int(hostport[1])
|
||||
else:
|
||||
host = request.host
|
||||
port = 443 if request.protocol == "https" else 80
|
||||
environ = {
|
||||
"REQUEST_METHOD": request.method,
|
||||
"SCRIPT_NAME": "",
|
||||
"PATH_INFO": urllib.unquote(request.path),
|
||||
"QUERY_STRING": request.query,
|
||||
"REMOTE_ADDR": request.remote_ip,
|
||||
"SERVER_NAME": host,
|
||||
"SERVER_PORT": str(port),
|
||||
"SERVER_PROTOCOL": request.version,
|
||||
"wsgi.version": (1, 0),
|
||||
"wsgi.url_scheme": request.protocol,
|
||||
"wsgi.input": BytesIO(escape.utf8(request.body)),
|
||||
"wsgi.errors": sys.stderr,
|
||||
"wsgi.multithread": False,
|
||||
"wsgi.multiprocess": True,
|
||||
"wsgi.run_once": False,
|
||||
}
|
||||
if "Content-Type" in request.headers:
|
||||
environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
|
||||
if "Content-Length" in request.headers:
|
||||
environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
|
||||
for key, value in request.headers.iteritems():
|
||||
environ["HTTP_" + key.replace("-", "_").upper()] = value
|
||||
return environ
|
||||
|
||||
def _log(self, status_code, request):
|
||||
if status_code < 400:
|
||||
log_method = logging.info
|
||||
elif status_code < 500:
|
||||
log_method = logging.warning
|
||||
else:
|
||||
log_method = logging.error
|
||||
request_time = 1000.0 * request.request_time()
|
||||
summary = request.method + " " + request.uri + " (" + \
|
||||
request.remote_ip + ")"
|
||||
log_method("%d %s %.2fms", status_code, summary, request_time)
|
||||
Reference in New Issue
Block a user