Dropping trailing whitespaces

This commit is contained in:
David
2014-08-06 12:46:26 +02:00
parent 78c866ce59
commit 0c61eb11f5
91 changed files with 3139 additions and 3139 deletions
+9 -9
View File
@@ -39,11 +39,11 @@ def switch(AlbumID, ReleaseID):
"ReleaseCountry": newalbumdata['ReleaseCountry'], "ReleaseCountry": newalbumdata['ReleaseCountry'],
"ReleaseFormat": newalbumdata['ReleaseFormat'] "ReleaseFormat": newalbumdata['ReleaseFormat']
} }
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
for track in newtrackdata: for track in newtrackdata:
controlValueDict = {"TrackID": track['TrackID'], controlValueDict = {"TrackID": track['TrackID'],
"AlbumID": AlbumID} "AlbumID": AlbumID}
@@ -60,23 +60,23 @@ def switch(AlbumID, ReleaseID):
"Format": track['Format'], "Format": track['Format'],
"BitRate": track['BitRate'] "BitRate": track['BitRate']
} }
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album # Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
total_track_count = len(newtrackdata) total_track_count = len(newtrackdata)
have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID])) have_track_count = len(myDB.select('SELECT * from tracks WHERE AlbumID=? AND Location IS NOT NULL', [AlbumID]))
if oldalbumdata['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)): if oldalbumdata['Status'] == 'Skipped' and ((have_track_count/float(total_track_count)) >= (headphones.ALBUM_COMPLETION_PCT/100.0)):
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID]) myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
# Update have track counts on index # Update have track counts on index
totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']])) totaltracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND AlbumID IN (SELECT AlbumID FROM albums WHERE Status != "Ignored")', [newalbumdata['ArtistID']]))
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']]))
controlValueDict = {"ArtistID": newalbumdata['ArtistID']} controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
newValueDict = { "TotalTracks": totaltracks, newValueDict = { "TotalTracks": totaltracks,
"HaveTracks": havetracks} "HaveTracks": havetracks}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
+88 -88
View File
@@ -21,29 +21,29 @@ import lib.simplejson as simplejson
from xml.dom.minidom import Document from xml.dom.minidom import Document
import copy import copy
cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs', cmd_list = [ 'getIndex', 'getArtist', 'getAlbum', 'getUpcoming', 'getWanted', 'getSimilar', 'getHistory', 'getLogs',
'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist', 'findArtist', 'findAlbum', 'addArtist', 'delArtist', 'pauseArtist', 'resumeArtist', 'refreshArtist',
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub', 'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo', 'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release'] 'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release']
class Api(object): class Api(object):
def __init__(self): def __init__(self):
self.apikey = None self.apikey = None
self.cmd = None self.cmd = None
self.id = None self.id = None
self.kwargs = None self.kwargs = None
self.data = None self.data = None
self.callback = None self.callback = None
def checkParams(self,*args,**kwargs): def checkParams(self,*args,**kwargs):
if not headphones.API_ENABLED: if not headphones.API_ENABLED:
self.data = 'API not enabled' self.data = 'API not enabled'
return return
@@ -53,32 +53,32 @@ class Api(object):
if len(headphones.API_KEY) != 32: if len(headphones.API_KEY) != 32:
self.data = 'API key not generated correctly' self.data = 'API key not generated correctly'
return return
if 'apikey' not in kwargs: if 'apikey' not in kwargs:
self.data = 'Missing api key' self.data = 'Missing api key'
return return
if kwargs['apikey'] != headphones.API_KEY: if kwargs['apikey'] != headphones.API_KEY:
self.data = 'Incorrect API key' self.data = 'Incorrect API key'
return return
else: else:
self.apikey = kwargs.pop('apikey') self.apikey = kwargs.pop('apikey')
if 'cmd' not in kwargs: if 'cmd' not in kwargs:
self.data = 'Missing parameter: cmd' self.data = 'Missing parameter: cmd'
return return
if kwargs['cmd'] not in cmd_list: if kwargs['cmd'] not in cmd_list:
self.data = 'Unknown command: %s' % kwargs['cmd'] self.data = 'Unknown command: %s' % kwargs['cmd']
return return
else: else:
self.cmd = kwargs.pop('cmd') self.cmd = kwargs.pop('cmd')
self.kwargs = kwargs self.kwargs = kwargs
self.data = 'OK' self.data = 'OK'
def fetchData(self): def fetchData(self):
if self.data == 'OK': if self.data == 'OK':
logger.info('Recieved API command: %s', self.cmd) logger.info('Recieved API command: %s', self.cmd)
methodToCall = getattr(self, "_" + self.cmd) methodToCall = getattr(self, "_" + self.cmd)
@@ -95,74 +95,74 @@ class Api(object):
return self.data return self.data
else: else:
return self.data return self.data
def _dic_from_query(self,query): def _dic_from_query(self,query):
myDB = db.DBConnection() myDB = db.DBConnection()
rows = myDB.select(query) rows = myDB.select(query)
rows_as_dic = [] rows_as_dic = []
for row in rows: for row in rows:
row_as_dic = dict(zip(row.keys(), row)) row_as_dic = dict(zip(row.keys(), row))
rows_as_dic.append(row_as_dic) rows_as_dic.append(row_as_dic)
return rows_as_dic return rows_as_dic
def _getIndex(self, **kwargs): def _getIndex(self, **kwargs):
self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE') self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
return return
def _getArtist(self, **kwargs): def _getArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
artist = self._dic_from_query('SELECT * from artists WHERE ArtistID="' + self.id + '"') artist = self._dic_from_query('SELECT * from artists WHERE ArtistID="' + self.id + '"')
albums = self._dic_from_query('SELECT * from albums WHERE ArtistID="' + self.id + '" order by ReleaseDate DESC') albums = self._dic_from_query('SELECT * from albums WHERE ArtistID="' + self.id + '" order by ReleaseDate DESC')
description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"') description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"')
self.data = { 'artist': artist, 'albums': albums, 'description' : description } self.data = { 'artist': artist, 'albums': albums, 'description' : description }
return return
def _getAlbum(self, **kwargs): def _getAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
album = self._dic_from_query('SELECT * from albums WHERE AlbumID="' + self.id + '"') album = self._dic_from_query('SELECT * from albums WHERE AlbumID="' + self.id + '"')
tracks = self._dic_from_query('SELECT * from tracks WHERE AlbumID="' + self.id + '"') tracks = self._dic_from_query('SELECT * from tracks WHERE AlbumID="' + self.id + '"')
description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"') description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"')
self.data = { 'album' : album, 'tracks' : tracks, 'description' : description } self.data = { 'album' : album, 'tracks' : tracks, 'description' : description }
return return
def _getHistory(self, **kwargs): def _getHistory(self, **kwargs):
self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC') self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC')
return return
def _getUpcoming(self, **kwargs): def _getUpcoming(self, **kwargs):
self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC") self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC")
return return
def _getWanted(self, **kwargs): def _getWanted(self, **kwargs):
self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'") self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'")
return return
def _getSimilar(self, **kwargs): def _getSimilar(self, **kwargs):
self.data = self._dic_from_query('SELECT * from lastfmcloud') self.data = self._dic_from_query('SELECT * from lastfmcloud')
return return
def _getLogs(self, **kwargs): def _getLogs(self, **kwargs):
pass pass
def _findArtist(self, **kwargs): def _findArtist(self, **kwargs):
if 'name' not in kwargs: if 'name' not in kwargs:
self.data = 'Missing parameter: name' self.data = 'Missing parameter: name'
@@ -171,7 +171,7 @@ class Api(object):
limit = kwargs['limit'] limit = kwargs['limit']
else: else:
limit=50 limit=50
self.data = mb.findArtist(kwargs['name'], limit) self.data = mb.findArtist(kwargs['name'], limit)
def _findAlbum(self, **kwargs): def _findAlbum(self, **kwargs):
@@ -182,216 +182,216 @@ class Api(object):
limit = kwargs['limit'] limit = kwargs['limit']
else: else:
limit=50 limit=50
self.data = mb.findRelease(kwargs['name'], limit) self.data = mb.findRelease(kwargs['name'], limit)
def _addArtist(self, **kwargs): def _addArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addArtisttoDB(self.id) importer.addArtisttoDB(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _delArtist(self, **kwargs): def _delArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"')
myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"') myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"')
def _pauseArtist(self, **kwargs): def _pauseArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id} controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Paused'} newValueDict = {'Status': 'Paused'}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
def _resumeArtist(self, **kwargs): def _resumeArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ArtistID': self.id} controlValueDict = {'ArtistID': self.id}
newValueDict = {'Status': 'Active'} newValueDict = {'Status': 'Active'}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
def _refreshArtist(self, **kwargs): def _refreshArtist(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addArtisttoDB(self.id) importer.addArtisttoDB(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _addAlbum(self, **kwargs): def _addAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
try: try:
importer.addReleaseById(self.id) importer.addReleaseById(self.id)
except Exception, e: except Exception, e:
self.data = e self.data = e
return return
def _queueAlbum(self, **kwargs): def _queueAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
if 'new' in kwargs: if 'new' in kwargs:
new = kwargs['new'] new = kwargs['new']
else: else:
new = False new = False
if 'lossless' in kwargs: if 'lossless' in kwargs:
lossless = kwargs['lossless'] lossless = kwargs['lossless']
else: else:
lossless = False lossless = False
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id} controlValueDict = {'AlbumID': self.id}
if lossless: if lossless:
newValueDict = {'Status': 'Wanted Lossless'} newValueDict = {'Status': 'Wanted Lossless'}
else: else:
newValueDict = {'Status': 'Wanted'} newValueDict = {'Status': 'Wanted'}
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
searcher.searchforalbum(self.id, new) searcher.searchforalbum(self.id, new)
def _unqueueAlbum(self, **kwargs): def _unqueueAlbum(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'AlbumID': self.id} controlValueDict = {'AlbumID': self.id}
newValueDict = {'Status': 'Skipped'} newValueDict = {'Status': 'Skipped'}
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
def _forceSearch(self, **kwargs): def _forceSearch(self, **kwargs):
searcher.searchforalbum() searcher.searchforalbum()
def _forceProcess(self, **kwargs): def _forceProcess(self, **kwargs):
self.dir = None self.dir = None
if 'dir' in kwargs: if 'dir' in kwargs:
self.dir = kwargs['dir'] self.dir = kwargs['dir']
postprocessor.forcePostProcess(self.dir) postprocessor.forcePostProcess(self.dir)
def _getVersion(self, **kwargs): def _getVersion(self, **kwargs):
self.data = { self.data = {
'git_path' : headphones.GIT_PATH, 'git_path' : headphones.GIT_PATH,
'install_type' : headphones.INSTALL_TYPE, 'install_type' : headphones.INSTALL_TYPE,
'current_version' : headphones.CURRENT_VERSION, 'current_version' : headphones.CURRENT_VERSION,
'latest_version' : headphones.LATEST_VERSION, 'latest_version' : headphones.LATEST_VERSION,
'commits_behind' : headphones.COMMITS_BEHIND, 'commits_behind' : headphones.COMMITS_BEHIND,
} }
def _checkGithub(self, **kwargs): def _checkGithub(self, **kwargs):
versioncheck.checkGithub() versioncheck.checkGithub()
self._getVersion() self._getVersion()
def _shutdown(self, **kwargs): def _shutdown(self, **kwargs):
headphones.SIGNAL = 'shutdown' headphones.SIGNAL = 'shutdown'
def _restart(self, **kwargs): def _restart(self, **kwargs):
headphones.SIGNAL = 'restart' headphones.SIGNAL = 'restart'
def _update(self, **kwargs): def _update(self, **kwargs):
headphones.SIGNAL = 'update' headphones.SIGNAL = 'update'
def _getArtistArt(self, **kwargs): def _getArtistArt(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getArtwork(ArtistID=self.id) self.data = cache.getArtwork(ArtistID=self.id)
def _getAlbumArt(self, **kwargs): def _getAlbumArt(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getArtwork(AlbumID=self.id) self.data = cache.getArtwork(AlbumID=self.id)
def _getArtistInfo(self, **kwargs): def _getArtistInfo(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getInfo(ArtistID=self.id) self.data = cache.getInfo(ArtistID=self.id)
def _getAlbumInfo(self, **kwargs): def _getAlbumInfo(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getInfo(AlbumID=self.id) self.data = cache.getInfo(AlbumID=self.id)
def _getArtistThumb(self, **kwargs): def _getArtistThumb(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getThumb(ArtistID=self.id) self.data = cache.getThumb(ArtistID=self.id)
def _getAlbumThumb(self, **kwargs): def _getAlbumThumb(self, **kwargs):
if 'id' not in kwargs: if 'id' not in kwargs:
self.data = 'Missing parameter: id' self.data = 'Missing parameter: id'
return return
else: else:
self.id = kwargs['id'] self.id = kwargs['id']
self.data = cache.getThumb(AlbumID=self.id) self.data = cache.getThumb(AlbumID=self.id)
def _choose_specific_download(self, **kwargs): def _choose_specific_download(self, **kwargs):
@@ -403,9 +403,9 @@ class Api(object):
self.id = kwargs['id'] self.id = kwargs['id']
results = searcher.searchforalbum(self.id, choose_specific_download=True) results = searcher.searchforalbum(self.id, choose_specific_download=True)
results_as_dicts = [] results_as_dicts = []
for result in results: for result in results:
result_dict = { result_dict = {
+17 -17
View File
@@ -31,7 +31,7 @@ from headphones import logger
def dbFilename(filename="headphones.db"): def dbFilename(filename="headphones.db"):
return os.path.join(headphones.DATA_DIR, filename) return os.path.join(headphones.DATA_DIR, filename)
def getCacheSize(): def getCacheSize():
#this will protect against typecasting problems produced by empty string and None settings #this will protect against typecasting problems produced by empty string and None settings
if not headphones.CACHE_SIZEMB: if not headphones.CACHE_SIZEMB:
@@ -42,25 +42,25 @@ def getCacheSize():
class DBConnection: class DBConnection:
def __init__(self, filename="headphones.db"): def __init__(self, filename="headphones.db"):
self.filename = filename self.filename = filename
self.connection = sqlite3.connect(dbFilename(filename), timeout=20) self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
#don't wait for the disk to finish writing #don't wait for the disk to finish writing
self.connection.execute("PRAGMA synchronous = OFF") self.connection.execute("PRAGMA synchronous = OFF")
#journal disabled since we never do rollbacks #journal disabled since we never do rollbacks
self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE) self.connection.execute("PRAGMA journal_mode = %s" % headphones.JOURNAL_MODE)
#64mb of cache memory,probably need to make it user configurable #64mb of cache memory,probably need to make it user configurable
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024)) self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024))
self.connection.row_factory = sqlite3.Row self.connection.row_factory = sqlite3.Row
def action(self, query, args=None): def action(self, query, args=None):
if query == None: if query == None:
return return
sqlResult = None sqlResult = None
attempt = 0 attempt = 0
while attempt < 5: while attempt < 5:
try: try:
if args == None: if args == None:
@@ -82,28 +82,28 @@ class DBConnection:
except sqlite3.DatabaseError, e: except sqlite3.DatabaseError, e:
logger.error('Fatal Error executing %s :: %s', query, e) logger.error('Fatal Error executing %s :: %s', query, e)
raise raise
return sqlResult return sqlResult
def select(self, query, args=None): def select(self, query, args=None):
sqlResults = self.action(query, args).fetchall() sqlResults = self.action(query, args).fetchall()
if sqlResults == None: if sqlResults == None:
return [] return []
return sqlResults return sqlResults
def upsert(self, tableName, valueDict, keyDict): def upsert(self, tableName, valueDict, keyDict):
changesBefore = self.connection.total_changes changesBefore = self.connection.total_changes
genParams = lambda myDict : [x + " = ?" for x in myDict.keys()] genParams = lambda myDict : [x + " = ?" for x in myDict.keys()]
query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
self.action(query, valueDict.values() + keyDict.values()) self.action(query, valueDict.values() + keyDict.values())
if self.connection.total_changes == changesBefore: if self.connection.total_changes == changesBefore:
query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \ query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
+2 -2
View File
@@ -8,7 +8,7 @@ from headphones import logger
def getXldProfile(xldProfile): def getXldProfile(xldProfile):
xldProfileNotFound = xldProfile xldProfileNotFound = xldProfile
expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist') expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
try: try:
preferences = plistlib.Plist.fromFile(expandedPath) preferences = plistlib.Plist.fromFile(expandedPath)
except (expat.ExpatError): except (expat.ExpatError):
os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath ) os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath )
@@ -61,7 +61,7 @@ def getXldProfile(xldProfile):
elif 'TVBR' in ShortDesc: elif 'TVBR' in ShortDesc:
XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality')) XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality'))
if XLDAacOutput2_VBRQuality > 122: if XLDAacOutput2_VBRQuality > 122:
xldBitrate = 320 xldBitrate = 320
elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122: elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122:
xldBitrate = 285 xldBitrate = 285
elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113: elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113:
+10 -10
View File
@@ -226,27 +226,27 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
skip_log = 0 skip_log = 0
#Make a user configurable variable to skip update of albums with release dates older than this date (in days) #Make a user configurable variable to skip update of albums with release dates older than this date (in days)
pause_delta = headphones.MB_IGNORE_AGE pause_delta = headphones.MB_IGNORE_AGE
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone() rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
if not forcefull: if not forcefull:
new_release_group = False new_release_group = False
try: try:
check_release_date = rg_exists['ReleaseDate'] check_release_date = rg_exists['ReleaseDate']
except TypeError: except TypeError:
check_release_date = None check_release_date = None
new_release_group = True new_release_group = True
if new_release_group: if new_release_group:
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title'])) logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras) new_releases = mb.get_new_releases(rgid,includeExtras)
else: else:
if check_release_date is None or check_release_date == u"None": if check_release_date is None or check_release_date == u"None":
logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title'])) logger.info("[%s] Now updating: %s (No Release Date)" % (artist['artist_name'], rg['title']))
new_releases = mb.get_new_releases(rgid,includeExtras,True) new_releases = mb.get_new_releases(rgid,includeExtras,True)
@@ -384,7 +384,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
# If there's no release in the main albums tables, add the default (hybrid) # If there's no release in the main albums tables, add the default (hybrid)
# If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated) # If there is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
# check if the album already exists # check if the album already exists
if not rg_exists: if not rg_exists:
releaseid = rg['id'] releaseid = rg['id']
else: else:
@@ -410,7 +410,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
if rg_exists: if rg_exists:
newValueDict['DateAdded'] = rg_exists['DateAdded'] newValueDict['DateAdded'] = rg_exists['DateAdded']
newValueDict['Status'] = rg_exists['Status'] newValueDict['Status'] = rg_exists['Status']
else: else:
today = helpers.today() today = helpers.today()
+40 -40
View File
@@ -27,18 +27,18 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
if cron and not headphones.LIBRARYSCAN: if cron and not headphones.LIBRARYSCAN:
return return
if not dir: if not dir:
if not headphones.MUSIC_DIR: if not headphones.MUSIC_DIR:
return return
else: else:
dir = headphones.MUSIC_DIR dir = headphones.MUSIC_DIR
# If we're appending a dir, it's coming from the post processor which is # If we're appending a dir, it's coming from the post processor which is
# already bytestring # already bytestring
if not append: if not append:
dir = dir.encode(headphones.SYS_ENCODING) dir = dir.encode(headphones.SYS_ENCODING)
if not os.path.isdir(dir): if not os.path.isdir(dir):
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace'))
return return
@@ -47,7 +47,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
new_artists = [] new_artists = []
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append: if not append:
# Clean up bad filepaths # Clean up bad filepaths
tracks = myDB.select('SELECT Location, TrackID from alltracks WHERE Location IS NOT NULL') tracks = myDB.select('SELECT Location, TrackID from alltracks WHERE Location IS NOT NULL')
@@ -57,7 +57,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
if not os.path.isfile(encoded_track_string): if not os.path.isfile(encoded_track_string):
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']]) myDB.action('UPDATE alltracks SET Location=?, BitRate=?, Format=? WHERE Location=?', [None, None, None, track['Location']])
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have') del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
for track in del_have_tracks: for track in del_have_tracks:
@@ -71,13 +71,13 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
###############myDB.action('DELETE from have') ###############myDB.action('DELETE from have')
bitrates = [] bitrates = []
song_list = [] song_list = []
new_song_count = 0 new_song_count = 0
file_count = 0 file_count = 0
latest_subdirectory = [] latest_subdirectory = []
for r,d,f in os.walk(dir): for r,d,f in os.walk(dir):
#need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one #need to abuse slicing to get a copy of the list, doing it directly will skip the element after a deleted one
#using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list) #using a list comprehension will not work correctly for nested subdirectories (os.walk keeps its original list)
@@ -108,11 +108,11 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
except: except:
logger.error('Cannot read file: ' + unicode_song_path) logger.error('Cannot read file: ' + unicode_song_path)
continue continue
# Grab the bitrates for the auto detect bit rate option # Grab the bitrates for the auto detect bit rate option
if f.bitrate: if f.bitrate:
bitrates.append(f.bitrate) bitrates.append(f.bitrate)
# Use the album artist over the artist if available # Use the album artist over the artist if available
if f.albumartist: if f.albumartist:
f_artist = f.albumartist f_artist = f.albumartist
@@ -120,8 +120,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
f_artist = f.artist f_artist = f.artist
else: else:
f_artist = None f_artist = None
# Add the song to our song list - # Add the song to our song list -
# TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements) # TODO: skip adding songs without the minimum requisite information (just a matter of putting together the right if statements)
if f_artist and f.album and f.title: if f_artist and f.album and f.title:
@@ -144,7 +144,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
'Format' : f.format, 'Format' : f.format,
'CleanName' : CleanName 'CleanName' : CleanName
} }
#song_list.append(song_dict) #song_list.append(song_dict)
check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone() check_exist_song = myDB.action("SELECT * FROM have WHERE Location=?", [unicode_song_path]).fetchone()
#Only attempt to match songs that are new, haven't yet been matched, or metadata has changed. #Only attempt to match songs that are new, haven't yet been matched, or metadata has changed.
@@ -182,17 +182,17 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]) song_list = myDB.action("SELECT * FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"])
total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0] total_number_of_songs = myDB.action("SELECT COUNT(*) FROM have WHERE Matched IS NULL AND LOCATION LIKE ?", [dir.decode(headphones.SYS_ENCODING, 'replace')+"%"]).fetchone()[0]
logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....") logger.info("Found " + str(total_number_of_songs) + " new/modified tracks in: '" + dir.decode(headphones.SYS_ENCODING, 'replace') + "'. Matching tracks to the appropriate releases....")
# Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid) # Sort the song_list by most vague (e.g. no trackid or releaseid) to most specific (both trackid & releaseid)
# When we insert into the database, the tracks with the most specific information will overwrite the more general matches # When we insert into the database, the tracks with the most specific information will overwrite the more general matches
##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID']) ##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle']) song_list = helpers.multikeysort(song_list, ['ArtistName', 'AlbumTitle'])
# We'll use this to give a % completion, just because the track matching might take a while # We'll use this to give a % completion, just because the track matching might take a while
song_count = 0 song_count = 0
latest_artist = [] latest_artist = []
for song in song_list: for song in song_list:
latest_artist.append(song['ArtistName']) latest_artist.append(song['ArtistName'])
@@ -200,26 +200,26 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
logger.info("Now matching songs by %s" % song['ArtistName']) logger.info("Now matching songs by %s" % song['ArtistName'])
elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0: elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0:
logger.info("Now matching songs by %s" % song['ArtistName']) logger.info("Now matching songs by %s" % song['ArtistName'])
#print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle'] #print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle']
song_count += 1 song_count += 1
completion_percentage = float(song_count)/total_number_of_songs * 100 completion_percentage = float(song_count)/total_number_of_songs * 100
if completion_percentage%10 == 0: if completion_percentage%10 == 0:
logger.info("Track matching is " + str(completion_percentage) + "% complete") logger.info("Track matching is " + str(completion_percentage) + "% complete")
#THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT #THE "MORE-SPECIFIC" CLAUSES HERE HAVE ALL BEEN REMOVED. WHEN RUNNING A LIBRARY SCAN, THE ONLY CLAUSES THAT
#EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION, #EVER GOT HIT WERE [ARTIST/ALBUM/TRACK] OR CLEANNAME. ARTISTID & RELEASEID ARE NEVER PASSED TO THIS FUNCTION,
#ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM #ARE NEVER FOUND, AND THE OTHER CLAUSES WERE NEVER HIT. FURTHERMORE, OTHER MATCHING FUNCTIONS IN THIS PROGRAM
#(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT. #(IMPORTER.PY, MB.PY) SIMPLY DO A [ARTIST/ALBUM/TRACK] OR CLEANNAME MATCH, SO IT'S ALL CONSISTENT.
if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']: if song['ArtistName'] and song['AlbumTitle'] and song['TrackTitle']:
track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone() track = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
if track: if track:
controlValueDict = { 'ArtistName' : track['ArtistName'], controlValueDict = { 'ArtistName' : track['ArtistName'],
'AlbumTitle' : track['AlbumTitle'], 'AlbumTitle' : track['AlbumTitle'],
'TrackTitle' : track['TrackTitle'] } 'TrackTitle' : track['TrackTitle'] }
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
@@ -231,10 +231,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
else: else:
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
if track: if track:
controlValueDict = { 'CleanName' : track['CleanName']} controlValueDict = { 'CleanName' : track['CleanName']}
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
@@ -244,9 +244,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : "Failed"} newValueDict2 = { 'Matched' : "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
alltrack = myDB.action('SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from alltracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
if alltrack: if alltrack:
controlValueDict = { 'ArtistName' : alltrack['ArtistName'], controlValueDict = { 'ArtistName' : alltrack['ArtistName'],
'AlbumTitle' : alltrack['AlbumTitle'], 'AlbumTitle' : alltrack['AlbumTitle'],
@@ -262,10 +262,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
else: else:
alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone() alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
if alltrack: if alltrack:
controlValueDict = { 'CleanName' : alltrack['CleanName']} controlValueDict = { 'CleanName' : alltrack['CleanName']}
newValueDict = { 'Location' : song['Location'], newValueDict = { 'Location' : song['Location'],
'BitRate' : song['BitRate'], 'BitRate' : song['BitRate'],
'Format' : song['Format'] } 'Format' : song['Format'] }
myDB.upsert("alltracks", newValueDict, controlValueDict) myDB.upsert("alltracks", newValueDict, controlValueDict)
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
@@ -279,35 +279,35 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
controlValueDict2 = { 'Location' : song['Location']} controlValueDict2 = { 'Location' : song['Location']}
newValueDict2 = { 'Matched' : "Failed"} newValueDict2 = { 'Matched' : "Failed"}
myDB.upsert("have", newValueDict2, controlValueDict2) myDB.upsert("have", newValueDict2, controlValueDict2)
#######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']]) #######myDB.action('INSERT INTO have (ArtistName, AlbumTitle, TrackNumber, TrackTitle, TrackLength, BitRate, Genre, Date, TrackID, Location, CleanName, Format) VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [song['ArtistName'], song['AlbumTitle'], song['TrackNumber'], song['TrackTitle'], song['TrackLength'], song['BitRate'], song['Genre'], song['Date'], song['TrackID'], song['Location'], CleanName, song['Format']])
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace')) logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
if not append: if not append:
logger.info('Updating scanned artist track counts') logger.info('Updating scanned artist track counts')
# Clean up the new artist list # Clean up the new artist list
unique_artists = {}.fromkeys(new_artists).keys() unique_artists = {}.fromkeys(new_artists).keys()
current_artists = myDB.select('SELECT ArtistName, ArtistID from artists') current_artists = myDB.select('SELECT ArtistName, ArtistID from artists')
#There was a bug where artists with special characters (-,') would show up in new artists. #There was a bug where artists with special characters (-,') would show up in new artists.
artist_list = [f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists]] artist_list = [f for f in unique_artists if helpers.cleanName(f).lower() not in [helpers.cleanName(x[0]).lower() for x in current_artists]]
artists_checked = [f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists]] artists_checked = [f for f in unique_artists if helpers.cleanName(f).lower() in [helpers.cleanName(x[0]).lower() for x in current_artists]]
# Update track counts # Update track counts
for artist in artists_checked: for artist in artists_checked:
# Have tracks are selected from tracks table and not all tracks because of duplicates # Have tracks are selected from tracks table and not all tracks because of duplicates
# We update the track count upon an album switch to compliment this # We update the track count upon an album switch to compliment this
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL', [artist])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [artist]))
#Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases #Note, some people complain about having "artist have tracks" > # of tracks total in artist official releases
# (can fix by getting rid of second len statement) # (can fix by getting rid of second len statement)
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist]) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
logger.info('Found %i new artists' % len(artist_list)) logger.info('Found %i new artists' % len(artist_list))
if len(artist_list): if len(artist_list):
if headphones.ADD_ARTISTS: if headphones.ADD_ARTISTS:
logger.info('Importing %i new artists' % len(artist_list)) logger.info('Importing %i new artists' % len(artist_list))
@@ -317,14 +317,14 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
#myDB.action('DELETE from newartists') #myDB.action('DELETE from newartists')
for artist in artist_list: for artist in artist_list:
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist]) myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
if headphones.DETECT_BITRATE: if headphones.DETECT_BITRATE:
headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000 headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000
else: else:
# If we're appending a new album to the database, update the artists total track counts # If we're appending a new album to the database, update the artists total track counts
logger.info('Updating artist track counts') logger.info('Updating artist track counts')
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName])) havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [ArtistID])) + len(myDB.select('SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"', [ArtistName]))
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID]) myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
@@ -357,7 +357,7 @@ def update_album_status(AlbumID=None):
if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped': if album_completion >= headphones.ALBUM_COMPLETION_PCT and album['Status'] == 'Skipped':
new_album_status = "Downloaded" new_album_status = "Downloaded"
# I don't think we want to change Downloaded->Skipped..... # I don't think we want to change Downloaded->Skipped.....
# I think we can only automatically change Skipped->Downloaded when updating # I think we can only automatically change Skipped->Downloaded when updating
# There was a bug report where this was causing infinite downloads if the album was # There was a bug report where this was causing infinite downloads if the album was
@@ -369,7 +369,7 @@ def update_album_status(AlbumID=None):
# new_album_status = album['Status'] # new_album_status = album['Status']
else: else:
new_album_status = album['Status'] new_album_status = album['Status']
myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']}) myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']})
if new_album_status != album['Status']: if new_album_status != album['Status']:
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status)) logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
+8 -8
View File
@@ -27,26 +27,26 @@ def getLyrics(artist, song):
url = 'http://lyrics.wikia.com/api.php' url = 'http://lyrics.wikia.com/api.php'
data = request.request_minidom(url, params=params) data = request.request_minidom(url, params=params)
if not data: if not data:
return return
url = data.getElementsByTagName("url") url = data.getElementsByTagName("url")
if url: if url:
lyricsurl = url[0].firstChild.nodeValue lyricsurl = url[0].firstChild.nodeValue
else: else:
logger.info('No lyrics found for %s - %s' % (artist, song)) logger.info('No lyrics found for %s - %s' % (artist, song))
return return
lyricspage = request.request_content(lyricsurl) lyricspage = request.request_content(lyricsurl)
if not lyricspage: if not lyricspage:
logger.warn('Error fetching lyrics from: %s' % lyricsurl) logger.warn('Error fetching lyrics from: %s' % lyricsurl)
return return
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage) m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage)
if not m: if not m:
m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage) m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage)
if m: if m:
@@ -54,10 +54,10 @@ def getLyrics(artist, song):
else: else:
logger.warn('Cannot find lyrics on: %s' % lyricsurl) logger.warn('Cannot find lyrics on: %s' % lyricsurl)
return return
lyrics = convert_html_entities(m.group(1)).replace('<br />', '\n') lyrics = convert_html_entities(m.group(1)).replace('<br />', '\n')
lyrics = re.sub('<.*?>', '', lyrics) lyrics = re.sub('<.*?>', '', lyrics)
return lyrics return lyrics
def convert_html_entities(s): def convert_html_entities(s):
+74 -74
View File
@@ -34,7 +34,7 @@ def startmb():
mbuser = None mbuser = None
mbpass = None mbpass = None
if headphones.MIRROR == "musicbrainz.org": if headphones.MIRROR == "musicbrainz.org":
mbhost = "musicbrainz.org" mbhost = "musicbrainz.org"
mbport = 80 mbport = 80
@@ -51,7 +51,7 @@ def startmb():
sleepytime = 0 sleepytime = 0
else: else:
return False return False
musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones") musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones")
musicbrainzngs.set_hostname(mbhost + ":" + str(mbport)) musicbrainzngs.set_hostname(mbhost + ":" + str(mbport))
if sleepytime == 0: if sleepytime == 0:
@@ -66,17 +66,17 @@ def startmb():
logger.warn("No username or password set for VIP server") logger.warn("No username or password set for VIP server")
else: else:
musicbrainzngs.hpauth(mbuser,mbpass) musicbrainzngs.hpauth(mbuser,mbpass)
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime) logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
return True return True
def findArtist(name, limit=1): def findArtist(name, limit=1):
with mb_lock: with mb_lock:
artistlist = [] artistlist = []
artistResults = None artistResults = None
chars = set('!?*-') chars = set('!?*-')
if any((c in chars) for c in name): if any((c in chars) for c in name):
name = '"'+name+'"' name = '"'+name+'"'
@@ -88,9 +88,9 @@ def findArtist(name, limit=1):
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
time.sleep(5) time.sleep(5)
if not artistResults: if not artistResults:
return False return False
for result in artistResults: for result in artistResults:
if 'disambiguation' in result: if 'disambiguation' in result:
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")") uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
@@ -98,7 +98,7 @@ def findArtist(name, limit=1):
uniquename = unicode(result['sort-name']) uniquename = unicode(result['sort-name'])
if result['name'] != uniquename and limit == 1: if result['name'] != uniquename and limit == 1:
logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name) logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name)
artistdict = findArtistbyAlbum(name) artistdict = findArtistbyAlbum(name)
if not artistdict: if not artistdict:
logger.info('Cannot determine the best match from an artist/album search. Using top match instead') logger.info('Cannot determine the best match from an artist/album search. Using top match instead')
artistlist.append({ artistlist.append({
@@ -108,10 +108,10 @@ def findArtist(name, limit=1):
'id': unicode(result['id']), 'id': unicode(result['id']),
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed # 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
# 'score': int(result['ext:score']) # 'score': int(result['ext:score'])
}) })
else: else:
artistlist.append(artistdict) artistlist.append(artistdict)
else: else:
artistlist.append({ artistlist.append({
'name': unicode(result['sort-name']), 'name': unicode(result['sort-name']),
'uniquename': uniquename, 'uniquename': uniquename,
@@ -120,10 +120,10 @@ def findArtist(name, limit=1):
'score': int(result['ext:score']) 'score': int(result['ext:score'])
}) })
return artistlist return artistlist
def findRelease(name, limit=1, artist=None): def findRelease(name, limit=1, artist=None):
with mb_lock: with mb_lock:
releaselist = [] releaselist = []
releaseResults = None releaseResults = None
@@ -193,38 +193,38 @@ def findRelease(name, limit=1, artist=None):
def getArtist(artistid, extrasonly=False): def getArtist(artistid, extrasonly=False):
with mb_lock: with mb_lock:
artist_dict = {} artist_dict = {}
artist = None artist = None
try: try:
limit = 200 limit = 200
artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] artist = musicbrainzngs.get_artist_by_id(artistid)['artist']
newRgs = None newRgs = None
artist['release-group-list'] = [] artist['release-group-list'] = []
while newRgs == None or len(newRgs) >= limit: while newRgs == None or len(newRgs) >= limit:
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list'] newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list']
artist['release-group-list'] += newRgs artist['release-group-list'] += newRgs
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
time.sleep(5) time.sleep(5)
except Exception,e: except Exception,e:
pass pass
if not artist: if not artist:
return False return False
#if 'disambiguation' in artist: #if 'disambiguation' in artist:
# uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")") # uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")")
#else: #else:
# uniquename = unicode(artist['sort-name']) # uniquename = unicode(artist['sort-name'])
artist_dict['artist_name'] = unicode(artist['name']) artist_dict['artist_name'] = unicode(artist['name'])
# Not using the following values anywhere yet so we don't need to grab them. # Not using the following values anywhere yet so we don't need to grab them.
# Was causing an exception to be raised if they didn't exist. # Was causing an exception to be raised if they didn't exist.
# #
#artist_dict['artist_sortname'] = unicode(artist['sort-name']) #artist_dict['artist_sortname'] = unicode(artist['sort-name'])
#artist_dict['artist_uniquename'] = uniquename #artist_dict['artist_uniquename'] = uniquename
#artist_dict['artist_type'] = unicode(artist['type']) #artist_dict['artist_type'] = unicode(artist['type'])
@@ -235,11 +235,11 @@ def getArtist(artistid, extrasonly=False):
# if 'begin' in artist['life-span']: # if 'begin' in artist['life-span']:
# artist_dict['artist_begindate'] = unicode(artist['life-span']['begin']) # artist_dict['artist_begindate'] = unicode(artist['life-span']['begin'])
# if 'end' in artist['life-span']: # if 'end' in artist['life-span']:
# artist_dict['artist_enddate'] = unicode(artist['life-span']['end']) # artist_dict['artist_enddate'] = unicode(artist['life-span']['end'])
releasegroups = [] releasegroups = []
if not extrasonly: if not extrasonly:
for rg in artist['release-group-list']: for rg in artist['release-group-list']:
if "secondary-type-list" in rg.keys(): #only add releases without a secondary type if "secondary-type-list" in rg.keys(): #only add releases without a secondary type
@@ -249,8 +249,8 @@ def getArtist(artistid, extrasonly=False):
'id': unicode(rg['id']), 'id': unicode(rg['id']),
'url': u"http://musicbrainz.org/release-group/" + rg['id'], 'url': u"http://musicbrainz.org/release-group/" + rg['id'],
'type': unicode(rg['type']) 'type': unicode(rg['type'])
}) })
# See if we need to grab extras. Artist specific extras take precedence over global option # See if we need to grab extras. Artist specific extras take precedence over global option
# Global options are set when adding a new artist # Global options are set when adding a new artist
myDB = db.DBConnection() myDB = db.DBConnection()
@@ -260,14 +260,14 @@ def getArtist(artistid, extrasonly=False):
includeExtras = db_artist['IncludeExtras'] includeExtras = db_artist['IncludeExtras']
except IndexError: except IndexError:
includeExtras = False includeExtras = False
if includeExtras: if includeExtras:
# Need to convert extras string from something like '2,5.6' to ['ep','live','remix'] # Need to convert extras string from something like '2,5.6' to ['ep','live','remix']
extras = db_artist['Extras'] extras = db_artist['Extras']
extras_list = ["single", "ep", "compilation", "soundtrack", "live", "remix", "dj-mix", "mixtape/street", "spokenword", "audiobook", "broadcast", "interview", "other"] extras_list = ["single", "ep", "compilation", "soundtrack", "live", "remix", "dj-mix", "mixtape/street", "spokenword", "audiobook", "broadcast", "interview", "other"]
includes = [] includes = []
i = 1 i = 1
for extra in extras_list: for extra in extras_list:
if str(i) in extras: if str(i) in extras:
@@ -282,7 +282,7 @@ def getArtist(artistid, extrasonly=False):
limit = 200 limit = 200
newRgs = None newRgs = None
while newRgs == None or len(newRgs) >= limit: while newRgs == None or len(newRgs) >= limit:
newRgs = musicbrainzngs.browse_release_groups(artistid,release_type=include,offset=len(mb_extras_list),limit=limit)['release-group-list'] newRgs = musicbrainzngs.browse_release_groups(artistid,release_type=include,offset=len(mb_extras_list),limit=limit)['release-group-list']
mb_extras_list += newRgs mb_extras_list += newRgs
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e)))
@@ -294,42 +294,42 @@ def getArtist(artistid, extrasonly=False):
'id': unicode(rg['id']), 'id': unicode(rg['id']),
'url': u"http://musicbrainz.org/release-group/" + rg['id'], 'url': u"http://musicbrainz.org/release-group/" + rg['id'],
'type': unicode(rg['type']) 'type': unicode(rg['type'])
}) })
artist_dict['releasegroups'] = releasegroups artist_dict['releasegroups'] = releasegroups
return artist_dict return artist_dict
def getReleaseGroup(rgid): def getReleaseGroup(rgid):
""" """
Returns a list of releases in a release group Returns a list of releases in a release group
""" """
with mb_lock: with mb_lock:
releaselist = [] releaselist = []
releaseGroup = None releaseGroup = None
try: try:
releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group'] releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group']
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
time.sleep(5) time.sleep(5)
if not releaseGroup: if not releaseGroup:
return False return False
else: else:
return releaseGroup['release-list'] return releaseGroup['release-list']
def getRelease(releaseid, include_artist_info=True): def getRelease(releaseid, include_artist_info=True):
""" """
Deep release search to get track info Deep release search to get track info
""" """
with mb_lock: with mb_lock:
release = {} release = {}
results = None results = None
try: try:
if include_artist_info: if include_artist_info:
results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release') results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release')
@@ -337,28 +337,28 @@ def getRelease(releaseid, include_artist_info=True):
results = musicbrainzngs.get_release_by_id(releaseid,["media","recordings"]).get('release') results = musicbrainzngs.get_release_by_id(releaseid,["media","recordings"]).get('release')
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e)))
time.sleep(5) time.sleep(5)
if not results: if not results:
return False return False
release['title'] = unicode(results['title']) release['title'] = unicode(results['title'])
release['id'] = unicode(results['id']) release['id'] = unicode(results['id'])
release['asin'] = unicode(results['asin']) if 'asin' in results else None release['asin'] = unicode(results['asin']) if 'asin' in results else None
release['date'] = unicode(results['date']) if 'date' in results else None release['date'] = unicode(results['date']) if 'date' in results else None
try: try:
release['format'] = unicode(results['medium-list'][0]['format']) release['format'] = unicode(results['medium-list'][0]['format'])
except: except:
release['format'] = u'Unknown' release['format'] = u'Unknown'
try: try:
release['country'] = unicode(results['country']) release['country'] = unicode(results['country'])
except: except:
release['country'] = u'Unknown' release['country'] = u'Unknown'
if include_artist_info: if include_artist_info:
if 'release-group' in results: if 'release-group' in results:
release['rgid'] = unicode(results['release-group']['id']) release['rgid'] = unicode(results['release-group']['id'])
release['rg_title'] = unicode(results['release-group']['title']) release['rg_title'] = unicode(results['release-group']['title'])
@@ -373,7 +373,7 @@ def getRelease(releaseid, include_artist_info=True):
release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id']) release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id'])
release['tracks'] = getTracksFromRelease(results) release['tracks'] = getTracksFromRelease(results)
return release return release
def get_new_releases(rgid,includeExtras=False,forcefull=False): def get_new_releases(rgid,includeExtras=False,forcefull=False):
@@ -389,12 +389,12 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
break #may want to raise an exception here instead ? break #may want to raise an exception here instead ?
newResults = newResults['release-list'] newResults = newResults['release-list']
results += newResults results += newResults
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
time.sleep(5) time.sleep(5)
return False return False
if not results or len(results) == 0: if not results or len(results) == 0:
return False return False
@@ -426,7 +426,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
#all official releases should have the Official status included #all official releases should have the Official status included
if not includeExtras and releasedata.get('status') != 'Official': if not includeExtras and releasedata.get('status') != 'Official':
continue continue
release = {} release = {}
rel_id_check = releasedata['id'] rel_id_check = releasedata['id']
artistid = unicode(releasedata['artist-credit'][0]['artist']['id']) artistid = unicode(releasedata['artist-credit'][0]['artist']['id'])
@@ -439,7 +439,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
release['AlbumTitle'] = unicode(releasedata['title']) release['AlbumTitle'] = unicode(releasedata['title'])
release['AlbumID'] = unicode(rgid) release['AlbumID'] = unicode(rgid)
release['AlbumASIN'] = unicode(releasedata['asin']) if 'asin' in releasedata else None release['AlbumASIN'] = unicode(releasedata['asin']) if 'asin' in releasedata else None
release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None release['ReleaseDate'] = unicode(releasedata['date']) if 'date' in releasedata else None
release['ReleaseID'] = releasedata['id'] release['ReleaseID'] = releasedata['id']
if 'release-group' not in releasedata: if 'release-group' not in releasedata:
raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid) raise Exception('No release group associated with release id ' + releasedata['id'] + ' album id' + rgid)
@@ -453,7 +453,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
else: else:
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.') logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
return False return False
release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown' release['ReleaseCountry'] = unicode(releasedata['country']) if 'country' in releasedata else u'Unknown'
#assuming that the list will contain media and that the format will be consistent #assuming that the list will contain media and that the format will be consistent
@@ -472,7 +472,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
release['ReleaseFormat'] = unicode(packaged_medium) release['ReleaseFormat'] = unicode(packaged_medium)
except: except:
release['ReleaseFormat'] = u'Unknown' release['ReleaseFormat'] = u'Unknown'
release['Tracks'] = getTracksFromRelease(releasedata) release['Tracks'] = getTracksFromRelease(releasedata)
# What we're doing here now is first updating the allalbums & alltracks table to the most # What we're doing here now is first updating the allalbums & alltracks table to the most
@@ -492,11 +492,11 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
} }
myDB.upsert("allalbums", newValueDict, controlValueDict) myDB.upsert("allalbums", newValueDict, controlValueDict)
for track in release['Tracks']: for track in release['Tracks']:
cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title']) cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
controlValueDict = {"TrackID": track['id'], controlValueDict = {"TrackID": track['id'],
"ReleaseID": release['ReleaseID']} "ReleaseID": release['ReleaseID']}
@@ -510,20 +510,20 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
"TrackNumber": track['number'], "TrackNumber": track['number'],
"CleanName": cleanname "CleanName": cleanname
} }
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone() match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
if not match: if not match:
match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone() match = myDB.action('SELECT Location, BitRate, Format from have WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?', [release['ArtistName'], release['AlbumTitle'], track['title']]).fetchone()
#if not match: #if not match:
#match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone() #match = myDB.action('SELECT Location, BitRate, Format from have WHERE TrackID=?', [track['id']]).fetchone()
if match: if match:
newValueDict['Location'] = match['Location'] newValueDict['Location'] = match['Location']
newValueDict['BitRate'] = match['BitRate'] newValueDict['BitRate'] = match['BitRate']
newValueDict['Format'] = match['Format'] newValueDict['Format'] = match['Format']
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']]) #myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location'])) myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location']))
myDB.upsert("alltracks", newValueDict, controlValueDict) myDB.upsert("alltracks", newValueDict, controlValueDict)
num_new_releases = num_new_releases + 1 num_new_releases = num_new_releases + 1
#print releasedata['title'] #print releasedata['title']
@@ -556,19 +556,19 @@ def getTracksFromRelease(release):
'url': u"http://musicbrainz.org/track/" + track['recording']['id'], 'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
'duration': int(track['length']) if 'length' in track else 0 'duration': int(track['length']) if 'length' in track else 0
}) })
totalTracks += 1 totalTracks += 1
return tracks return tracks
# Used when there is a disambiguation # Used when there is a disambiguation
def findArtistbyAlbum(name): def findArtistbyAlbum(name):
myDB = db.DBConnection() myDB = db.DBConnection()
artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone() artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone()
if not artist: if not artist:
return False return False
# Probably not neccessary but just want to double check # Probably not neccessary but just want to double check
if not artist['AlbumTitle']: if not artist['AlbumTitle']:
return False return False
@@ -576,20 +576,20 @@ def findArtistbyAlbum(name):
term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"' term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"'
results = None results = None
try: try:
results = musicbrainzngs.search_release_groups(term).get('release-group-list') results = musicbrainzngs.search_release_groups(term).get('release-group-list')
except WebServiceError, e: except WebServiceError, e:
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
time.sleep(5) time.sleep(5)
if not results: if not results:
return False return False
artist_dict = {} artist_dict = {}
for releaseGroup in results: for releaseGroup in results:
newArtist = releaseGroup['artist-credit'][0]['artist'] newArtist = releaseGroup['artist-credit'][0]['artist']
# Only need the artist ID if we're doing an artist+album lookup # Only need the artist ID if we're doing an artist+album lookup
#if 'disambiguation' in newArtist: #if 'disambiguation' in newArtist:
# uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")") # uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")")
@@ -601,10 +601,10 @@ def findArtistbyAlbum(name):
#artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id'] #artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id']
#artist_dict['score'] = int(releaseGroup['ext:score']) #artist_dict['score'] = int(releaseGroup['ext:score'])
return artist_dict return artist_dict
def findAlbumID(artist=None, album=None): def findAlbumID(artist=None, album=None):
results = None results = None
@@ -632,6 +632,6 @@ def findAlbumID(artist=None, album=None):
return False return False
if len(results) < 1: if len(results) < 1:
return False return False
rgid = unicode(results[0]['id']) rgid = unicode(results[0]['id'])
return rgid return rgid
+9 -9
View File
@@ -123,7 +123,7 @@ class PROWL:
def __init__(self): def __init__(self):
self.enabled = headphones.PROWL_ENABLED self.enabled = headphones.PROWL_ENABLED
self.keys = headphones.PROWL_KEYS self.keys = headphones.PROWL_KEYS
self.priority = headphones.PROWL_PRIORITY self.priority = headphones.PROWL_PRIORITY
def conf(self, options): def conf(self, options):
return cherrypy.config['config'].get('Prowl', options) return cherrypy.config['config'].get('Prowl', options)
@@ -150,7 +150,7 @@ class PROWL:
if request_status == 200: if request_status == 200:
logger.info(u"Prowl notifications sent.") logger.info(u"Prowl notifications sent.")
return True return True
elif request_status == 401: elif request_status == 401:
logger.info(u"Prowl auth failed: %s" % response.reason) logger.info(u"Prowl auth failed: %s" % response.reason)
return False return False
else: else:
@@ -413,7 +413,7 @@ class NMA:
logger.error(u'Could not send notification to NotifyMyAndroid') logger.error(u'Could not send notification to NotifyMyAndroid')
return False return False
else: else:
return True return True
class PUSHBULLET: class PUSHBULLET:
@@ -449,7 +449,7 @@ class PUSHBULLET:
if request_status == 200: if request_status == 200:
logger.info(u"PushBullet notifications sent.") logger.info(u"PushBullet notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500: elif request_status >= 400 and request_status < 500:
logger.info(u"PushBullet request failed: %s" % response.reason) logger.info(u"PushBullet request failed: %s" % response.reason)
return False return False
else: else:
@@ -482,7 +482,7 @@ class PUSHALOT:
http_handler = HTTPSConnection("pushalot.com") http_handler = HTTPSConnection("pushalot.com")
data = {'AuthorizationToken': pushalot_authorizationtoken, data = {'AuthorizationToken': pushalot_authorizationtoken,
'Title': event.encode('utf-8'), 'Title': event.encode('utf-8'),
'Body': message.encode("utf-8") } 'Body': message.encode("utf-8") }
@@ -500,7 +500,7 @@ class PUSHALOT:
if request_status == 200: if request_status == 200:
logger.info(u"Pushalot notifications sent.") logger.info(u"Pushalot notifications sent.")
return True return True
elif request_status == 410: elif request_status == 410:
logger.info(u"Pushalot auth failed: %s" % response.reason) logger.info(u"Pushalot auth failed: %s" % response.reason)
return False return False
else: else:
@@ -552,7 +552,7 @@ class PUSHOVER:
def __init__(self): def __init__(self):
self.enabled = headphones.PUSHOVER_ENABLED self.enabled = headphones.PUSHOVER_ENABLED
self.keys = headphones.PUSHOVER_KEYS self.keys = headphones.PUSHOVER_KEYS
self.priority = headphones.PUSHOVER_PRIORITY self.priority = headphones.PUSHOVER_PRIORITY
if headphones.PUSHOVER_APITOKEN: if headphones.PUSHOVER_APITOKEN:
self.application_token = headphones.PUSHOVER_APITOKEN self.application_token = headphones.PUSHOVER_APITOKEN
pass pass
@@ -566,7 +566,7 @@ class PUSHOVER:
http_handler = HTTPSConnection("api.pushover.net") http_handler = HTTPSConnection("api.pushover.net")
data = {'token': self.application_token, data = {'token': self.application_token,
'user': headphones.PUSHOVER_KEYS, 'user': headphones.PUSHOVER_KEYS,
'title': event, 'title': event,
'message': message.encode("utf-8"), 'message': message.encode("utf-8"),
@@ -585,7 +585,7 @@ class PUSHOVER:
if request_status == 200: if request_status == 200:
logger.info(u"Pushover notifications sent.") logger.info(u"Pushover notifications sent.")
return True return True
elif request_status >= 400 and request_status < 500: elif request_status >= 400 and request_status < 500:
logger.info(u"Pushover request failed: %s" % response.reason) logger.info(u"Pushover request failed: %s" % response.reason)
return False return False
else: else:
+153 -153
View File
@@ -38,9 +38,9 @@ def checkFolder():
snatched = myDB.select('SELECT * from snatched WHERE Status="Snatched"') snatched = myDB.select('SELECT * from snatched WHERE Status="Snatched"')
for album in snatched: for album in snatched:
if album['FolderName']: if album['FolderName']:
if album['Kind'] == 'nzb': if album['Kind'] == 'nzb':
download_dir = headphones.DOWNLOAD_DIR download_dir = headphones.DOWNLOAD_DIR
else: else:
@@ -67,13 +67,13 @@ def verify(albumid, albumpath, Kind=None, forced=False):
#TODO: This should be a call to a class method.. copied it out of importer with only minor changes #TODO: This should be a call to a class method.. copied it out of importer with only minor changes
#TODO: odd things can happen when there are diacritic characters in the folder name, need to translate them? #TODO: odd things can happen when there are diacritic characters in the folder name, need to translate them?
release_list = None release_list = None
try: try:
release_list = mb.getReleaseGroup(albumid) release_list = mb.getReleaseGroup(albumid)
except Exception, e: except Exception, e:
logger.error('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e)) logger.error('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
return return
if not release_list: if not release_list:
logger.error('Unable to get release information for manual album with rgid: %s' % albumid) logger.error('Unable to get release information for manual album with rgid: %s' % albumid)
return return
@@ -82,36 +82,36 @@ def verify(albumid, albumpath, Kind=None, forced=False):
releaseid = release_list[0]['id'] releaseid = release_list[0]['id']
release_dict = mb.getRelease(releaseid) release_dict = mb.getRelease(releaseid)
if not release_dict: if not release_dict:
logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue' % albumid) logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue' % albumid)
return return
logger.info(u"Now adding/updating artist: " + release_dict['artist_name']) logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
if release_dict['artist_name'].startswith('The '): if release_dict['artist_name'].startswith('The '):
sortname = release_dict['artist_name'][4:] sortname = release_dict['artist_name'][4:]
else: else:
sortname = release_dict['artist_name'] sortname = release_dict['artist_name']
controlValueDict = {"ArtistID": release_dict['artist_id']} controlValueDict = {"ArtistID": release_dict['artist_id']}
newValueDict = {"ArtistName": release_dict['artist_name'], newValueDict = {"ArtistName": release_dict['artist_name'],
"ArtistSortName": sortname, "ArtistSortName": sortname,
"DateAdded": helpers.today(), "DateAdded": helpers.today(),
"Status": "Paused"} "Status": "Paused"}
logger.info("ArtistID: " + release_dict['artist_id'] + " , ArtistName: " + release_dict['artist_name']) logger.info("ArtistID: " + release_dict['artist_id'] + " , ArtistName: " + release_dict['artist_name'])
if headphones.INCLUDE_EXTRAS: if headphones.INCLUDE_EXTRAS:
newValueDict['IncludeExtras'] = 1 newValueDict['IncludeExtras'] = 1
newValueDict['Extras'] = headphones.EXTRAS newValueDict['Extras'] = headphones.EXTRAS
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
logger.info(u"Now adding album: " + release_dict['title']) logger.info(u"Now adding album: " + release_dict['title'])
controlValueDict = {"AlbumID": albumid} controlValueDict = {"AlbumID": albumid}
newValueDict = {"ArtistID": release_dict['artist_id'], newValueDict = {"ArtistID": release_dict['artist_id'],
"ReleaseID": albumid, "ReleaseID": albumid,
"ArtistName": release_dict['artist_name'], "ArtistName": release_dict['artist_name'],
@@ -124,14 +124,14 @@ def verify(albumid, albumpath, Kind=None, forced=False):
} }
myDB.upsert("albums", newValueDict, controlValueDict) myDB.upsert("albums", newValueDict, controlValueDict)
# Delete existing tracks associated with this AlbumID since we're going to replace them and don't want any extras # Delete existing tracks associated with this AlbumID since we're going to replace them and don't want any extras
myDB.action('DELETE from tracks WHERE AlbumID=?', [albumid]) myDB.action('DELETE from tracks WHERE AlbumID=?', [albumid])
for track in release_dict['tracks']: for track in release_dict['tracks']:
controlValueDict = {"TrackID": track['id'], controlValueDict = {"TrackID": track['id'],
"AlbumID": albumid} "AlbumID": albumid}
newValueDict = {"ArtistID": release_dict['artist_id'], newValueDict = {"ArtistID": release_dict['artist_id'],
"ArtistName": release_dict['artist_name'], "ArtistName": release_dict['artist_name'],
"AlbumTitle": release_dict['title'], "AlbumTitle": release_dict['title'],
@@ -140,25 +140,25 @@ def verify(albumid, albumpath, Kind=None, forced=False):
"TrackDuration": track['duration'], "TrackDuration": track['duration'],
"TrackNumber": track['number'] "TrackNumber": track['number']
} }
myDB.upsert("tracks", newValueDict, controlValueDict) myDB.upsert("tracks", newValueDict, controlValueDict)
controlValueDict = {"ArtistID": release_dict['artist_id']} controlValueDict = {"ArtistID": release_dict['artist_id']}
newValueDict = {"Status": "Paused"} newValueDict = {"Status": "Paused"}
myDB.upsert("artists", newValueDict, controlValueDict) myDB.upsert("artists", newValueDict, controlValueDict)
logger.info(u"Addition complete for: " + release_dict['title'] + " - " + release_dict['artist_name']) logger.info(u"Addition complete for: " + release_dict['title'] + " - " + release_dict['artist_name'])
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone() release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
downloaded_track_list = [] downloaded_track_list = []
downloaded_cuecount = 0 downloaded_cuecount = 0
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
for files in f: for files in f:
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
downloaded_track_list.append(os.path.join(r, files)) downloaded_track_list.append(os.path.join(r, files))
elif files.lower().endswith('.cue'): elif files.lower().endswith('.cue'):
downloaded_cuecount += 1 downloaded_cuecount += 1
# if any of the files end in *.part, we know the torrent isn't done yet. Process if forced, though # if any of the files end in *.part, we know the torrent isn't done yet. Process if forced, though
@@ -166,13 +166,13 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.info("Looks like " + os.path.basename(albumpath).decode(headphones.SYS_ENCODING, 'replace') + " isn't complete yet. Will try again on the next run") logger.info("Looks like " + os.path.basename(albumpath).decode(headphones.SYS_ENCODING, 'replace') + " isn't complete yet. Will try again on the next run")
return return
# use xld to split cue # use xld to split cue
if headphones.ENCODER == 'xld' and headphones.MUSIC_ENCODER and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list): if headphones.ENCODER == 'xld' and headphones.MUSIC_ENCODER and downloaded_cuecount and downloaded_cuecount >= len(downloaded_track_list):
import getXldProfile import getXldProfile
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.XLDPROFILE) (xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.XLDPROFILE)
if not xldFormat: if not xldFormat:
logger.info(u'Details for xld profile "%s" not found, cannot split cue' % (xldProfile)) logger.info(u'Details for xld profile "%s" not found, cannot split cue' % (xldProfile))
@@ -181,7 +181,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
xldencoder = os.path.join(headphones.ENCODERFOLDER, 'xld') xldencoder = os.path.join(headphones.ENCODERFOLDER, 'xld')
else: else:
xldencoder = os.path.join('/Applications','xld') xldencoder = os.path.join('/Applications','xld')
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
xldfolder = r xldfolder = r
xldfile = '' xldfile = ''
@@ -191,7 +191,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
xldfile = os.path.join(r, file) xldfile = os.path.join(r, file)
elif file.lower().endswith('.cue') and not xldcue: elif file.lower().endswith('.cue') and not xldcue:
xldcue = os.path.join(r, file) xldcue = os.path.join(r, file)
if xldfile and xldcue and xldfolder: if xldfile and xldcue and xldfolder:
xldcmd = xldencoder xldcmd = xldencoder
xldcmd = xldcmd + ' "' + xldfile + '"' xldcmd = xldcmd + ' "' + xldfile + '"'
@@ -204,25 +204,25 @@ def verify(albumid, albumpath, Kind=None, forced=False):
logger.info(u"Cue found, splitting file " + xldfile.decode(headphones.SYS_ENCODING, 'replace')) logger.info(u"Cue found, splitting file " + xldfile.decode(headphones.SYS_ENCODING, 'replace'))
logger.debug(xldcmd) logger.debug(xldcmd)
os.system(xldcmd) os.system(xldcmd)
# count files, should now be more than original if xld successfully split # count files, should now be more than original if xld successfully split
new_downloaded_track_list_count = 0 new_downloaded_track_list_count = 0
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
for file in f: for file in f:
if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
new_downloaded_track_list_count += 1 new_downloaded_track_list_count += 1
if new_downloaded_track_list_count > len(downloaded_track_list): if new_downloaded_track_list_count > len(downloaded_track_list):
# rename original unsplit files # rename original unsplit files
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
os.rename(downloaded_track, downloaded_track + '.original') os.rename(downloaded_track, downloaded_track + '.original')
#reload #reload
downloaded_track_list = [] downloaded_track_list = []
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
for file in f: for file in f:
if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
downloaded_track_list.append(os.path.join(r, file)) downloaded_track_list.append(os.path.join(r, file))
@@ -236,58 +236,58 @@ def verify(albumid, albumpath, Kind=None, forced=False):
except Exception, e: except Exception, e:
logger.info(u"Exception from MediaFile for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + u" : " + unicode(e)) logger.info(u"Exception from MediaFile for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + u" : " + unicode(e))
continue continue
if not f.artist: if not f.artist:
continue continue
if not f.album: if not f.album:
continue continue
metaartist = helpers.latinToAscii(f.artist.lower()).encode('UTF-8') metaartist = helpers.latinToAscii(f.artist.lower()).encode('UTF-8')
dbartist = helpers.latinToAscii(release['ArtistName'].lower()).encode('UTF-8') dbartist = helpers.latinToAscii(release['ArtistName'].lower()).encode('UTF-8')
metaalbum = helpers.latinToAscii(f.album.lower()).encode('UTF-8') metaalbum = helpers.latinToAscii(f.album.lower()).encode('UTF-8')
dbalbum = helpers.latinToAscii(release['AlbumTitle'].lower()).encode('UTF-8') dbalbum = helpers.latinToAscii(release['AlbumTitle'].lower()).encode('UTF-8')
logger.debug('Matching metadata artist: %s with artist name: %s' % (metaartist, dbartist)) logger.debug('Matching metadata artist: %s with artist name: %s' % (metaartist, dbartist))
logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum)) logger.debug('Matching metadata album: %s with album name: %s' % (metaalbum, dbalbum))
if metaartist == dbartist and metaalbum == dbalbum: if metaartist == dbartist and metaalbum == dbalbum:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
return return
# test #2: filenames # test #2: filenames
logger.debug('Metadata check failed. Verifying filenames...') logger.debug('Metadata check failed. Verifying filenames...')
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
track_name = os.path.splitext(downloaded_track)[0] track_name = os.path.splitext(downloaded_track)[0]
split_track_name = re.sub('[\.\-\_]', ' ', track_name).lower() split_track_name = re.sub('[\.\-\_]', ' ', track_name).lower()
for track in tracks: for track in tracks:
if not track['TrackTitle']: if not track['TrackTitle']:
continue continue
dbtrack = helpers.latinToAscii(track['TrackTitle'].lower()).encode('UTF-8') dbtrack = helpers.latinToAscii(track['TrackTitle'].lower()).encode('UTF-8')
filetrack = helpers.latinToAscii(split_track_name).encode('UTF-8') filetrack = helpers.latinToAscii(split_track_name).encode('UTF-8')
logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack)) logger.debug('Checking if track title: %s is in file name: %s' % (dbtrack, filetrack))
if dbtrack in filetrack: if dbtrack in filetrack:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
return return
# test #3: number of songs and duration # test #3: number of songs and duration
logger.debug('Filename check failed. Verifying album length...') logger.debug('Filename check failed. Verifying album length...')
db_track_duration = 0 db_track_duration = 0
downloaded_track_duration = 0 downloaded_track_duration = 0
logger.debug('Total music files in %s: %i' % (albumpath, len(downloaded_track_list))) logger.debug('Total music files in %s: %i' % (albumpath, len(downloaded_track_list)))
logger.debug('Total tracks for this album in the database: %i' % len(tracks)) logger.debug('Total tracks for this album in the database: %i' % len(tracks))
if len(tracks) == len(downloaded_track_list): if len(tracks) == len(downloaded_track_list):
for track in tracks: for track in tracks:
try: try:
db_track_duration += track['TrackDuration']/1000 db_track_duration += track['TrackDuration']/1000
except: except:
downloaded_track_duration = False downloaded_track_duration = False
break break
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
try: try:
f = MediaFile(downloaded_track) f = MediaFile(downloaded_track)
@@ -295,7 +295,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
except: except:
downloaded_track_duration = False downloaded_track_duration = False
break break
if downloaded_track_duration and db_track_duration: if downloaded_track_duration and db_track_duration:
logger.debug('Downloaded album duration: %i' % downloaded_track_duration) logger.debug('Downloaded album duration: %i' % downloaded_track_duration)
logger.debug('Database track duration: %i' % db_track_duration) logger.debug('Database track duration: %i' % db_track_duration)
@@ -303,7 +303,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
if delta < 240: if delta < 240:
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind) doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
return return
logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace')) logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace'))
myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE AlbumID=?', [albumid]) myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE AlbumID=?', [albumid])
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath) processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
@@ -311,7 +311,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
renameUnprocessedFolder(albumpath) renameUnprocessedFolder(albumpath)
else: else:
logger.info(u"Already marked as unprocessed: " + albumpath.decode(headphones.SYS_ENCODING, 'replace')) logger.info(u"Already marked as unprocessed: " + albumpath.decode(headphones.SYS_ENCODING, 'replace'))
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None): def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None):
logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle'])) logger.info('Starting post-processing for: %s - %s' % (release['ArtistName'], release['AlbumTitle']))
@@ -326,17 +326,17 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
except Exception, e: except Exception, e:
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e)) logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
return return
# Need to update the downloaded track list with the new location. # Need to update the downloaded track list with the new location.
# Could probably just throw in the "headphones-modified" folder, # Could probably just throw in the "headphones-modified" folder,
# but this is good to make sure we're not counting files that may have failed to move # but this is good to make sure we're not counting files that may have failed to move
downloaded_track_list = [] downloaded_track_list = []
downloaded_cuecount = 0 downloaded_cuecount = 0
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
for files in f: for files in f:
if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS): if any(files.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
downloaded_track_list.append(os.path.join(r, files)) downloaded_track_list.append(os.path.join(r, files))
elif files.lower().endswith('.cue'): elif files.lower().endswith('.cue'):
downloaded_cuecount += 1 downloaded_cuecount += 1
@@ -363,14 +363,14 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
#start encoding #start encoding
if headphones.MUSIC_ENCODER: if headphones.MUSIC_ENCODER:
downloaded_track_list=music_encoder.encode(albumpath) downloaded_track_list=music_encoder.encode(albumpath)
if not downloaded_track_list: if not downloaded_track_list:
return return
artwork = None artwork = None
album_art_path = albumart.getAlbumArt(albumid) album_art_path = albumart.getAlbumArt(albumid)
if headphones.EMBED_ALBUM_ART or headphones.ADD_ALBUM_ART: if headphones.EMBED_ALBUM_ART or headphones.ADD_ALBUM_ART:
if album_art_path: if album_art_path:
artwork = request.request_content(album_art_path) artwork = request.request_content(album_art_path)
else: else:
@@ -382,25 +382,25 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
if not artwork or len(artwork) < 100: if not artwork or len(artwork) < 100:
artwork = False artwork = False
logger.info("No suitable album art found from Last.FM. Not adding album art") logger.info("No suitable album art found from Last.FM. Not adding album art")
if headphones.EMBED_ALBUM_ART and artwork: if headphones.EMBED_ALBUM_ART and artwork:
embedAlbumArt(artwork, downloaded_track_list) embedAlbumArt(artwork, downloaded_track_list)
if headphones.CLEANUP_FILES: if headphones.CLEANUP_FILES:
cleanupFiles(albumpath) cleanupFiles(albumpath)
if headphones.ADD_ALBUM_ART and artwork: if headphones.ADD_ALBUM_ART and artwork:
addAlbumArt(artwork, albumpath, release) addAlbumArt(artwork, albumpath, release)
if headphones.CORRECT_METADATA: if headphones.CORRECT_METADATA:
correctMetadata(albumid, release, downloaded_track_list) correctMetadata(albumid, release, downloaded_track_list)
if headphones.EMBED_LYRICS: if headphones.EMBED_LYRICS:
embedLyrics(downloaded_track_list) embedLyrics(downloaded_track_list)
if headphones.RENAME_FILES: if headphones.RENAME_FILES:
renameFiles(albumpath, downloaded_track_list, release) renameFiles(albumpath, downloaded_track_list, release)
if headphones.MOVE_FILES and not headphones.DESTINATION_DIR: if headphones.MOVE_FILES and not headphones.DESTINATION_DIR:
logger.error('No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to') logger.error('No DESTINATION_DIR has been set. Set "Destination Directory" to the parent directory you want to move the files to')
albumpaths = [albumpath] albumpaths = [albumpath]
@@ -408,9 +408,9 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
albumpaths = moveFiles(albumpath, release, tracks) albumpaths = moveFiles(albumpath, release, tracks)
else: else:
albumpaths = [albumpath] albumpaths = [albumpath]
updateFilePermissions(albumpaths) updateFilePermissions(albumpaths)
myDB = db.DBConnection() myDB = db.DBConnection()
myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid]) myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid])
myDB.action('UPDATE snatched SET status = "Processed" WHERE AlbumID=?', [albumid]) myDB.action('UPDATE snatched SET status = "Processed" WHERE AlbumID=?', [albumid])
@@ -418,7 +418,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
# Update the have tracks for all created dirs: # Update the have tracks for all created dirs:
for albumpath in albumpaths: for albumpath in albumpaths:
librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'], ArtistName=release['ArtistName']) librarysync.libraryScan(dir=albumpath, append=True, ArtistID=release['ArtistID'], ArtistName=release['ArtistName'])
logger.info(u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle'])) logger.info(u'Post-processing for %s - %s complete' % (release['ArtistName'], release['AlbumTitle']))
if headphones.GROWL_ENABLED: if headphones.GROWL_ENABLED:
@@ -432,18 +432,18 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
logger.info(u"Prowl request") logger.info(u"Prowl request")
prowl = notifiers.PROWL() prowl = notifiers.PROWL()
prowl.notify(pushmessage,"Download and Postprocessing completed") prowl.notify(pushmessage,"Download and Postprocessing completed")
if headphones.XBMC_ENABLED: if headphones.XBMC_ENABLED:
xbmc = notifiers.XBMC() xbmc = notifiers.XBMC()
if headphones.XBMC_UPDATE: if headphones.XBMC_UPDATE:
xbmc.update() xbmc.update()
if headphones.XBMC_NOTIFY: if headphones.XBMC_NOTIFY:
xbmc.notify(release['ArtistName'], release['AlbumTitle'], album_art_path) xbmc.notify(release['ArtistName'], release['AlbumTitle'], album_art_path)
if headphones.LMS_ENABLED: if headphones.LMS_ENABLED:
lms = notifiers.LMS() lms = notifiers.LMS()
lms.update() lms.update()
if headphones.PLEX_ENABLED: if headphones.PLEX_ENABLED:
plex = notifiers.Plex() plex = notifiers.Plex()
if headphones.PLEX_UPDATE: if headphones.PLEX_UPDATE:
@@ -465,7 +465,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
syno = notifiers.Synoindex() syno = notifiers.Synoindex()
for albumpath in albumpaths: for albumpath in albumpaths:
syno.notify(albumpath) syno.notify(albumpath)
if headphones.PUSHOVER_ENABLED: if headphones.PUSHOVER_ENABLED:
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle'] pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
logger.info(u"Pushover request") logger.info(u"Pushover request")
@@ -501,31 +501,31 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
def embedAlbumArt(artwork, downloaded_track_list): def embedAlbumArt(artwork, downloaded_track_list):
logger.info('Embedding album art') logger.info('Embedding album art')
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
try: try:
f = MediaFile(downloaded_track) f = MediaFile(downloaded_track)
except: except:
logger.error(u'Could not read %s. Not adding album art' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace')) logger.error(u'Could not read %s. Not adding album art' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
continue continue
logger.debug('Adding album art to: %s' % downloaded_track) logger.debug('Adding album art to: %s' % downloaded_track)
try: try:
f.art = artwork f.art = artwork
f.save() f.save()
except Exception, e: except Exception, e:
logger.error(u'Error ebedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))) logger.error(u'Error ebedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
continue continue
def addAlbumArt(artwork, albumpath, release): def addAlbumArt(artwork, albumpath, release):
logger.info('Adding album art to folder') logger.info('Adding album art to folder')
try: try:
year = release['ReleaseDate'][:4] year = release['ReleaseDate'][:4]
except TypeError: except TypeError:
year = '' year = ''
values = { '$Artist': release['ArtistName'], values = { '$Artist': release['ArtistName'],
'$Album': release['AlbumTitle'], '$Album': release['AlbumTitle'],
'$Year': year, '$Year': year,
@@ -533,7 +533,7 @@ def addAlbumArt(artwork, albumpath, release):
'$album': release['AlbumTitle'].lower(), '$album': release['AlbumTitle'].lower(),
'$year': year '$year': year
} }
album_art_name = helpers.replace_all(headphones.ALBUM_ART_FORMAT.strip(), values) + ".jpg" album_art_name = helpers.replace_all(headphones.ALBUM_ART_FORMAT.strip(), values) + ".jpg"
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(headphones.SYS_ENCODING, 'replace') album_art_name = helpers.replace_illegal_chars(album_art_name).encode(headphones.SYS_ENCODING, 'replace')
@@ -551,7 +551,7 @@ def addAlbumArt(artwork, albumpath, release):
except Exception, e: except Exception, e:
logger.error('Error saving album art: %s' % str(e)) logger.error('Error saving album art: %s' % str(e))
return return
def cleanupFiles(albumpath): def cleanupFiles(albumpath):
logger.info('Cleaning up files') logger.info('Cleaning up files')
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
@@ -562,14 +562,14 @@ def cleanupFiles(albumpath):
os.remove(os.path.join(r, files)) os.remove(os.path.join(r, files))
except Exception, e: except Exception, e:
logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e)) logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e))
def moveFiles(albumpath, release, tracks): def moveFiles(albumpath, release, tracks):
try: try:
year = release['ReleaseDate'][:4] year = release['ReleaseDate'][:4]
except TypeError: except TypeError:
year = '' year = ''
artist = release['ArtistName'].replace('/', '_') artist = release['ArtistName'].replace('/', '_')
album = release['AlbumTitle'].replace('/', '_') album = release['AlbumTitle'].replace('/', '_')
if headphones.FILE_UNDERSCORES: if headphones.FILE_UNDERSCORES:
@@ -582,12 +582,12 @@ def moveFiles(albumpath, release, tracks):
sortname = release['ArtistName'][4:] + ", The" sortname = release['ArtistName'][4:] + ", The"
else: else:
sortname = release['ArtistName'] sortname = release['ArtistName']
if sortname[0].isdigit(): if sortname[0].isdigit():
firstchar = '0-9' firstchar = '0-9'
else: else:
firstchar = sortname[0] firstchar = sortname[0]
values = { '$Artist': artist, values = { '$Artist': artist,
'$SortArtist': sortname, '$SortArtist': sortname,
@@ -602,24 +602,24 @@ def moveFiles(albumpath, release, tracks):
'$type': releasetype.lower(), '$type': releasetype.lower(),
'$first': firstchar.lower() '$first': firstchar.lower()
} }
folder = helpers.replace_all(headphones.FOLDER_FORMAT.strip(), values) folder = helpers.replace_all(headphones.FOLDER_FORMAT.strip(), values)
folder = helpers.replace_illegal_chars(folder, type="folder") folder = helpers.replace_illegal_chars(folder, type="folder")
folder = folder.replace('./', '_/').replace('/.','/_') folder = folder.replace('./', '_/').replace('/.','/_')
if folder.endswith('.'): if folder.endswith('.'):
folder = folder[:-1] + '_' folder = folder[:-1] + '_'
if folder.startswith('.'): if folder.startswith('.'):
folder = '_' + folder[1:] folder = '_' + folder[1:]
# Grab our list of files early on so we can determine if we need to create # Grab our list of files early on so we can determine if we need to create
# the lossy_dest_dir, lossless_dest_dir, or both # the lossy_dest_dir, lossless_dest_dir, or both
files_to_move = [] files_to_move = []
lossy_media = False lossy_media = False
lossless_media = False lossless_media = False
for r,d,f in os.walk(albumpath): for r,d,f in os.walk(albumpath):
for files in f: for files in f:
files_to_move.append(os.path.join(r, files)) files_to_move.append(os.path.join(r, files))
@@ -631,10 +631,10 @@ def moveFiles(albumpath, release, tracks):
# Do some sanity checking to see what directories we need to create: # Do some sanity checking to see what directories we need to create:
make_lossy_folder = False make_lossy_folder = False
make_lossless_folder = False make_lossless_folder = False
lossy_destination_path = os.path.normpath(os.path.join(headphones.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace') lossy_destination_path = os.path.normpath(os.path.join(headphones.DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace')
lossless_destination_path = os.path.normpath(os.path.join(headphones.LOSSLESS_DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace') lossless_destination_path = os.path.normpath(os.path.join(headphones.LOSSLESS_DESTINATION_DIR, folder)).encode(headphones.SYS_ENCODING, 'replace')
# If they set a destination dir for lossless media, only create the lossy folder if there is lossy media # If they set a destination dir for lossless media, only create the lossy folder if there is lossy media
if headphones.LOSSLESS_DESTINATION_DIR: if headphones.LOSSLESS_DESTINATION_DIR:
if lossy_media: if lossy_media:
@@ -646,7 +646,7 @@ def moveFiles(albumpath, release, tracks):
make_lossy_folder = True make_lossy_folder = True
last_folder = headphones.FOLDER_FORMAT.strip().split('/')[-1] last_folder = headphones.FOLDER_FORMAT.strip().split('/')[-1]
if make_lossless_folder: if make_lossless_folder:
# Only rename the folder if they use the album name, otherwise merge into existing folder # Only rename the folder if they use the album name, otherwise merge into existing folder
if os.path.exists(lossless_destination_path) and 'album' in last_folder.lower(): if os.path.exists(lossless_destination_path) and 'album' in last_folder.lower():
@@ -662,7 +662,7 @@ def moveFiles(albumpath, release, tracks):
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder: if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
temp_folder = folder temp_folder = folder
i = 1 i = 1
while True: while True:
newfolder = temp_folder + '[%i]' % i newfolder = temp_folder + '[%i]' % i
@@ -672,7 +672,7 @@ def moveFiles(albumpath, release, tracks):
else: else:
temp_folder = newfolder temp_folder = newfolder
break break
if not os.path.exists(lossless_destination_path): if not os.path.exists(lossless_destination_path):
try: try:
os.makedirs(lossless_destination_path) os.makedirs(lossless_destination_path)
@@ -680,7 +680,7 @@ def moveFiles(albumpath, release, tracks):
logger.error('Could not create lossless folder for %s. (Error: %s)' % (release['AlbumTitle'], e)) logger.error('Could not create lossless folder for %s. (Error: %s)' % (release['AlbumTitle'], e))
if not make_lossy_folder: if not make_lossy_folder:
return [albumpath] return [albumpath]
if make_lossy_folder: if make_lossy_folder:
if os.path.exists(lossy_destination_path) and 'album' in last_folder.lower(): if os.path.exists(lossy_destination_path) and 'album' in last_folder.lower():
@@ -692,10 +692,10 @@ def moveFiles(albumpath, release, tracks):
except Exception, e: except Exception, e:
logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e)) logger.error("Error deleting existing folder: %s. Creating duplicate folder. Error: %s" % (lossy_destination_path.decode(headphones.SYS_ENCODING, 'replace'), e))
create_duplicate_folder = True create_duplicate_folder = True
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder: if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
temp_folder = folder temp_folder = folder
i = 1 i = 1
while True: while True:
newfolder = temp_folder + '[%i]' % i newfolder = temp_folder + '[%i]' % i
@@ -705,35 +705,35 @@ def moveFiles(albumpath, release, tracks):
else: else:
temp_folder = newfolder temp_folder = newfolder
break break
if not os.path.exists(lossy_destination_path): if not os.path.exists(lossy_destination_path):
try: try:
os.makedirs(lossy_destination_path) os.makedirs(lossy_destination_path)
except Exception, e: except Exception, e:
logger.error('Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e)) logger.error('Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e))
return [albumpath] return [albumpath]
logger.info('Checking which files we need to move.....') logger.info('Checking which files we need to move.....')
# Move files to the destination folder, renaming them if they already exist # Move files to the destination folder, renaming them if they already exist
# If we have two desination_dirs, move non-music files to both # If we have two desination_dirs, move non-music files to both
if make_lossy_folder and make_lossless_folder: if make_lossy_folder and make_lossless_folder:
for file_to_move in files_to_move: for file_to_move in files_to_move:
if any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS): if any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
helpers.smartMove(file_to_move, lossy_destination_path) helpers.smartMove(file_to_move, lossy_destination_path)
elif any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS): elif any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
helpers.smartMove(file_to_move, lossless_destination_path) helpers.smartMove(file_to_move, lossless_destination_path)
# If it's a non-music file, move it to both dirs # If it's a non-music file, move it to both dirs
# TODO: Move specific-to-lossless files to the lossless dir only # TODO: Move specific-to-lossless files to the lossless dir only
else: else:
moved_to_lossy_folder = helpers.smartMove(file_to_move, lossy_destination_path, delete=False) moved_to_lossy_folder = helpers.smartMove(file_to_move, lossy_destination_path, delete=False)
moved_to_lossless_folder = helpers.smartMove(file_to_move, lossless_destination_path, delete=False) moved_to_lossless_folder = helpers.smartMove(file_to_move, lossless_destination_path, delete=False)
if moved_to_lossy_folder or moved_to_lossless_folder: if moved_to_lossy_folder or moved_to_lossless_folder:
try: try:
os.remove(file_to_move) os.remove(file_to_move)
@@ -741,62 +741,62 @@ def moveFiles(albumpath, release, tracks):
logger.error("Error deleting file '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "' from source directory") logger.error("Error deleting file '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "' from source directory")
else: else:
logger.error("Error copying '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "'. Not deleting from download directory") logger.error("Error copying '" + file_to_move.decode(headphones.SYS_ENCODING, 'replace') + "'. Not deleting from download directory")
elif make_lossless_folder and not make_lossy_folder: elif make_lossless_folder and not make_lossy_folder:
for file_to_move in files_to_move: for file_to_move in files_to_move:
helpers.smartMove(file_to_move, lossless_destination_path) helpers.smartMove(file_to_move, lossless_destination_path)
else: else:
for file_to_move in files_to_move: for file_to_move in files_to_move:
helpers.smartMove(file_to_move, lossy_destination_path) helpers.smartMove(file_to_move, lossy_destination_path)
# Chmod the directories using the folder_format (script courtesy of premiso!) # Chmod the directories using the folder_format (script courtesy of premiso!)
folder_list = folder.split('/') folder_list = folder.split('/')
temp_fs = [] temp_fs = []
if make_lossless_folder: if make_lossless_folder:
temp_fs.append(headphones.LOSSLESS_DESTINATION_DIR) temp_fs.append(headphones.LOSSLESS_DESTINATION_DIR)
if make_lossy_folder: if make_lossy_folder:
temp_fs.append(headphones.DESTINATION_DIR) temp_fs.append(headphones.DESTINATION_DIR)
for temp_f in temp_fs: for temp_f in temp_fs:
for f in folder_list: for f in folder_list:
temp_f = os.path.join(temp_f, f) temp_f = os.path.join(temp_f, f)
try: try:
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8)) os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8))
except Exception, e: except Exception, e:
logger.error("Error trying to change permissions on folder: %s. %s", temp_f, e) logger.error("Error trying to change permissions on folder: %s. %s", temp_f, e)
# If we failed to move all the files out of the directory, this will fail too # If we failed to move all the files out of the directory, this will fail too
try: try:
shutil.rmtree(albumpath) shutil.rmtree(albumpath)
except Exception, e: except Exception, e:
logger.error('Could not remove directory: %s. %s', albumpath, e) logger.error('Could not remove directory: %s. %s', albumpath, e)
destination_paths = [] destination_paths = []
if make_lossy_folder: if make_lossy_folder:
destination_paths.append(lossy_destination_path) destination_paths.append(lossy_destination_path)
if make_lossless_folder: if make_lossless_folder:
destination_paths.append(lossless_destination_path) destination_paths.append(lossless_destination_path)
return destination_paths return destination_paths
def correctMetadata(albumid, release, downloaded_track_list): def correctMetadata(albumid, release, downloaded_track_list):
logger.info('Preparing to write metadata to tracks....') logger.info('Preparing to write metadata to tracks....')
lossy_items = [] lossy_items = []
lossless_items = [] lossless_items = []
# Process lossless & lossy media formats separately # Process lossless & lossy media formats separately
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
try: try:
if any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS): if any(downloaded_track.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
@@ -806,14 +806,14 @@ def correctMetadata(albumid, release, downloaded_track_list):
else: else:
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace')) logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
except Exception, e: except Exception, e:
logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)) logger.error("Beets couldn't create an Item from: %s - not a media file? %s", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e))
for items in [lossy_items, lossless_items]: for items in [lossy_items, lossless_items]:
if not items: if not items:
continue continue
try: try:
cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle'])) cur_artist, cur_album, candidates, rec = autotag.tag_album(items, search_artist=helpers.latinToAscii(release['ArtistName']), search_album=helpers.latinToAscii(release['AlbumTitle']))
except Exception, e: except Exception, e:
@@ -822,29 +822,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
if str(rec) == 'recommendation.none': if str(rec) == 'recommendation.none':
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle']) logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return return
if candidates: if candidates:
dist, info, mapping, extra_items, extra_tracks = candidates[0] dist, info, mapping, extra_items, extra_tracks = candidates[0]
else: else:
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle']) logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
return return
logger.info('Beets recommendation for tagging items: %s' % rec) logger.info('Beets recommendation for tagging items: %s' % rec)
# TODO: Handle extra_items & extra_tracks # TODO: Handle extra_items & extra_tracks
autotag.apply_metadata(info, mapping) autotag.apply_metadata(info, mapping)
for item in items: for item in items:
try: try:
item.write() item.write()
logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace')) logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace'))
except Exception, e: except Exception, e:
logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e)) logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
def embedLyrics(downloaded_track_list): def embedLyrics(downloaded_track_list):
logger.info('Adding lyrics') logger.info('Adding lyrics')
# TODO: If adding lyrics for flac & lossy, only fetch the lyrics once # TODO: If adding lyrics for flac & lossy, only fetch the lyrics once
# and apply it to both files # and apply it to both files
for downloaded_track in downloaded_track_list: for downloaded_track in downloaded_track_list:
@@ -854,7 +854,7 @@ def embedLyrics(downloaded_track_list):
except: except:
logger.error('Could not read %s. Not checking lyrics', track_title) logger.error('Could not read %s. Not checking lyrics', track_title)
continue continue
if f.albumartist and f.title: if f.albumartist and f.title:
metalyrics = lyrics.getLyrics(f.albumartist, f.title) metalyrics = lyrics.getLyrics(f.albumartist, f.title)
elif f.artist and f.title: elif f.artist and f.title:
@@ -862,7 +862,7 @@ def embedLyrics(downloaded_track_list):
else: else:
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics', track_title) logger.info('No artist/track metadata found for track: %s. Not fetching lyrics', track_title)
metalyrics = None metalyrics = None
if metalyrics: if metalyrics:
logger.debug('Adding lyrics to: %s', track_title) logger.debug('Adding lyrics to: %s', track_title)
f.lyrics = metalyrics f.lyrics = metalyrics
@@ -899,28 +899,28 @@ def renameFiles(albumpath, downloaded_track_list, release):
tracknumber = '' tracknumber = ''
else: else:
tracknumber = '%02d' % f.track tracknumber = '%02d' % f.track
if not f.title: if not f.title:
basename = os.path.basename(downloaded_track.decode(headphones.SYS_ENCODING, 'replace')) basename = os.path.basename(downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
title = os.path.splitext(basename)[0] title = os.path.splitext(basename)[0]
ext = os.path.splitext(basename)[1] ext = os.path.splitext(basename)[1]
new_file_name = helpers.cleanTitle(title) + ext new_file_name = helpers.cleanTitle(title) + ext
else: else:
title = f.title title = f.title
if release['ArtistName'] == "Various Artists" and f.artist: if release['ArtistName'] == "Various Artists" and f.artist:
artistname = f.artist artistname = f.artist
else: else:
artistname = release['ArtistName'] artistname = release['ArtistName']
if artistname.startswith('The '): if artistname.startswith('The '):
sortname = artistname[4:] + ", The" sortname = artistname[4:] + ", The"
else: else:
sortname = artistname sortname = artistname
values = { '$Disc': discnumber, values = { '$Disc': discnumber,
'$Track': tracknumber, '$Track': tracknumber,
'$Title': title, '$Title': title,
@@ -936,12 +936,12 @@ def renameFiles(albumpath, downloaded_track_list, release):
'$album': release['AlbumTitle'].lower(), '$album': release['AlbumTitle'].lower(),
'$year': year '$year': year
} }
ext = os.path.splitext(downloaded_track)[1] ext = os.path.splitext(downloaded_track)[1]
new_file_name = helpers.replace_all(headphones.FILE_FORMAT.strip(), values).replace('/','_') + ext new_file_name = helpers.replace_all(headphones.FILE_FORMAT.strip(), values).replace('/','_') + ext
new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING, 'replace') new_file_name = helpers.replace_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING, 'replace')
if headphones.FILE_UNDERSCORES: if headphones.FILE_UNDERSCORES:
@@ -949,9 +949,9 @@ def renameFiles(albumpath, downloaded_track_list, release):
if new_file_name.startswith('.'): if new_file_name.startswith('.'):
new_file_name = new_file_name.replace(".", "_", 1) new_file_name = new_file_name.replace(".", "_", 1)
new_file = os.path.join(albumpath, new_file_name) new_file = os.path.join(albumpath, new_file_name)
if downloaded_track == new_file_name: if downloaded_track == new_file_name:
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary") logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
continue continue
@@ -962,7 +962,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
except Exception, e: except Exception, e:
logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e) logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
continue continue
def updateFilePermissions(albumpaths): def updateFilePermissions(albumpaths):
for folder in albumpaths: for folder in albumpaths:
@@ -977,21 +977,21 @@ def updateFilePermissions(albumpaths):
continue continue
def renameUnprocessedFolder(albumpath): def renameUnprocessedFolder(albumpath):
i = 0 i = 0
while True: while True:
if i == 0: if i == 0:
new_folder_name = albumpath + ' (Unprocessed)' new_folder_name = albumpath + ' (Unprocessed)'
else: else:
new_folder_name = albumpath + ' (Unprocessed)[%i]' % i new_folder_name = albumpath + ' (Unprocessed)[%i]' % i
if os.path.exists(new_folder_name): if os.path.exists(new_folder_name):
i += 1 i += 1
else: else:
os.rename(albumpath, new_folder_name) os.rename(albumpath, new_folder_name)
return return
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None): def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
if album_dir: if album_dir:
@@ -1035,7 +1035,7 @@ def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
# Parse the folder names to get artist album info # Parse the folder names to get artist album info
myDB = db.DBConnection() myDB = db.DBConnection()
for folder in folders: for folder in folders:
folder_basename = os.path.basename(folder).decode(headphones.SYS_ENCODING, 'replace') folder_basename = os.path.basename(folder).decode(headphones.SYS_ENCODING, 'replace')
logger.info('Processing: %s', folder_basename) logger.info('Processing: %s', folder_basename)
+2 -2
View File
@@ -610,7 +610,7 @@ def send_to_downloader(data, bestqual, album):
# Get torrent name from .torrent, this is usually used by the torrent client as the folder name # Get torrent name from .torrent, this is usually used by the torrent client as the folder name
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent' torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
download_path = os.path.join(headphones.TORRENTBLACKHOLE_DIR, torrent_name) download_path = os.path.join(headphones.TORRENTBLACKHOLE_DIR, torrent_name)
if bestqual[2].startswith("magnet:"): if bestqual[2].startswith("magnet:"):
if headphones.OPEN_MAGNET_LINKS: if headphones.OPEN_MAGNET_LINKS:
try: try:
@@ -689,7 +689,7 @@ def send_to_downloader(data, bestqual, album):
else:# if headphones.TORRENT_DOWNLOADER == 2: else:# if headphones.TORRENT_DOWNLOADER == 2:
logger.info("Sending torrent to uTorrent") logger.info("Sending torrent to uTorrent")
# rutracker needs cookies to be set, pass the .torrent file instead of url # rutracker needs cookies to be set, pass the .torrent file instead of url
if bestqual[3] == 'rutracker.org': if bestqual[3] == 'rutracker.org':
file_or_url, _hash = rutracker.get_torrent(bestqual[2]) file_or_url, _hash = rutracker.get_torrent(bestqual[2])
+40 -40
View File
@@ -24,7 +24,7 @@ class Rutracker():
# Stores a number of login attempts to prevent recursion. # Stores a number of login attempts to prevent recursion.
#login_counter = 0 #login_counter = 0
def __init__(self): def __init__(self):
self.cookiejar = cookielib.CookieJar() self.cookiejar = cookielib.CookieJar()
@@ -40,11 +40,11 @@ class Rutracker():
return False return False
#self.login_counter += 1 #self.login_counter += 1
# No recursion wanted. # No recursion wanted.
#if self.login_counter > 1: #if self.login_counter > 1:
# return False # return False
params = urllib.urlencode({"login_username" : login, params = urllib.urlencode({"login_username" : login,
"login_password" : password, "login_password" : password,
"login" : "Вход"}) "login" : "Вход"})
@@ -53,19 +53,19 @@ class Rutracker():
self.opener.open("http://login.rutracker.org/forum/login.php", params) self.opener.open("http://login.rutracker.org/forum/login.php", params)
except : except :
pass pass
# Check if we're logged in # Check if we're logged in
for cookie in self.cookiejar: for cookie in self.cookiejar:
if cookie.name == 'bb_data': if cookie.name == 'bb_data':
self.logged_in = True self.logged_in = True
return self.logged_in return self.logged_in
def searchurl(self, artist, album, year, format): def searchurl(self, artist, album, year, format):
""" """
Return the search url Return the search url
""" """
# Build search url # Build search url
searchterm = '' searchterm = ''
if artist != 'Various Artists': if artist != 'Various Artists':
@@ -74,69 +74,69 @@ class Rutracker():
searchterm = searchterm + album searchterm = searchterm + album
searchterm = searchterm + ' ' searchterm = searchterm + ' '
searchterm = searchterm + year searchterm = searchterm + year
providerurl = "http://rutracker.org/forum/tracker.php" providerurl = "http://rutracker.org/forum/tracker.php"
if format == 'lossless': if format == 'lossless':
format = '+lossless' format = '+lossless'
elif format == 'lossless+mp3': elif format == 'lossless+mp3':
format = '+lossless||mp3||aac' format = '+lossless||mp3||aac'
else: else:
format = '+mp3||aac' format = '+mp3||aac'
# sort by size, descending. # sort by size, descending.
sort = '&o=7&s=2' sort = '&o=7&s=2'
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort) searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
return searchurl return searchurl
def search(self, searchurl, maxsize, minseeders, albumid): def search(self, searchurl, maxsize, minseeders, albumid):
""" """
Parse the search results and return valid torrent list Parse the search results and return valid torrent list
""" """
titles = [] titles = []
urls = [] urls = []
seeders = [] seeders = []
sizes = [] sizes = []
torrentlist = [] torrentlist = []
rulist = [] rulist = []
try: try:
page = self.opener.open(searchurl, timeout=60) page = self.opener.open(searchurl, timeout=60)
soup = BeautifulSoup(page.read()) soup = BeautifulSoup(page.read())
# Debug # Debug
#logger.debug (soup.prettify()) #logger.debug (soup.prettify())
# Title # Title
for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}): for link in soup.find_all('a', attrs={'class' : 'med tLink hl-tags bold'}):
title = link.get_text() title = link.get_text()
titles.append(title) titles.append(title)
# Download URL # Download URL
for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}): for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}):
url = link.get('href') url = link.get('href')
urls.append(url) urls.append(url)
# Seeders # Seeders
for link in soup.find_all('b', attrs={'class' : 'seedmed'}): for link in soup.find_all('b', attrs={'class' : 'seedmed'}):
seeder = link.get_text() seeder = link.get_text()
seeders.append(seeder) seeders.append(seeder)
# Size # Size
for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}): for link in soup.find_all('td', attrs={'class' : 'row4 small nowrap tor-size'}):
size = link.u.string size = link.u.string
sizes.append(size) sizes.append(size)
except : except :
pass pass
# Combine lists # Combine lists
torrentlist = zip(titles, urls, seeders, sizes) torrentlist = zip(titles, urls, seeders, sizes)
# return if nothing found # return if nothing found
if not torrentlist: if not torrentlist:
return False return False
@@ -151,20 +151,20 @@ class Rutracker():
myDB = db.DBConnection() myDB = db.DBConnection()
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid]) tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
hptrackcount = len(tracks) hptrackcount = len(tracks)
if not hptrackcount: if not hptrackcount:
logger.info('headphones track info not found, cannot compare to torrent') logger.info('headphones track info not found, cannot compare to torrent')
return False return False
# Return all valid entries, ignored, required words now checked in searcher.py # Return all valid entries, ignored, required words now checked in searcher.py
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd'] #unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif'] formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive'] deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
for torrent in torrentlist: for torrent in torrentlist:
returntitle = torrent[0].encode('utf-8') returntitle = torrent[0].encode('utf-8')
url = torrent[1] url = torrent[1]
seeders = torrent[2] seeders = torrent[2]
@@ -183,11 +183,11 @@ class Rutracker():
# Check torrent info # Check torrent info
self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)) self.cookiejar.set_cookie(cookielib.Cookie(version=0, name='bb_dl', value=torrent_id, port=None, port_specified=False, domain='.rutracker.org', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False))
# Debug # Debug
#for cookie in self.cookiejar: #for cookie in self.cookiejar:
# logger.debug ('Cookie: %s' % cookie) # logger.debug ('Cookie: %s' % cookie)
try: try:
page = self.opener.open(url) page = self.opener.open(url)
torrent = page.read() torrent = page.read()
@@ -198,11 +198,11 @@ class Rutracker():
except Exception, e: except Exception, e:
logger.error('Error getting torrent: %s' % e) logger.error('Error getting torrent: %s' % e)
return False return False
# get torrent track count and check for cue # get torrent track count and check for cue
trackcount = 0 trackcount = 0
cuecount = 0 cuecount = 0
if 'files' in metainfo: # multi if 'files' in metainfo: # multi
for pathfile in metainfo['files']: for pathfile in metainfo['files']:
path = pathfile['path'] path = pathfile['path']
@@ -240,22 +240,22 @@ class Rutracker():
else: else:
break break
totallogcount = totallogcount + logcount totallogcount = totallogcount + logcount
if totallogcount > 0: if totallogcount > 0:
trackcount = totallogcount trackcount = totallogcount
logger.debug ('rutracker logtrackcount: %s' % totallogcount) logger.debug ('rutracker logtrackcount: %s' % totallogcount)
# If torrent track count = hp track count then return torrent, # If torrent track count = hp track count then return torrent,
# if greater, check for deluxe/special/foreign editions # if greater, check for deluxe/special/foreign editions
# if less, then allow if it's a single track with a cue # if less, then allow if it's a single track with a cue
valid = False valid = False
if trackcount == hptrackcount: if trackcount == hptrackcount:
valid = True valid = True
elif trackcount > hptrackcount: elif trackcount > hptrackcount:
if any(deluxe in title for deluxe in deluxelist): if any(deluxe in title for deluxe in deluxelist):
valid = True valid = True
# Add to list # Add to list
if valid: if valid:
rulist.append((returntitle, size, topicurl)) rulist.append((returntitle, size, topicurl))
+3 -3
View File
@@ -23,10 +23,10 @@ def dbUpdate(forcefull=False):
activeartists = myDB.select('SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC') activeartists = myDB.select('SELECT ArtistID, ArtistName from artists WHERE Status="Active" or Status="Loading" order by LastUpdated ASC')
logger.info('Starting update for %i active artists' % len(activeartists)) logger.info('Starting update for %i active artists' % len(activeartists))
for artist in activeartists: for artist in activeartists:
artistid = artist[0] artistid = artist[0]
importer.addArtisttoDB(artistid=artistid, extrasonly=False, forcefull=forcefull) importer.addArtisttoDB(artistid=artistid, extrasonly=False, forcefull=forcefull)
logger.info('Active artist update complete') logger.info('Active artist update complete')
+1 -1
View File
@@ -27,7 +27,7 @@ class utorrentclient(object):
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"]) UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
def __init__(self, base_url = None, username = None, password = None,): def __init__(self, base_url = None, username = None, password = None,):
host = headphones.UTORRENT_HOST host = headphones.UTORRENT_HOST
if not host.startswith('http'): if not host.startswith('http'):
host = 'http://' + host host = 'http://' + host
+5 -5
View File
@@ -324,9 +324,9 @@ class WebInterface(object):
def choose_specific_download(self, AlbumID): def choose_specific_download(self, AlbumID):
results = searcher.searchforalbum(AlbumID, choose_specific_download=True) results = searcher.searchforalbum(AlbumID, choose_specific_download=True)
results_as_dicts = [] results_as_dicts = []
for result in results: for result in results:
result_dict = { result_dict = {
@@ -341,7 +341,7 @@ class WebInterface(object):
s = simplejson.dumps(results_as_dicts) s = simplejson.dumps(results_as_dicts)
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
return s return s
choose_specific_download.exposed = True choose_specific_download.exposed = True
def download_specific_release(self, AlbumID, title, size, url, provider, kind, **kwargs): def download_specific_release(self, AlbumID, title, size, url, provider, kind, **kwargs):
@@ -878,9 +878,9 @@ class WebInterface(object):
def getArtistjson(self, ArtistID, **kwargs): def getArtistjson(self, ArtistID, **kwargs):
myDB = db.DBConnection() myDB = db.DBConnection()
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone() artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
artist_json = json.dumps({ artist_json = json.dumps({
'ArtistName': artist['ArtistName'], 'ArtistName': artist['ArtistName'],
'Status': artist['Status'] 'Status': artist['Status']
}) })
return artist_json return artist_json
getArtistjson.exposed=True getArtistjson.exposed=True
+10 -10
View File
@@ -43,7 +43,7 @@ def initialize(options={}):
logger.warn(u"Disabled HTTPS because of missing CERT and KEY files") logger.warn(u"Disabled HTTPS because of missing CERT and KEY files")
headphones.ENABLE_HTTPS = False headphones.ENABLE_HTTPS = False
enable_https = False enable_https = False
options_dict = { options_dict = {
'log.screen': False, 'log.screen': False,
'server.thread_pool': 10, 'server.thread_pool': 10,
@@ -54,14 +54,14 @@ def initialize(options={}):
'tools.encode.encoding' : 'utf-8', 'tools.encode.encoding' : 'utf-8',
'tools.decode.on' : True, 'tools.decode.on' : True,
} }
if enable_https: if enable_https:
options_dict['server.ssl_certificate'] = https_cert options_dict['server.ssl_certificate'] = https_cert
options_dict['server.ssl_private_key'] = https_key options_dict['server.ssl_private_key'] = https_key
protocol = "https" protocol = "https"
else: else:
protocol = "http" protocol = "http"
logger.info("Starting Headphones on %s://%s:%d/", protocol, options['http_host'], options['http_port']) logger.info("Starting Headphones on %s://%s:%d/", protocol, options['http_host'], options['http_port'])
cherrypy.config.update(options_dict) cherrypy.config.update(options_dict)
@@ -95,7 +95,7 @@ def initialize(options={}):
'tools.staticdir.dir': headphones.CACHE_DIR 'tools.staticdir.dir': headphones.CACHE_DIR
} }
} }
if options['http_password'] != "": if options['http_password'] != "":
conf['/'].update({ conf['/'].update({
'tools.auth_basic.on': True, 'tools.auth_basic.on': True,
@@ -104,20 +104,20 @@ def initialize(options={}):
{options['http_username']:options['http_password']}) {options['http_username']:options['http_password']})
}) })
conf['/api'] = { 'tools.auth_basic.on': False } conf['/api'] = { 'tools.auth_basic.on': False }
# Prevent time-outs # Prevent time-outs
cherrypy.engine.timeout_monitor.unsubscribe() cherrypy.engine.timeout_monitor.unsubscribe()
cherrypy.tree.mount(WebInterface(), options['http_root'], config = conf) cherrypy.tree.mount(WebInterface(), options['http_root'], config = conf)
try: try:
cherrypy.process.servers.check_port(options['http_host'], options['http_port']) cherrypy.process.servers.check_port(options['http_host'], options['http_port'])
cherrypy.server.start() cherrypy.server.start()
except IOError: except IOError:
print 'Failed to start on port: %i. Is something else running?' % (options['http_port']) print 'Failed to start on port: %i. Is something else running?' % (options['http_port'])
sys.exit(0) sys.exit(0)
cherrypy.server.wait() cherrypy.server.wait()
+1 -1
View File
@@ -8,7 +8,7 @@ from lib.apscheduler.jobstores.base import JobStore
class RAMJobStore(JobStore): class RAMJobStore(JobStore):
def __init__(self): def __init__(self):
self.jobs = [] self.jobs = []
def add_job(self, job): def add_job(self, job):
self.jobs.append(job) self.jobs.append(job)
+1 -1
View File
@@ -242,6 +242,6 @@ def apply_metadata(album_info, mapping):
item[field] = value item[field] = value
if track_info.disctitle is not None: if track_info.disctitle is not None:
item.disctitle = track_info.disctitle item.disctitle = track_info.disctitle
# Headphones seal of approval # Headphones seal of approval
item.comments = 'tagged by headphones/beets' item.comments = 'tagged by headphones/beets'
+3 -3
View File
@@ -147,7 +147,7 @@ def transform_value(value):
return float(value) return float(value)
except ValueError: except ValueError:
pass pass
return value return value
def transform_data(data): def transform_data(data):
@@ -209,7 +209,7 @@ def transform_data(data):
if section == 'importfeeds': if section == 'importfeeds':
if key.startswith(IMPORTFEEDS_PREFIX): if key.startswith(IMPORTFEEDS_PREFIX):
key = key[len(IMPORTFEEDS_PREFIX):] key = key[len(IMPORTFEEDS_PREFIX):]
sec_out[key] = transform_value(value) sec_out[key] = transform_value(value)
return out return out
@@ -313,7 +313,7 @@ def migrate_db(replace=False):
# Old DB does not exist or we're configured to point to the same # Old DB does not exist or we're configured to point to the same
# database. Do nothing. # database. Do nothing.
return return
if os.path.exists(destfn): if os.path.exists(destfn):
if replace: if replace:
log.debug(u'moving old database aside: {0}'.format( log.debug(u'moving old database aside: {0}'.format(
+1 -1
View File
@@ -56,7 +56,7 @@ class HumanReadableException(Exception):
gerund = self.verb[:-1] if self.verb.endswith('e') else self.verb gerund = self.verb[:-1] if self.verb.endswith('e') else self.verb
gerund += 'ing' gerund += 'ing'
return gerund return gerund
def _reasonstr(self): def _reasonstr(self):
"""Get the reason as a string.""" """Get the reason as a string."""
if isinstance(self.reason, unicode): if isinstance(self.reason, unicode):
+1 -1
View File
@@ -299,7 +299,7 @@ class EncodingDetector:
else: else:
xml_endpos = 1024 xml_endpos = 1024
html_endpos = max(2048, int(len(markup) * 0.05)) html_endpos = max(2048, int(len(markup) * 0.05))
declared_encoding = None declared_encoding = None
declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos) declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos)
if not declared_encoding_match and is_html: if not declared_encoding_match and is_html:
+2 -2
View File
@@ -135,7 +135,7 @@ def rword(length=5):
def rsentence(length=4): def rsentence(length=4):
"Generate a random sentence-like string." "Generate a random sentence-like string."
return " ".join(rword(random.randint(4,9)) for i in range(length)) return " ".join(rword(random.randint(4,9)) for i in range(length))
def rdoc(num_elements=1000): def rdoc(num_elements=1000):
"""Randomly generate an invalid HTML document.""" """Randomly generate an invalid HTML document."""
tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table'] tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table']
@@ -159,7 +159,7 @@ def benchmark_parsers(num_elements=100000):
print "Comparative parser benchmark on Beautiful Soup %s" % __version__ print "Comparative parser benchmark on Beautiful Soup %s" % __version__
data = rdoc(num_elements) data = rdoc(num_elements)
print "Generated a large invalid HTML document (%d bytes)." % len(data) print "Generated a large invalid HTML document (%d bytes)." % len(data)
for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]: for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]:
success = False success = False
try: try:
+70 -70
View File
@@ -95,20 +95,20 @@ engine.listeners['before_request'] = set()
engine.listeners['after_request'] = set() engine.listeners['after_request'] = set()
class _TimeoutMonitor(process.plugins.Monitor): class _TimeoutMonitor(process.plugins.Monitor):
def __init__(self, bus): def __init__(self, bus):
self.servings = [] self.servings = []
process.plugins.Monitor.__init__(self, bus, self.run) process.plugins.Monitor.__init__(self, bus, self.run)
def before_request(self): def before_request(self):
self.servings.append((serving.request, serving.response)) self.servings.append((serving.request, serving.response))
def after_request(self): def after_request(self):
try: try:
self.servings.remove((serving.request, serving.response)) self.servings.remove((serving.request, serving.response))
except ValueError: except ValueError:
pass pass
def run(self): def run(self):
"""Check timeout on all responses. (Internal)""" """Check timeout on all responses. (Internal)"""
for req, resp in self.servings: for req, resp in self.servings:
@@ -132,7 +132,7 @@ server.subscribe()
def quickstart(root=None, script_name="", config=None): def quickstart(root=None, script_name="", config=None):
"""Mount the given root, start the builtin server (and engine), then block. """Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler root: an instance of a "controller class" (a collection of page handler
methods) which represents the root of the application. methods) which represents the root of the application.
script_name: a string containing the "mount point" of the application. script_name: a string containing the "mount point" of the application.
@@ -140,7 +140,7 @@ def quickstart(root=None, script_name="", config=None):
at which to mount the given root. For example, if root.index() will at which to mount the given root. For example, if root.index() will
handle requests to "http://www.example.com:8080/dept/app1/", then handle requests to "http://www.example.com:8080/dept/app1/", then
the script_name argument would be "/dept/app1". the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the root It MUST NOT end in a slash. If the script_name refers to the root
of the URI, it MUST be an empty string (not "/"). of the URI, it MUST be an empty string (not "/").
config: a file or dict containing application config. If this contains config: a file or dict containing application config. If this contains
@@ -149,14 +149,14 @@ def quickstart(root=None, script_name="", config=None):
""" """
if config: if config:
_global_conf_alias.update(config) _global_conf_alias.update(config)
tree.mount(root, script_name, config) tree.mount(root, script_name, config)
if hasattr(engine, "signal_handler"): if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe() engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"): if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe() engine.console_control_handler.subscribe()
engine.start() engine.start()
engine.block() engine.block()
@@ -165,7 +165,7 @@ from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local): class _Serving(_local):
"""An interface for registering request and response objects. """An interface for registering request and response objects.
Rather than have a separate "thread local" object for the request and Rather than have a separate "thread local" object for the request and
the response, this class works as a single threadlocal container for the response, this class works as a single threadlocal container for
both objects (and any others which developers wish to define). In this both objects (and any others which developers wish to define). In this
@@ -173,22 +173,22 @@ class _Serving(_local):
conversation, yet still refer to them as module-level globals in a conversation, yet still refer to them as module-level globals in a
thread-safe way. thread-safe way.
""" """
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
_httputil.Host("127.0.0.1", 1111)) _httputil.Host("127.0.0.1", 1111))
""" """
The request object for the current thread. In the main thread, The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None.""" and any threads which are not receiving HTTP requests, this is None."""
response = _cprequest.Response() response = _cprequest.Response()
""" """
The response object for the current thread. In the main thread, The response object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None.""" and any threads which are not receiving HTTP requests, this is None."""
def load(self, request, response): def load(self, request, response):
self.request = request self.request = request
self.response = response self.response = response
def clear(self): def clear(self):
"""Remove all attributes of self.""" """Remove all attributes of self."""
self.__dict__.clear() self.__dict__.clear()
@@ -197,54 +197,54 @@ serving = _Serving()
class _ThreadLocalProxy(object): class _ThreadLocalProxy(object):
__slots__ = ['__attrname__', '__dict__'] __slots__ = ['__attrname__', '__dict__']
def __init__(self, attrname): def __init__(self, attrname):
self.__attrname__ = attrname self.__attrname__ = attrname
def __getattr__(self, name): def __getattr__(self, name):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
return getattr(child, name) return getattr(child, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name in ("__attrname__", ): if name in ("__attrname__", ):
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
else: else:
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
setattr(child, name, value) setattr(child, name, value)
def __delattr__(self, name): def __delattr__(self, name):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
delattr(child, name) delattr(child, name)
def _get_dict(self): def _get_dict(self):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
d = child.__class__.__dict__.copy() d = child.__class__.__dict__.copy()
d.update(child.__dict__) d.update(child.__dict__)
return d return d
__dict__ = property(_get_dict) __dict__ = property(_get_dict)
def __getitem__(self, key): def __getitem__(self, key):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
return child[key] return child[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
child[key] = value child[key] = value
def __delitem__(self, key): def __delitem__(self, key):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
del child[key] del child[key]
def __contains__(self, key): def __contains__(self, key):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
return key in child return key in child
def __len__(self): def __len__(self):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
return len(child) return len(child)
def __nonzero__(self): def __nonzero__(self):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
return bool(child) return bool(child)
@@ -285,14 +285,14 @@ from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager): class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate. """A site-wide LogManager; routes to app.log or global log as appropriate.
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
cherrypy.log() and cherrypy.log.access(). If either cherrypy.log() and cherrypy.log.access(). If either
function is called during a request, the message will be sent to the function is called during a request, the message will be sent to the
logger for the current Application. If they are called outside of a logger for the current Application. If they are called outside of a
request, the message will be sent to the site-wide logger. request, the message will be sent to the site-wide logger.
""" """
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log as appropriate.""" """Log the given message to the app.log or global log as appropriate."""
# Do NOT use try/except here. See http://www.cherrypy.org/ticket/945 # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
@@ -301,7 +301,7 @@ class _GlobalLogManager(_cplogging.LogManager):
else: else:
log = self log = self
return log.error(*args, **kwargs) return log.error(*args, **kwargs)
def access(self): def access(self):
"""Log an access message to the app.log or global log as appropriate.""" """Log an access message to the app.log or global log as appropriate."""
try: try:
@@ -335,7 +335,7 @@ def expose(func=None, alias=None):
for a in alias: for a in alias:
parents[a.replace(".", "_")] = func parents[a.replace(".", "_")] = func
return func return func
import sys, types import sys, types
if isinstance(func, (types.FunctionType, types.MethodType)): if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None: if alias is None:
@@ -364,23 +364,23 @@ def expose(func=None, alias=None):
return expose_ return expose_
def popargs(*args, **kwargs): def popargs(*args, **kwargs):
"""A decorator for _cp_dispatch """A decorator for _cp_dispatch
(cherrypy.dispatch.Dispatcher.dispatch_method_name). (cherrypy.dispatch.Dispatcher.dispatch_method_name).
Optional keyword argument: handler=(Object or Function) Optional keyword argument: handler=(Object or Function)
Provides a _cp_dispatch function that pops off path segments into Provides a _cp_dispatch function that pops off path segments into
cherrypy.request.params under the names specified. The dispatch cherrypy.request.params under the names specified. The dispatch
is then forwarded on to the next vpath element. is then forwarded on to the next vpath element.
Note that any existing (and exposed) member function of the class that Note that any existing (and exposed) member function of the class that
popargs is applied to will override that value of the argument. For popargs is applied to will override that value of the argument. For
instance, if you have a method named "list" on the class decorated with instance, if you have a method named "list" on the class decorated with
popargs, then accessing "/list" will call that function instead of popping popargs, then accessing "/list" will call that function instead of popping
it off as the requested parameter. This restriction applies to all it off as the requested parameter. This restriction applies to all
_cp_dispatch functions. The only way around this restriction is to create _cp_dispatch functions. The only way around this restriction is to create
a "blank class" whose only function is to provide _cp_dispatch. a "blank class" whose only function is to provide _cp_dispatch.
If there are path elements after the arguments, or more arguments If there are path elements after the arguments, or more arguments
are requested than are available in the vpath, then the 'handler' are requested than are available in the vpath, then the 'handler'
keyword argument specifies the next object to handle the parameterized keyword argument specifies the next object to handle the parameterized
@@ -389,9 +389,9 @@ def popargs(*args, **kwargs):
will be called with the args specified and the return value from that will be called with the args specified and the return value from that
function used as the next object INSTEAD of adding the parameters to function used as the next object INSTEAD of adding the parameters to
cherrypy.request.args. cherrypy.request.args.
This decorator may be used in one of two ways: This decorator may be used in one of two ways:
As a class decorator: As a class decorator:
@cherrypy.popargs('year', 'month', 'day') @cherrypy.popargs('year', 'month', 'day')
class Blog: class Blog:
@@ -399,47 +399,47 @@ def popargs(*args, **kwargs):
#Process the parameters here; any url like #Process the parameters here; any url like
#/, /2009, /2009/12, or /2009/12/31 #/, /2009, /2009/12, or /2009/12/31
#will fill in the appropriate parameters. #will fill in the appropriate parameters.
def create(self): def create(self):
#This link will still be available at /create. Defined functions #This link will still be available at /create. Defined functions
#take precedence over arguments. #take precedence over arguments.
Or as a member of a class: Or as a member of a class:
class Blog: class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day') _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#... #...
The handler argument may be used to mix arguments with built in functions. The handler argument may be used to mix arguments with built in functions.
For instance, the following setup allows different activities at the For instance, the following setup allows different activities at the
day, month, and year level: day, month, and year level:
class DayHandler: class DayHandler:
def index(self, year, month, day): def index(self, year, month, day):
#Do something with this day; probably list entries #Do something with this day; probably list entries
def delete(self, year, month, day): def delete(self, year, month, day):
#Delete all entries for this day #Delete all entries for this day
@cherrypy.popargs('day', handler=DayHandler()) @cherrypy.popargs('day', handler=DayHandler())
class MonthHandler: class MonthHandler:
def index(self, year, month): def index(self, year, month):
#Do something with this month; probably list entries #Do something with this month; probably list entries
def delete(self, year, month): def delete(self, year, month):
#Delete all entries for this month #Delete all entries for this month
@cherrypy.popargs('month', handler=MonthHandler()) @cherrypy.popargs('month', handler=MonthHandler())
class YearHandler: class YearHandler:
def index(self, year): def index(self, year):
#Do something with this year #Do something with this year
#... #...
@cherrypy.popargs('year', handler=YearHandler()) @cherrypy.popargs('year', handler=YearHandler())
class Root: class Root:
def index(self): def index(self):
#... #...
""" """
#Since keyword arg comes after *args, we have to process it ourselves #Since keyword arg comes after *args, we have to process it ourselves
@@ -461,14 +461,14 @@ def popargs(*args, **kwargs):
if handler is not None \ if handler is not None \
and (hasattr(handler, '__call__') or inspect.isclass(handler)): and (hasattr(handler, '__call__') or inspect.isclass(handler)):
handler_call = True handler_call = True
def decorated(cls_or_self=None, vpath=None): def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self): if inspect.isclass(cls_or_self):
#cherrypy.popargs is a class decorator #cherrypy.popargs is a class decorator
cls = cls_or_self cls = cls_or_self
setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated) setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
return cls return cls
#We're in the actual function #We're in the actual function
self = cls_or_self self = cls_or_self
parms = {} parms = {}
@@ -476,16 +476,16 @@ def popargs(*args, **kwargs):
if not vpath: if not vpath:
break break
parms[arg] = vpath.pop(0) parms[arg] = vpath.pop(0)
if handler is not None: if handler is not None:
if handler_call: if handler_call:
return handler(**parms) return handler(**parms)
else: else:
request.params.update(parms) request.params.update(parms)
return handler return handler
request.params.update(parms) request.params.update(parms)
#If we are the ultimate handler, then to prevent our _cp_dispatch #If we are the ultimate handler, then to prevent our _cp_dispatch
#from being called again, we will resolve remaining elements through #from being called again, we will resolve remaining elements through
#getattr() directly. #getattr() directly.
@@ -493,28 +493,28 @@ def popargs(*args, **kwargs):
return getattr(self, vpath.pop(0), None) return getattr(self, vpath.pop(0), None)
else: else:
return self return self
return decorated return decorated
def url(path="", qs="", script_name=None, base=None, relative=None): def url(path="", qs="", script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path. """Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return If 'path' starts with a slash ('/'), this will return
(base + script_name + path + qs). (base + script_name + path + qs).
If it does not start with a slash, this returns If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs). (base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used If script_name is None, cherrypy.request will be used
to find a script_name, if available. to find a script_name, if available.
If base is None, cherrypy.request.base will be used (if available). If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this. Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL Finally, note that this function can be used to obtain an absolute URL
for the current request path (minus the querystring) by passing no args. for the current request path (minus the querystring) by passing no args.
If you call url(qs=cherrypy.request.query_string), you should get the If you call url(qs=cherrypy.request.query_string), you should get the
original browser URL (assuming no internal redirections). original browser URL (assuming no internal redirections).
If relative is None or not provided, request.app.relative_urls will If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an be used (if available, else False). If False, the output will be an
absolute URL (including the scheme, host, vhost, and script_name). absolute URL (including the scheme, host, vhost, and script_name).
@@ -527,7 +527,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
qs = _urlencode(qs) qs = _urlencode(qs)
if qs: if qs:
qs = '?' + qs qs = '?' + qs
if request.app: if request.app:
if not path.startswith("/"): if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed # Append/remove trailing slash from path_info as needed
@@ -540,17 +540,17 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
elif request.is_index is False: elif request.is_index is False:
if pi.endswith('/') and pi != '/': if pi.endswith('/') and pi != '/':
pi = pi[:-1] pi = pi[:-1]
if path == "": if path == "":
path = pi path = pi
else: else:
path = _urljoin(pi, path) path = _urljoin(pi, path)
if script_name is None: if script_name is None:
script_name = request.script_name script_name = request.script_name
if base is None: if base is None:
base = request.base base = request.base
newurl = base + script_name + path + qs newurl = base + script_name + path + qs
else: else:
# No request.app (we're being called outside a request). # No request.app (we're being called outside a request).
@@ -559,10 +559,10 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
# if you're using vhosts or tools.proxy. # if you're using vhosts or tools.proxy.
if base is None: if base is None:
base = server.base() base = server.base()
path = (script_name or "") + path path = (script_name or "") + path
newurl = base + path + qs newurl = base + path + qs
if './' in newurl: if './' in newurl:
# Normalize the URL by removing ./ and ../ # Normalize the URL by removing ./ and ../
atoms = [] atoms = []
@@ -574,12 +574,12 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
else: else:
atoms.append(atom) atoms.append(atom)
newurl = '/'.join(atoms) newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL. # At this point, we should have a fully-qualified absolute URL.
if relative is None: if relative is None:
relative = getattr(request.app, "relative_urls", False) relative = getattr(request.app, "relative_urls", False)
# See http://www.ietf.org/rfc/rfc2396.txt # See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server': if relative == 'server':
# "A relative reference beginning with a single slash character is # "A relative reference beginning with a single slash character is
@@ -599,7 +599,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
new.pop(0) new.pop(0)
new = (['..'] * len(old)) + new new = (['..'] * len(old)) + new
newurl = '/'.join(new) newurl = '/'.join(new)
return newurl return newurl
+42 -42
View File
@@ -7,25 +7,25 @@ from cherrypy._cpcompat import iteritems, copykeys, builtins
class Checker(object): class Checker(object):
"""A checker for CherryPy sites and their mounted applications. """A checker for CherryPy sites and their mounted applications.
When this object is called at engine startup, it executes each When this object is called at engine startup, it executes each
of its own methods whose names start with ``check_``. If you wish of its own methods whose names start with ``check_``. If you wish
to disable selected checks, simply add a line in your global to disable selected checks, simply add a line in your global
config which sets the appropriate method to False:: config which sets the appropriate method to False::
[global] [global]
checker.check_skipped_app_config = False checker.check_skipped_app_config = False
You may also dynamically add or replace ``check_*`` methods in this way. You may also dynamically add or replace ``check_*`` methods in this way.
""" """
on = True on = True
"""If True (the default), run all checks; if False, turn off all checks.""" """If True (the default), run all checks; if False, turn off all checks."""
def __init__(self): def __init__(self):
self._populate_known_types() self._populate_known_types()
def __call__(self): def __call__(self):
"""Run all check_* methods.""" """Run all check_* methods."""
if self.on: if self.on:
@@ -39,14 +39,14 @@ class Checker(object):
method() method()
finally: finally:
warnings.formatwarning = oldformatwarning warnings.formatwarning = oldformatwarning
def formatwarning(self, message, category, filename, lineno, line=None): def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning.""" """Function to format a warning."""
return "CherryPy Checker:\n%s\n\n" % message return "CherryPy Checker:\n%s\n\n" % message
# This value should be set inside _cpconfig. # This value should be set inside _cpconfig.
global_config_contained_paths = False global_config_contained_paths = False
def check_app_config_entries_dont_start_with_script_name(self): def check_app_config_entries_dont_start_with_script_name(self):
"""Check for Application config with sections that repeat script_name.""" """Check for Application config with sections that repeat script_name."""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
@@ -63,13 +63,13 @@ class Checker(object):
warnings.warn( warnings.warn(
"The application mounted at %r has config " \ "The application mounted at %r has config " \
"entries that start with its script name: %r" % (sn, key)) "entries that start with its script name: %r" % (sn, key))
def check_site_config_entries_in_app_config(self): def check_site_config_entries_in_app_config(self):
"""Check for mounted Applications that have site-scoped config.""" """Check for mounted Applications that have site-scoped config."""
for sn, app in iteritems(cherrypy.tree.apps): for sn, app in iteritems(cherrypy.tree.apps):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
msg = [] msg = []
for section, entries in iteritems(app.config): for section, entries in iteritems(app.config):
if section.startswith('/'): if section.startswith('/'):
@@ -84,7 +84,7 @@ class Checker(object):
"config. Move them to a [global] section and pass them " "config. Move them to a [global] section and pass them "
"to cherrypy.config.update() instead of tree.mount()." % sn) "to cherrypy.config.update() instead of tree.mount()." % sn)
warnings.warn(os.linesep.join(msg)) warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self): def check_skipped_app_config(self):
"""Check for mounted Applications that have no config.""" """Check for mounted Applications that have no config."""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
@@ -100,7 +100,7 @@ class Checker(object):
"cherrypy.tree.mount(..., config=app_config)") "cherrypy.tree.mount(..., config=app_config)")
warnings.warn(msg) warnings.warn(msg)
return return
def check_app_config_brackets(self): def check_app_config_brackets(self):
"""Check for Application config with extraneous brackets in section names.""" """Check for Application config with extraneous brackets in section names."""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
@@ -115,7 +115,7 @@ class Checker(object):
"section names with extraneous brackets: %r. " "section names with extraneous brackets: %r. "
"Config *files* need brackets; config *dicts* " "Config *files* need brackets; config *dicts* "
"(e.g. passed to tree.mount) do not." % (sn, key)) "(e.g. passed to tree.mount) do not." % (sn, key))
def check_static_paths(self): def check_static_paths(self):
"""Check Application config for incorrect static paths.""" """Check Application config for incorrect static paths."""
# Use the dummy Request object in the main thread. # Use the dummy Request object in the main thread.
@@ -128,7 +128,7 @@ class Checker(object):
# get_resource will populate request.config # get_resource will populate request.config
request.get_resource(section + "/dummy.html") request.get_resource(section + "/dummy.html")
conf = request.config.get conf = request.config.get
if conf("tools.staticdir.on", False): if conf("tools.staticdir.on", False):
msg = "" msg = ""
root = conf("tools.staticdir.root") root = conf("tools.staticdir.root")
@@ -154,20 +154,20 @@ class Checker(object):
fulldir = os.path.join(root, dir) fulldir = os.path.join(root, dir)
if not os.path.isabs(fulldir): if not os.path.isabs(fulldir):
msg = "%r is not an absolute path." % fulldir msg = "%r is not an absolute path." % fulldir
if fulldir and not os.path.exists(fulldir): if fulldir and not os.path.exists(fulldir):
if msg: if msg:
msg += "\n" msg += "\n"
msg += ("%r (root + dir) is not an existing " msg += ("%r (root + dir) is not an existing "
"filesystem path." % fulldir) "filesystem path." % fulldir)
if msg: if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
% (msg, section, root, dir)) % (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- # # -------------------------- Compatibility -------------------------- #
obsolete = { obsolete = {
'server.default_content_type': 'tools.response_headers.headers', 'server.default_content_type': 'tools.response_headers.headers',
'log_access_file': 'log.access_file', 'log_access_file': 'log.access_file',
@@ -181,9 +181,9 @@ class Checker(object):
'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' 'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
'cherrypy.Application(Root())))'), 'cherrypy.Application(Root())))'),
} }
deprecated = {} deprecated = {}
def _compat(self, config): def _compat(self, config):
"""Process config and warn on each obsolete or deprecated entry.""" """Process config and warn on each obsolete or deprecated entry."""
for section, conf in config.items(): for section, conf in config.items():
@@ -204,7 +204,7 @@ class Checker(object):
elif section in self.deprecated: elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead." warnings.warn("%r is deprecated. Use %r instead."
% (section, self.deprecated[section])) % (section, self.deprecated[section]))
def check_compatibility(self): def check_compatibility(self):
"""Process config and warn on each obsolete or deprecated entry.""" """Process config and warn on each obsolete or deprecated entry."""
self._compat(cherrypy.config) self._compat(cherrypy.config)
@@ -212,12 +212,12 @@ class Checker(object):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
self._compat(app.config) self._compat(app.config)
# ------------------------ Known Namespaces ------------------------ # # ------------------------ Known Namespaces ------------------------ #
extra_config_namespaces = [] extra_config_namespaces = []
def _known_ns(self, app): def _known_ns(self, app):
ns = ["wsgi"] ns = ["wsgi"]
ns.extend(copykeys(app.toolboxes)) ns.extend(copykeys(app.toolboxes))
@@ -225,7 +225,7 @@ class Checker(object):
ns.extend(copykeys(app.request_class.namespaces)) ns.extend(copykeys(app.request_class.namespaces))
ns.extend(copykeys(cherrypy.config.namespaces)) ns.extend(copykeys(cherrypy.config.namespaces))
ns += self.extra_config_namespaces ns += self.extra_config_namespaces
for section, conf in app.config.items(): for section, conf in app.config.items():
is_path_section = section.startswith("/") is_path_section = section.startswith("/")
if is_path_section and isinstance(conf, dict): if is_path_section and isinstance(conf, dict):
@@ -250,7 +250,7 @@ class Checker(object):
"because the %r tool was not found.\n" "because the %r tool was not found.\n"
"section: [%s]" % (k, atoms[1], section)) "section: [%s]" % (k, atoms[1], section))
warnings.warn(msg) warnings.warn(msg)
def check_config_namespaces(self): def check_config_namespaces(self):
"""Process config and warn on each unknown config namespace.""" """Process config and warn on each unknown config namespace."""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
@@ -259,16 +259,16 @@ class Checker(object):
self._known_ns(app) self._known_ns(app)
# -------------------------- Config Types -------------------------- # # -------------------------- Config Types -------------------------- #
known_config_types = {} known_config_types = {}
def _populate_known_types(self): def _populate_known_types(self):
b = [x for x in vars(builtins).values() b = [x for x in vars(builtins).values()
if type(x) is type(str)] if type(x) is type(str)]
def traverse(obj, namespace): def traverse(obj, namespace):
for name in dir(obj): for name in dir(obj):
# Hack for 3.2's warning about body_params # Hack for 3.2's warning about body_params
@@ -277,17 +277,17 @@ class Checker(object):
vtype = type(getattr(obj, name, None)) vtype = type(getattr(obj, name, None))
if vtype in b: if vtype in b:
self.known_config_types[namespace + "." + name] = vtype self.known_config_types[namespace + "." + name] = vtype
traverse(cherrypy.request, "request") traverse(cherrypy.request, "request")
traverse(cherrypy.response, "response") traverse(cherrypy.response, "response")
traverse(cherrypy.server, "server") traverse(cherrypy.server, "server")
traverse(cherrypy.engine, "engine") traverse(cherrypy.engine, "engine")
traverse(cherrypy.log, "log") traverse(cherrypy.log, "log")
def _known_types(self, config): def _known_types(self, config):
msg = ("The config entry %r in section %r is of type %r, " msg = ("The config entry %r in section %r is of type %r, "
"which does not match the expected type %r.") "which does not match the expected type %r.")
for section, conf in config.items(): for section, conf in config.items():
if isinstance(conf, dict): if isinstance(conf, dict):
for k, v in conf.items(): for k, v in conf.items():
@@ -305,7 +305,7 @@ class Checker(object):
if expected_type and vtype != expected_type: if expected_type and vtype != expected_type:
warnings.warn(msg % (k, section, vtype.__name__, warnings.warn(msg % (k, section, vtype.__name__,
expected_type.__name__)) expected_type.__name__))
def check_config_types(self): def check_config_types(self):
"""Assert that config values are of the same type as default values.""" """Assert that config values are of the same type as default values."""
self._known_types(cherrypy.config) self._known_types(cherrypy.config)
@@ -313,10 +313,10 @@ class Checker(object):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
self._known_types(app.config) self._known_types(app.config)
# -------------------- Specific config warnings -------------------- # # -------------------- Specific config warnings -------------------- #
def check_localhost(self): def check_localhost(self):
"""Warn if any socket_host is 'localhost'. See #711.""" """Warn if any socket_host is 'localhost'. See #711."""
for k, v in cherrypy.config.items(): for k, v in cherrypy.config.items():
+7 -7
View File
@@ -50,14 +50,14 @@ attribute. For example::
class Demo: class Demo:
_cp_config = {'tools.gzip.on': True} _cp_config = {'tools.gzip.on': True}
def index(self): def index(self):
return "Hello world" return "Hello world"
index.exposed = True index.exposed = True
index._cp_config = {'request.show_tracebacks': False} index._cp_config = {'request.show_tracebacks': False}
.. note:: .. note::
This behavior is only guaranteed for the default dispatcher. This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where Other dispatchers may have different restrictions on where
you can attach _cp_config attributes. you can attach _cp_config attributes.
@@ -127,13 +127,13 @@ NamespaceSet = reprconf.NamespaceSet
def merge(base, other): def merge(base, other):
"""Merge one app config (from a dict, file, or filename) into another. """Merge one app config (from a dict, file, or filename) into another.
If the given config is a filename, it will be appended to If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes. the list of files to monitor for "autoreload" changes.
""" """
if isinstance(other, basestring): if isinstance(other, basestring):
cherrypy.engine.autoreload.files.add(other) cherrypy.engine.autoreload.files.add(other)
# Load other into base # Load other into base
for section, value_map in reprconf.as_dict(other).items(): for section, value_map in reprconf.as_dict(other).items():
if not isinstance(value_map, dict): if not isinstance(value_map, dict):
@@ -164,7 +164,7 @@ class Config(reprconf.Config):
if 'tools.staticdir.dir' in config: if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global" config['tools.staticdir.section'] = "global"
reprconf.Config._apply(self, config) reprconf.Config._apply(self, config)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Decorator for page handlers to set _cp_config.""" """Decorator for page handlers to set _cp_config."""
if args: if args:
@@ -226,14 +226,14 @@ def _server_namespace_handler(k, v):
# to configure additional HTTP servers. # to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"): if not hasattr(cherrypy, "servers"):
cherrypy.servers = {} cherrypy.servers = {}
servername, k = atoms servername, k = atoms
if servername not in cherrypy.servers: if servername not in cherrypy.servers:
from cherrypy import _cpserver from cherrypy import _cpserver
cherrypy.servers[servername] = _cpserver.Server() cherrypy.servers[servername] = _cpserver.Server()
# On by default, but 'on = False' can unsubscribe it (see below). # On by default, but 'on = False' can unsubscribe it (see below).
cherrypy.servers[servername].subscribe() cherrypy.servers[servername].subscribe()
if k == 'on': if k == 'on':
if v: if v:
cherrypy.servers[servername].subscribe() cherrypy.servers[servername].subscribe()
+67 -67
View File
@@ -23,12 +23,12 @@ from cherrypy._cpcompat import set
class PageHandler(object): class PageHandler(object):
"""Callable which sets response.body.""" """Callable which sets response.body."""
def __init__(self, callable, *args, **kwargs): def __init__(self, callable, *args, **kwargs):
self.callable = callable self.callable = callable
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
def __call__(self): def __call__(self):
try: try:
return self.callable(*self.args, **self.kwargs) return self.callable(*self.args, **self.kwargs)
@@ -70,7 +70,7 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if isinstance(callable, object) and hasattr(callable, '__call__'): if isinstance(callable, object) and hasattr(callable, '__call__'):
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__) (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
else: else:
# If it wasn't one of our own types, re-raise # If it wasn't one of our own types, re-raise
# the original error # the original error
raise raise
@@ -117,10 +117,10 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# 2. not enough body parameters -> 400 # 2. not enough body parameters -> 400
# 3. not enough path parts (partial matches) -> 404 # 3. not enough path parts (partial matches) -> 404
# #
# We can't actually tell which case it is, # We can't actually tell which case it is,
# so I'm raising a 404 because that covers 2/3 of the # so I'm raising a 404 because that covers 2/3 of the
# possibilities # possibilities
# #
# In the case where the method does not allow body # In the case where the method does not allow body
# arguments it's definitely a 404. # arguments it's definitely a 404.
message = None message = None
@@ -187,16 +187,16 @@ class LateParamPageHandler(PageHandler):
takes that into account, and allows request.params to be 'bound late' takes that into account, and allows request.params to be 'bound late'
(it's more complicated than that, but that's the effect). (it's more complicated than that, but that's the effect).
""" """
def _get_kwargs(self): def _get_kwargs(self):
kwargs = cherrypy.serving.request.params.copy() kwargs = cherrypy.serving.request.params.copy()
if self._kwargs: if self._kwargs:
kwargs.update(self._kwargs) kwargs.update(self._kwargs)
return kwargs return kwargs
def _set_kwargs(self, kwargs): def _set_kwargs(self, kwargs):
self._kwargs = kwargs self._kwargs = kwargs
kwargs = property(_get_kwargs, _set_kwargs, kwargs = property(_get_kwargs, _set_kwargs,
doc='page handler kwargs (with ' doc='page handler kwargs (with '
'cherrypy.request.params copied in)') 'cherrypy.request.params copied in)')
@@ -217,7 +217,7 @@ else:
class Dispatcher(object): class Dispatcher(object):
"""CherryPy Dispatcher which walks a tree of objects to find a handler. """CherryPy Dispatcher which walks a tree of objects to find a handler.
The tree is rooted at cherrypy.request.app.root, and each hierarchical The tree is rooted at cherrypy.request.app.root, and each hierarchical
component in the path_info argument is matched to a corresponding nested component in the path_info argument is matched to a corresponding nested
attribute of the root object. Matching handlers must have an 'exposed' attribute of the root object. Matching handlers must have an 'exposed'
@@ -225,16 +225,16 @@ class Dispatcher(object):
matches a URI which ends in a slash ("/"). The special method name matches a URI which ends in a slash ("/"). The special method name
"default" may match a portion of the path_info (but only when no longer "default" may match a portion of the path_info (but only when no longer
substring of the path_info matches some other object). substring of the path_info matches some other object).
This is the default, built-in dispatcher for CherryPy. This is the default, built-in dispatcher for CherryPy.
""" """
dispatch_method_name = '_cp_dispatch' dispatch_method_name = '_cp_dispatch'
""" """
The name of the dispatch method that nodes may optionally implement The name of the dispatch method that nodes may optionally implement
to provide their own dynamic dispatch algorithm. to provide their own dynamic dispatch algorithm.
""" """
def __init__(self, dispatch_method_name=None, def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores): translate=punctuation_to_underscores):
validate_translator(translate) validate_translator(translate)
@@ -246,27 +246,27 @@ class Dispatcher(object):
"""Set handler and config for the current request.""" """Set handler and config for the current request."""
request = cherrypy.serving.request request = cherrypy.serving.request
func, vpath = self.find_handler(path_info) func, vpath = self.find_handler(path_info)
if func: if func:
# Decode any leftover %2F in the virtual_path atoms. # Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath] vpath = [x.replace("%2F", "/") for x in vpath]
request.handler = LateParamPageHandler(func, *vpath) request.handler = LateParamPageHandler(func, *vpath)
else: else:
request.handler = cherrypy.NotFound() request.handler = cherrypy.NotFound()
def find_handler(self, path): def find_handler(self, path):
"""Return the appropriate page handler, plus any virtual path. """Return the appropriate page handler, plus any virtual path.
This will return two objects. The first will be a callable, This will return two objects. The first will be a callable,
which can be used to generate page output. Any parameters from which can be used to generate page output. Any parameters from
the query string or request body will be sent to that callable the query string or request body will be sent to that callable
as keyword arguments. as keyword arguments.
The callable is found by traversing the application's tree, The callable is found by traversing the application's tree,
starting from cherrypy.request.app.root, and matching path starting from cherrypy.request.app.root, and matching path
components to successive objects in the tree. For example, the components to successive objects in the tree. For example, the
URL "/path/to/handler" might return root.path.to.handler. URL "/path/to/handler" might return root.path.to.handler.
The second object returned will be a list of names which are The second object returned will be a list of names which are
'virtual path' components: parts of the URL which are dynamic, 'virtual path' components: parts of the URL which are dynamic,
and were not used when looking up the handler. and were not used when looking up the handler.
@@ -277,7 +277,7 @@ class Dispatcher(object):
app = request.app app = request.app
root = app.root root = app.root
dispatch_name = self.dispatch_method_name dispatch_name = self.dispatch_method_name
# Get config for the root object/path. # Get config for the root object/path.
fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
fullpath_len = len(fullpath) fullpath_len = len(fullpath)
@@ -288,14 +288,14 @@ class Dispatcher(object):
if "/" in app.config: if "/" in app.config:
nodeconf.update(app.config["/"]) nodeconf.update(app.config["/"])
object_trail = [['root', root, nodeconf, segleft]] object_trail = [['root', root, nodeconf, segleft]]
node = root node = root
iternames = fullpath[:] iternames = fullpath[:]
while iternames: while iternames:
name = iternames[0] name = iternames[0]
# map to legal Python identifiers (e.g. replace '.' with '_') # map to legal Python identifiers (e.g. replace '.' with '_')
objname = name.translate(self.translate) objname = name.translate(self.translate)
nodeconf = {} nodeconf = {}
subnode = getattr(node, objname, None) subnode = getattr(node, objname, None)
pre_len = len(iternames) pre_len = len(iternames)
@@ -327,7 +327,7 @@ class Dispatcher(object):
) )
elif segleft == pre_len: elif segleft == pre_len:
#Assume that the handler used the current path segment, but #Assume that the handler used the current path segment, but
#did not pop it. This allows things like #did not pop it. This allows things like
#return getattr(self, vpath[0], None) #return getattr(self, vpath[0], None)
iternames.pop(0) iternames.pop(0)
segleft -= 1 segleft -= 1
@@ -337,7 +337,7 @@ class Dispatcher(object):
# Get _cp_config attached to this node. # Get _cp_config attached to this node.
if hasattr(node, "_cp_config"): if hasattr(node, "_cp_config"):
nodeconf.update(node._cp_config) nodeconf.update(node._cp_config)
# Mix in values from app.config for this path. # Mix in values from app.config for this path.
existing_len = fullpath_len - pre_len existing_len = fullpath_len - pre_len
if existing_len != 0: if existing_len != 0:
@@ -349,9 +349,9 @@ class Dispatcher(object):
curpath += '/' + seg curpath += '/' + seg
if curpath in app.config: if curpath in app.config:
nodeconf.update(app.config[curpath]) nodeconf.update(app.config[curpath])
object_trail.append([name, node, nodeconf, segleft]) object_trail.append([name, node, nodeconf, segleft])
def set_conf(): def set_conf():
"""Collapse all object_trail config into cherrypy.request.config.""" """Collapse all object_trail config into cherrypy.request.config."""
base = cherrypy.config.copy() base = cherrypy.config.copy()
@@ -362,15 +362,15 @@ class Dispatcher(object):
if 'tools.staticdir.dir' in conf: if 'tools.staticdir.dir' in conf:
base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft]) base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
return base return base
# Try successive objects (reverse order) # Try successive objects (reverse order)
num_candidates = len(object_trail) - 1 num_candidates = len(object_trail) - 1
for i in range(num_candidates, -1, -1): for i in range(num_candidates, -1, -1):
name, candidate, nodeconf, segleft = object_trail[i] name, candidate, nodeconf, segleft = object_trail[i]
if candidate is None: if candidate is None:
continue continue
# Try a "default" method on the current leaf. # Try a "default" method on the current leaf.
if hasattr(candidate, "default"): if hasattr(candidate, "default"):
defhandler = candidate.default defhandler = candidate.default
@@ -382,10 +382,10 @@ class Dispatcher(object):
# See http://www.cherrypy.org/ticket/613 # See http://www.cherrypy.org/ticket/613
request.is_index = path.endswith("/") request.is_index = path.endswith("/")
return defhandler, fullpath[fullpath_len - segleft:-1] return defhandler, fullpath[fullpath_len - segleft:-1]
# Uncomment the next line to restrict positional params to "default". # Uncomment the next line to restrict positional params to "default".
# if i < num_candidates - 2: continue # if i < num_candidates - 2: continue
# Try the current leaf. # Try the current leaf.
if getattr(candidate, 'exposed', False): if getattr(candidate, 'exposed', False):
request.config = set_conf() request.config = set_conf()
@@ -400,7 +400,7 @@ class Dispatcher(object):
# positional parameters (virtual paths). # positional parameters (virtual paths).
request.is_index = False request.is_index = False
return candidate, fullpath[fullpath_len - segleft:-1] return candidate, fullpath[fullpath_len - segleft:-1]
# We didn't find anything # We didn't find anything
request.config = set_conf() request.config = set_conf()
return None, [] return None, []
@@ -408,20 +408,20 @@ class Dispatcher(object):
class MethodDispatcher(Dispatcher): class MethodDispatcher(Dispatcher):
"""Additional dispatch based on cherrypy.request.method.upper(). """Additional dispatch based on cherrypy.request.method.upper().
Methods named GET, POST, etc will be called on an exposed class. Methods named GET, POST, etc will be called on an exposed class.
The method names must be all caps; the appropriate Allow header The method names must be all caps; the appropriate Allow header
will be output showing all capitalized method names as allowable will be output showing all capitalized method names as allowable
HTTP verbs. HTTP verbs.
Note that the containing class must be exposed, not the methods. Note that the containing class must be exposed, not the methods.
""" """
def __call__(self, path_info): def __call__(self, path_info):
"""Set handler and config for the current request.""" """Set handler and config for the current request."""
request = cherrypy.serving.request request = cherrypy.serving.request
resource, vpath = self.find_handler(path_info) resource, vpath = self.find_handler(path_info)
if resource: if resource:
# Set Allow header # Set Allow header
avail = [m for m in dir(resource) if m.isupper()] avail = [m for m in dir(resource) if m.isupper()]
@@ -429,7 +429,7 @@ class MethodDispatcher(Dispatcher):
avail.append("HEAD") avail.append("HEAD")
avail.sort() avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail) cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
# Find the subhandler # Find the subhandler
meth = request.method.upper() meth = request.method.upper()
func = getattr(resource, meth, None) func = getattr(resource, meth, None)
@@ -439,7 +439,7 @@ class MethodDispatcher(Dispatcher):
# Grab any _cp_config on the subhandler. # Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"): if hasattr(func, "_cp_config"):
request.config.update(func._cp_config) request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms. # Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath] vpath = [x.replace("%2F", "/") for x in vpath]
request.handler = LateParamPageHandler(func, *vpath) request.handler = LateParamPageHandler(func, *vpath)
@@ -451,7 +451,7 @@ class MethodDispatcher(Dispatcher):
class RoutesDispatcher(object): class RoutesDispatcher(object):
"""A Routes based dispatcher for CherryPy.""" """A Routes based dispatcher for CherryPy."""
def __init__(self, full_result=False): def __init__(self, full_result=False):
""" """
Routes dispatcher Routes dispatcher
@@ -465,14 +465,14 @@ class RoutesDispatcher(object):
self.controllers = {} self.controllers = {}
self.mapper = routes.Mapper() self.mapper = routes.Mapper()
self.mapper.controller_scan = self.controllers.keys self.mapper.controller_scan = self.controllers.keys
def connect(self, name, route, controller, **kwargs): def connect(self, name, route, controller, **kwargs):
self.controllers[name] = controller self.controllers[name] = controller
self.mapper.connect(name, route, controller=name, **kwargs) self.mapper.connect(name, route, controller=name, **kwargs)
def redirect(self, url): def redirect(self, url):
raise cherrypy.HTTPRedirect(url) raise cherrypy.HTTPRedirect(url)
def __call__(self, path_info): def __call__(self, path_info):
"""Set handler and config for the current request.""" """Set handler and config for the current request."""
func = self.find_handler(path_info) func = self.find_handler(path_info)
@@ -480,13 +480,13 @@ class RoutesDispatcher(object):
cherrypy.serving.request.handler = LateParamPageHandler(func) cherrypy.serving.request.handler = LateParamPageHandler(func)
else: else:
cherrypy.serving.request.handler = cherrypy.NotFound() cherrypy.serving.request.handler = cherrypy.NotFound()
def find_handler(self, path_info): def find_handler(self, path_info):
"""Find the right page handler, and set request.config.""" """Find the right page handler, and set request.config."""
import routes import routes
request = cherrypy.serving.request request = cherrypy.serving.request
config = routes.request_config() config = routes.request_config()
config.mapper = self.mapper config.mapper = self.mapper
if hasattr(request, 'wsgi_environ'): if hasattr(request, 'wsgi_environ'):
@@ -494,9 +494,9 @@ class RoutesDispatcher(object):
config.host = request.headers.get('Host', None) config.host = request.headers.get('Host', None)
config.protocol = request.scheme config.protocol = request.scheme
config.redirect = self.redirect config.redirect = self.redirect
result = self.mapper.match(path_info) result = self.mapper.match(path_info)
config.mapper_dict = result config.mapper_dict = result
params = {} params = {}
if result: if result:
@@ -505,23 +505,23 @@ class RoutesDispatcher(object):
params.pop('controller', None) params.pop('controller', None)
params.pop('action', None) params.pop('action', None)
request.params.update(params) request.params.update(params)
# Get config for the root object/path. # Get config for the root object/path.
request.config = base = cherrypy.config.copy() request.config = base = cherrypy.config.copy()
curpath = "" curpath = ""
def merge(nodeconf): def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf: if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/" nodeconf['tools.staticdir.section'] = curpath or "/"
base.update(nodeconf) base.update(nodeconf)
app = request.app app = request.app
root = app.root root = app.root
if hasattr(root, "_cp_config"): if hasattr(root, "_cp_config"):
merge(root._cp_config) merge(root._cp_config)
if "/" in app.config: if "/" in app.config:
merge(app.config["/"]) merge(app.config["/"])
# Mix in values from app.config. # Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x] atoms = [x for x in path_info.split("/") if x]
if atoms: if atoms:
@@ -532,7 +532,7 @@ class RoutesDispatcher(object):
curpath = "/".join((curpath, atom)) curpath = "/".join((curpath, atom))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
handler = None handler = None
if result: if result:
controller = result.get('controller') controller = result.get('controller')
@@ -543,23 +543,23 @@ class RoutesDispatcher(object):
# Get config from the controller. # Get config from the controller.
if hasattr(controller, "_cp_config"): if hasattr(controller, "_cp_config"):
merge(controller._cp_config) merge(controller._cp_config)
action = result.get('action') action = result.get('action')
if action is not None: if action is not None:
handler = getattr(controller, action, None) handler = getattr(controller, action, None)
# Get config from the handler # Get config from the handler
if hasattr(handler, "_cp_config"): if hasattr(handler, "_cp_config"):
merge(handler._cp_config) merge(handler._cp_config)
else: else:
handler = controller handler = controller
# Do the last path atom here so it can # Do the last path atom here so it can
# override the controller's _cp_config. # override the controller's _cp_config.
if last: if last:
curpath = "/".join((curpath, last)) curpath = "/".join((curpath, last))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
return handler return handler
@@ -574,33 +574,33 @@ def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains): def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
""" """
Select a different handler based on the Host header. Select a different handler based on the Host header.
This can be useful when running multiple sites within one CP server. This can be useful when running multiple sites within one CP server.
It allows several domains to point to different parts of a single It allows several domains to point to different parts of a single
website structure. For example:: website structure. For example::
http://www.domain.example -> root http://www.domain.example -> root
http://www.domain2.example -> root/domain2/ http://www.domain2.example -> root/domain2/
http://www.domain2.example:443 -> root/secure http://www.domain2.example:443 -> root/secure
can be accomplished via the following config:: can be accomplished via the following config::
[/] [/]
request.dispatch = cherrypy.dispatch.VirtualHost( request.dispatch = cherrypy.dispatch.VirtualHost(
**{'www.domain2.example': '/domain2', **{'www.domain2.example': '/domain2',
'www.domain2.example:443': '/secure', 'www.domain2.example:443': '/secure',
}) })
next_dispatcher next_dispatcher
The next dispatcher object in the dispatch chain. The next dispatcher object in the dispatch chain.
The VirtualHost dispatcher adds a prefix to the URL and calls The VirtualHost dispatcher adds a prefix to the URL and calls
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
use_x_forwarded_host use_x_forwarded_host
If True (the default), any "X-Forwarded-Host" If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying. is commonly added by HTTP servers (such as Apache) when proxying.
``**domains`` ``**domains``
A dict of {host header value: virtual prefix} pairs. A dict of {host header value: virtual prefix} pairs.
The incoming "Host" request header is looked up in this dict, The incoming "Host" request header is looked up in this dict,
@@ -614,23 +614,23 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai
def vhost_dispatch(path_info): def vhost_dispatch(path_info):
request = cherrypy.serving.request request = cherrypy.serving.request
header = request.headers.get header = request.headers.get
domain = header('Host', '') domain = header('Host', '')
if use_x_forwarded_host: if use_x_forwarded_host:
domain = header("X-Forwarded-Host", domain) domain = header("X-Forwarded-Host", domain)
prefix = domains.get(domain, "") prefix = domains.get(domain, "")
if prefix: if prefix:
path_info = httputil.urljoin(prefix, path_info) path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info) result = next_dispatcher(path_info)
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614. # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
section = request.config.get('tools.staticdir.section') section = request.config.get('tools.staticdir.section')
if section: if section:
section = section[len(prefix):] section = section[len(prefix):]
request.config['tools.staticdir.section'] = section request.config['tools.staticdir.section'] = section
return result return result
return vhost_dispatch return vhost_dispatch
+61 -61
View File
@@ -123,37 +123,37 @@ class TimeoutError(CherryPyException):
class InternalRedirect(CherryPyException): class InternalRedirect(CherryPyException):
"""Exception raised to switch to the handler for a different URL. """Exception raised to switch to the handler for a different URL.
This exception will redirect processing to another path within the site This exception will redirect processing to another path within the site
(without informing the client). Provide the new path as an argument when (without informing the client). Provide the new path as an argument when
raising the exception. Provide any params in the querystring for the new URL. raising the exception. Provide any params in the querystring for the new URL.
""" """
def __init__(self, path, query_string=""): def __init__(self, path, query_string=""):
import cherrypy import cherrypy
self.request = cherrypy.serving.request self.request = cherrypy.serving.request
self.query_string = query_string self.query_string = query_string
if "?" in path: if "?" in path:
# Separate any params included in the path # Separate any params included in the path
path, self.query_string = path.split("?", 1) path, self.query_string = path.split("?", 1)
# Note that urljoin will "do the right thing" whether url is: # Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy") # 1. a URL relative to root (e.g. "/dummy")
# 2. a URL relative to the current path # 2. a URL relative to the current path
# Note that any query string will be discarded. # Note that any query string will be discarded.
path = _urljoin(self.request.path_info, path) path = _urljoin(self.request.path_info, path)
# Set a 'path' member attribute so that code which traps this # Set a 'path' member attribute so that code which traps this
# error can have access to it. # error can have access to it.
self.path = path self.path = path
CherryPyException.__init__(self, path, self.query_string) CherryPyException.__init__(self, path, self.query_string)
class HTTPRedirect(CherryPyException): class HTTPRedirect(CherryPyException):
"""Exception raised when the request should be redirected. """Exception raised when the request should be redirected.
This exception will force a HTTP redirect to the URL or URL's you give it. This exception will force a HTTP redirect to the URL or URL's you give it.
The new URL must be passed as the first argument to the Exception, The new URL must be passed as the first argument to the Exception,
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
@@ -162,40 +162,40 @@ class HTTPRedirect(CherryPyException):
If one of the provided URL is a unicode object, it will be encoded If one of the provided URL is a unicode object, it will be encoded
using the default encoding or the one passed in parameter. using the default encoding or the one passed in parameter.
There are multiple types of redirect, from which you can select via the There are multiple types of redirect, from which you can select via the
``status`` argument. If you do not provide a ``status`` arg, it defaults to ``status`` argument. If you do not provide a ``status`` arg, it defaults to
303 (or 302 if responding with HTTP/1.0). 303 (or 302 if responding with HTTP/1.0).
Examples:: Examples::
raise cherrypy.HTTPRedirect("") raise cherrypy.HTTPRedirect("")
raise cherrypy.HTTPRedirect("/abs/path", 307) raise cherrypy.HTTPRedirect("/abs/path", 307)
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
See :ref:`redirectingpost` for additional caveats. See :ref:`redirectingpost` for additional caveats.
""" """
status = None status = None
"""The integer HTTP status code to emit.""" """The integer HTTP status code to emit."""
urls = None urls = None
"""The list of URL's to emit.""" """The list of URL's to emit."""
encoding = 'utf-8' encoding = 'utf-8'
"""The encoding when passed urls are not native strings""" """The encoding when passed urls are not native strings"""
def __init__(self, urls, status=None, encoding=None): def __init__(self, urls, status=None, encoding=None):
import cherrypy import cherrypy
request = cherrypy.serving.request request = cherrypy.serving.request
if isinstance(urls, basestring): if isinstance(urls, basestring):
urls = [urls] urls = [urls]
abs_urls = [] abs_urls = []
for url in urls: for url in urls:
url = tonative(url, encoding or self.encoding) url = tonative(url, encoding or self.encoding)
# Note that urljoin will "do the right thing" whether url is: # Note that urljoin will "do the right thing" whether url is:
# 1. a complete URL with host (e.g. "http://www.example.com/test") # 1. a complete URL with host (e.g. "http://www.example.com/test")
# 2. a URL relative to root (e.g. "/dummy") # 2. a URL relative to root (e.g. "/dummy")
@@ -204,7 +204,7 @@ class HTTPRedirect(CherryPyException):
url = _urljoin(cherrypy.url(), url) url = _urljoin(cherrypy.url(), url)
abs_urls.append(url) abs_urls.append(url)
self.urls = abs_urls self.urls = abs_urls
# RFC 2616 indicates a 301 response code fits our goal; however, # RFC 2616 indicates a 301 response code fits our goal; however,
# browser support for 301 is quite messy. Do 302/303 instead. See # browser support for 301 is quite messy. Do 302/303 instead. See
# http://www.alanflavell.org.uk/www/post-redirect.html # http://www.alanflavell.org.uk/www/post-redirect.html
@@ -217,26 +217,26 @@ class HTTPRedirect(CherryPyException):
status = int(status) status = int(status)
if status < 300 or status > 399: if status < 300 or status > 399:
raise ValueError("status must be between 300 and 399.") raise ValueError("status must be between 300 and 399.")
self.status = status self.status = status
CherryPyException.__init__(self, abs_urls, status) CherryPyException.__init__(self, abs_urls, status)
def set_response(self): def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self. """Modify cherrypy.response status, headers, and body to represent self.
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create an
HTTPRedirect object and set its output without *raising* the exception. HTTPRedirect object and set its output without *raising* the exception.
""" """
import cherrypy import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
response.status = status = self.status response.status = status = self.status
if status in (300, 301, 302, 303, 307): if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html;charset=utf-8" response.headers['Content-Type'] = "text/html;charset=utf-8"
# "The ... URI SHOULD be given by the Location field # "The ... URI SHOULD be given by the Location field
# in the response." # in the response."
response.headers['Location'] = self.urls[0] response.headers['Location'] = self.urls[0]
# "Unless the request method was HEAD, the entity of the response # "Unless the request method was HEAD, the entity of the response
# SHOULD contain a short hypertext note with a hyperlink to the # SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)." # new URI(s)."
@@ -256,7 +256,7 @@ class HTTPRedirect(CherryPyException):
# "The response MUST include the following header fields: # "The response MUST include the following header fields:
# Date, unless its omission is required by section 14.18.1" # Date, unless its omission is required by section 14.18.1"
# The "Date" header should have been set in Response.__init__ # The "Date" header should have been set in Response.__init__
# "...the response SHOULD NOT include other entity-headers." # "...the response SHOULD NOT include other entity-headers."
for key in ('Allow', 'Content-Encoding', 'Content-Language', for key in ('Allow', 'Content-Encoding', 'Content-Language',
'Content-Length', 'Content-Location', 'Content-MD5', 'Content-Length', 'Content-Location', 'Content-MD5',
@@ -264,7 +264,7 @@ class HTTPRedirect(CherryPyException):
'Last-Modified'): 'Last-Modified'):
if key in response.headers: if key in response.headers:
del response.headers[key] del response.headers[key]
# "The 304 response MUST NOT contain a message-body." # "The 304 response MUST NOT contain a message-body."
response.body = None response.body = None
# Previous code may have set C-L, so we have to reset it. # Previous code may have set C-L, so we have to reset it.
@@ -278,7 +278,7 @@ class HTTPRedirect(CherryPyException):
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
else: else:
raise ValueError("The %s status code is unknown." % status) raise ValueError("The %s status code is unknown." % status)
def __call__(self): def __call__(self):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
raise self raise self
@@ -287,9 +287,9 @@ class HTTPRedirect(CherryPyException):
def clean_headers(status): def clean_headers(status):
"""Remove any headers which should not apply to an error response.""" """Remove any headers which should not apply to an error response."""
import cherrypy import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
# Remove headers which applied to the original content, # Remove headers which applied to the original content,
# but do not apply to the error page. # but do not apply to the error page.
respheaders = response.headers respheaders = response.headers
@@ -298,7 +298,7 @@ def clean_headers(status):
"Content-Location", "Content-MD5", "Last-Modified"]: "Content-Location", "Content-MD5", "Last-Modified"]:
if key in respheaders: if key in respheaders:
del respheaders[key] del respheaders[key]
if status != 416: if status != 416:
# A server sending a response with status code 416 (Requested # A server sending a response with status code 416 (Requested
# range not satisfiable) SHOULD include a Content-Range field # range not satisfiable) SHOULD include a Content-Range field
@@ -312,7 +312,7 @@ def clean_headers(status):
class HTTPError(CherryPyException): class HTTPError(CherryPyException):
"""Exception used to return an HTTP error code (4xx-5xx) to the client. """Exception used to return an HTTP error code (4xx-5xx) to the client.
This exception can be used to automatically send a response using a http status This exception can be used to automatically send a response using a http status
code, with an appropriate error page. It takes an optional code, with an appropriate error page. It takes an optional
``status`` argument (which must be between 400 and 599); it defaults to 500 ``status`` argument (which must be between 400 and 599); it defaults to 500
@@ -320,49 +320,49 @@ class HTTPError(CherryPyException):
which will be returned in the response body. See which will be returned in the response body. See
`RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_ `RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
for a complete list of available error codes and when to use them. for a complete list of available error codes and when to use them.
Examples:: Examples::
raise cherrypy.HTTPError(403) raise cherrypy.HTTPError(403)
raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.") raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
""" """
status = None status = None
"""The HTTP status code. May be of type int or str (with a Reason-Phrase).""" """The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
code = None code = None
"""The integer HTTP status code.""" """The integer HTTP status code."""
reason = None reason = None
"""The HTTP Reason-Phrase string.""" """The HTTP Reason-Phrase string."""
def __init__(self, status=500, message=None): def __init__(self, status=500, message=None):
self.status = status self.status = status
try: try:
self.code, self.reason, defaultmsg = _httputil.valid_status(status) self.code, self.reason, defaultmsg = _httputil.valid_status(status)
except ValueError: except ValueError:
raise self.__class__(500, _exc_info()[1].args[0]) raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599: if self.code < 400 or self.code > 599:
raise ValueError("status must be between 400 and 599.") raise ValueError("status must be between 400 and 599.")
# See http://www.python.org/dev/peps/pep-0352/ # See http://www.python.org/dev/peps/pep-0352/
# self.message = message # self.message = message
self._message = message or defaultmsg self._message = message or defaultmsg
CherryPyException.__init__(self, status, message) CherryPyException.__init__(self, status, message)
def set_response(self): def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self. """Modify cherrypy.response status, headers, and body to represent self.
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create an
HTTPError object and set its output without *raising* the exception. HTTPError object and set its output without *raising* the exception.
""" """
import cherrypy import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
clean_headers(self.code) clean_headers(self.code)
# In all cases, finalize will be called after this method, # In all cases, finalize will be called after this method,
# so don't bother cleaning up response values here. # so don't bother cleaning up response values here.
response.status = self.status response.status = self.status
@@ -371,16 +371,16 @@ class HTTPError(CherryPyException):
tb = format_exc() tb = format_exc()
response.headers['Content-Type'] = "text/html;charset=utf-8" response.headers['Content-Type'] = "text/html;charset=utf-8"
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
content = ntob(self.get_error_page(self.status, traceback=tb, content = ntob(self.get_error_page(self.status, traceback=tb,
message=self._message), 'utf-8') message=self._message), 'utf-8')
response.body = content response.body = content
_be_ie_unfriendly(self.code) _be_ie_unfriendly(self.code)
def get_error_page(self, *args, **kwargs): def get_error_page(self, *args, **kwargs):
return get_error_page(*args, **kwargs) return get_error_page(*args, **kwargs)
def __call__(self): def __call__(self):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
raise self raise self
@@ -388,11 +388,11 @@ class HTTPError(CherryPyException):
class NotFound(HTTPError): class NotFound(HTTPError):
"""Exception raised when a URL could not be mapped to any handler (404). """Exception raised when a URL could not be mapped to any handler (404).
This is equivalent to raising This is equivalent to raising
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`. :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
""" """
def __init__(self, path=None): def __init__(self, path=None):
if path is None: if path is None:
import cherrypy import cherrypy
@@ -433,17 +433,17 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona
def get_error_page(status, **kwargs): def get_error_page(status, **kwargs):
"""Return an HTML page, containing a pretty error response. """Return an HTML page, containing a pretty error response.
status should be an int or a str. status should be an int or a str.
kwargs will be interpolated into the page template. kwargs will be interpolated into the page template.
""" """
import cherrypy import cherrypy
try: try:
code, reason, message = _httputil.valid_status(status) code, reason, message = _httputil.valid_status(status)
except ValueError: except ValueError:
raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
# We can't use setdefault here, because some # We can't use setdefault here, because some
# callers send None for kwarg values. # callers send None for kwarg values.
if kwargs.get('status') is None: if kwargs.get('status') is None:
@@ -454,13 +454,13 @@ def get_error_page(status, **kwargs):
kwargs['traceback'] = '' kwargs['traceback'] = ''
if kwargs.get('version') is None: if kwargs.get('version') is None:
kwargs['version'] = cherrypy.__version__ kwargs['version'] = cherrypy.__version__
for k, v in iteritems(kwargs): for k, v in iteritems(kwargs):
if v is None: if v is None:
kwargs[k] = "" kwargs[k] = ""
else: else:
kwargs[k] = _escape(kwargs[k]) kwargs[k] = _escape(kwargs[k])
# Use a custom template or callable for the error page? # Use a custom template or callable for the error page?
pages = cherrypy.serving.request.error_page pages = cherrypy.serving.request.error_page
error_page = pages.get(code) or pages.get('default') error_page = pages.get(code) or pages.get('default')
@@ -478,7 +478,7 @@ def get_error_page(status, **kwargs):
m += "<br />" m += "<br />"
m += "In addition, the custom error page failed:\n<br />%s" % e m += "In addition, the custom error page failed:\n<br />%s" % e
kwargs['message'] = m kwargs['message'] = m
return _HTTPErrorTemplate % kwargs return _HTTPErrorTemplate % kwargs
@@ -492,7 +492,7 @@ _ie_friendly_error_sizes = {
def _be_ie_unfriendly(status): def _be_ie_unfriendly(status):
import cherrypy import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
# For some statuses, Internet Explorer 5+ shows "friendly error # For some statuses, Internet Explorer 5+ shows "friendly error
# messages" instead of our response.body if the body is smaller # messages" instead of our response.body if the body is smaller
# than a given size. Fix this by returning a body over that size # than a given size. Fix this by returning a body over that size
@@ -527,27 +527,27 @@ def format_exc(exc=None):
def bare_error(extrabody=None): def bare_error(extrabody=None):
"""Produce status, headers, body for a critical error. """Produce status, headers, body for a critical error.
Returns a triple without calling any other questionable functions, Returns a triple without calling any other questionable functions,
so it should be as error-free as possible. Call it from an HTTP server so it should be as error-free as possible. Call it from an HTTP server
if you get errors outside of the request. if you get errors outside of the request.
If extrabody is None, a friendly but rather unhelpful error message If extrabody is None, a friendly but rather unhelpful error message
is set in the body. If extrabody is a string, it will be appended is set in the body. If extrabody is a string, it will be appended
as-is to the body. as-is to the body.
""" """
# The whole point of this function is to be a last line-of-defense # The whole point of this function is to be a last line-of-defense
# in handling errors. That is, it must not raise any errors itself; # in handling errors. That is, it must not raise any errors itself;
# it cannot be allowed to fail. Therefore, don't add to it! # it cannot be allowed to fail. Therefore, don't add to it!
# In particular, don't call any other CP functions. # In particular, don't call any other CP functions.
body = ntob("Unrecoverable error in the server.") body = ntob("Unrecoverable error in the server.")
if extrabody is not None: if extrabody is not None:
if not isinstance(extrabody, bytestr): if not isinstance(extrabody, bytestr):
extrabody = extrabody.encode('utf-8') extrabody = extrabody.encode('utf-8')
body += ntob("\n") + extrabody body += ntob("\n") + extrabody
return (ntob("500 Internal Server Error"), return (ntob("500 Internal Server Error"),
[(ntob('Content-Type'), ntob('text/plain')), [(ntob('Content-Type'), ntob('text/plain')),
(ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))], (ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))],
+49 -49
View File
@@ -69,21 +69,21 @@ and uses a RotatingFileHandler instead:
#python #python
log = app.log log = app.log
# Remove the default FileHandlers if present. # Remove the default FileHandlers if present.
log.error_file = "" log.error_file = ""
log.access_file = "" log.access_file = ""
maxBytes = getattr(log, "rot_maxBytes", 10000000) maxBytes = getattr(log, "rot_maxBytes", 10000000)
backupCount = getattr(log, "rot_backupCount", 1000) backupCount = getattr(log, "rot_backupCount", 1000)
# Make a new RotatingFileHandler for the error log. # Make a new RotatingFileHandler for the error log.
fname = getattr(log, "rot_error_file", "error.log") fname = getattr(log, "rot_error_file", "error.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
h.setLevel(DEBUG) h.setLevel(DEBUG)
h.setFormatter(_cplogging.logfmt) h.setFormatter(_cplogging.logfmt)
log.error_log.addHandler(h) log.error_log.addHandler(h)
# Make a new RotatingFileHandler for the access log. # Make a new RotatingFileHandler for the access log.
fname = getattr(log, "rot_access_file", "access.log") fname = getattr(log, "rot_access_file", "access.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
@@ -127,38 +127,38 @@ class NullHandler(logging.Handler):
class LogManager(object): class LogManager(object):
"""An object to assist both simple and advanced logging. """An object to assist both simple and advanced logging.
``cherrypy.log`` is an instance of this class. ``cherrypy.log`` is an instance of this class.
""" """
appid = None appid = None
"""The id() of the Application object which owns this log manager. If this """The id() of the Application object which owns this log manager. If this
is a global log manager, appid is None.""" is a global log manager, appid is None."""
error_log = None error_log = None
"""The actual :class:`logging.Logger` instance for error messages.""" """The actual :class:`logging.Logger` instance for error messages."""
access_log = None access_log = None
"""The actual :class:`logging.Logger` instance for access messages.""" """The actual :class:`logging.Logger` instance for access messages."""
if py3k: if py3k:
access_log_format = \ access_log_format = \
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
else: else:
access_log_format = \ access_log_format = \
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
logger_root = None logger_root = None
"""The "top-level" logger name. """The "top-level" logger name.
This string will be used as the first segment in the Logger names. This string will be used as the first segment in the Logger names.
The default is "cherrypy", for example, in which case the Logger names The default is "cherrypy", for example, in which case the Logger names
will be of the form:: will be of the form::
cherrypy.error.<appid> cherrypy.error.<appid>
cherrypy.access.<appid> cherrypy.access.<appid>
""" """
def __init__(self, appid=None, logger_root="cherrypy"): def __init__(self, appid=None, logger_root="cherrypy"):
self.logger_root = logger_root self.logger_root = logger_root
self.appid = appid self.appid = appid
@@ -186,34 +186,34 @@ class LogManager(object):
h.stream.close() h.stream.close()
h.stream = open(h.baseFilename, h.mode) h.stream = open(h.baseFilename, h.mode)
h.release() h.release()
def error(self, msg='', context='', severity=logging.INFO, traceback=False): def error(self, msg='', context='', severity=logging.INFO, traceback=False):
"""Write the given ``msg`` to the error log. """Write the given ``msg`` to the error log.
This is not just for errors! Applications may call this at any time This is not just for errors! Applications may call this at any time
to log application-specific information. to log application-specific information.
If ``traceback`` is True, the traceback of the current exception If ``traceback`` is True, the traceback of the current exception
(if any) will be appended to ``msg``. (if any) will be appended to ``msg``.
""" """
if traceback: if traceback:
msg += _cperror.format_exc() msg += _cperror.format_exc()
self.error_log.log(severity, ' '.join((self.time(), context, msg))) self.error_log.log(severity, ' '.join((self.time(), context, msg)))
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""An alias for ``error``.""" """An alias for ``error``."""
return self.error(*args, **kwargs) return self.error(*args, **kwargs)
def access(self): def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format). """Write to the access log (in Apache/NCSA Combined Log format).
See http://httpd.apache.org/docs/2.0/logs.html#combined for format See http://httpd.apache.org/docs/2.0/logs.html#combined for format
details. details.
CherryPy calls this automatically for you. Note there are no arguments; CherryPy calls this automatically for you. Note there are no arguments;
it collects the data itself from it collects the data itself from
:class:`cherrypy.request<cherrypy._cprequest.Request>`. :class:`cherrypy.request<cherrypy._cprequest.Request>`.
Like Apache started doing in 2.0.46, non-printable and other special Like Apache started doing in 2.0.46, non-printable and other special
characters in %r (and we expand that to all parts) are escaped using characters in %r (and we expand that to all parts) are escaped using
\\xhh sequences, where hh stands for the hexadecimal representation \\xhh sequences, where hh stands for the hexadecimal representation
@@ -232,7 +232,7 @@ class LogManager(object):
status = response.output_status.split(ntob(" "), 1)[0] status = response.output_status.split(ntob(" "), 1)[0]
if py3k: if py3k:
status = status.decode('ISO-8859-1') status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip, atoms = {'h': remote.name or remote.ip,
'l': '-', 'l': '-',
'u': getattr(request, "login", None) or "-", 'u': getattr(request, "login", None) or "-",
@@ -251,15 +251,15 @@ class LogManager(object):
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
# and backslash for us. All we have to do is strip the quotes. # and backslash for us. All we have to do is strip the quotes.
v = repr(v)[2:-1] v = repr(v)[2:-1]
# in python 3.0 the repr of bytes (as returned by encode) # in python 3.0 the repr of bytes (as returned by encode)
# uses double \'s. But then the logger escapes them yet, again # uses double \'s. But then the logger escapes them yet, again
# resulting in quadruple slashes. Remove the extra one here. # resulting in quadruple slashes. Remove the extra one here.
v = v.replace('\\\\', '\\') v = v.replace('\\\\', '\\')
# Escape double-quote. # Escape double-quote.
atoms[k] = v atoms[k] = v
try: try:
self.access_log.log(logging.INFO, self.access_log_format.format(**atoms)) self.access_log.log(logging.INFO, self.access_log_format.format(**atoms))
except: except:
@@ -275,12 +275,12 @@ class LogManager(object):
v = repr(v)[1:-1] v = repr(v)[1:-1]
# Escape double-quote. # Escape double-quote.
atoms[k] = v.replace('"', '\\"') atoms[k] = v.replace('"', '\\"')
try: try:
self.access_log.log(logging.INFO, self.access_log_format % atoms) self.access_log.log(logging.INFO, self.access_log_format % atoms)
except: except:
self(traceback=True) self(traceback=True)
def time(self): def time(self):
"""Return now() in Apache Common Log Format (no timezone).""" """Return now() in Apache Common Log Format (no timezone)."""
now = datetime.datetime.now() now = datetime.datetime.now()
@@ -289,15 +289,15 @@ class LogManager(object):
month = monthnames[now.month - 1].capitalize() month = monthnames[now.month - 1].capitalize()
return ('[%02d/%s/%04d:%02d:%02d:%02d]' % return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
(now.day, month, now.year, now.hour, now.minute, now.second)) (now.day, month, now.year, now.hour, now.minute, now.second))
def _get_builtin_handler(self, log, key): def _get_builtin_handler(self, log, key):
for h in log.handlers: for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key: if getattr(h, "_cpbuiltin", None) == key:
return h return h
# ------------------------- Screen handlers ------------------------- # # ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None): def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, "screen") h = self._get_builtin_handler(log, "screen")
if enable: if enable:
@@ -310,30 +310,30 @@ class LogManager(object):
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_screen(self): def _get_screen(self):
h = self._get_builtin_handler h = self._get_builtin_handler
has_h = h(self.error_log, "screen") or h(self.access_log, "screen") has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
return bool(has_h) return bool(has_h)
def _set_screen(self, newvalue): def _set_screen(self, newvalue):
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
screen = property(_get_screen, _set_screen, screen = property(_get_screen, _set_screen,
doc="""Turn stderr/stdout logging on or off. doc="""Turn stderr/stdout logging on or off.
If you set this to True, it'll add the appropriate StreamHandler for If you set this to True, it'll add the appropriate StreamHandler for
you. If you set it to False, it will remove the handler. you. If you set it to False, it will remove the handler.
""") """)
# -------------------------- File handlers -------------------------- # # -------------------------- File handlers -------------------------- #
def _add_builtin_file_handler(self, log, fname): def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname) h = logging.FileHandler(fname)
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "file" h._cpbuiltin = "file"
log.addHandler(h) log.addHandler(h)
def _set_file_handler(self, log, filename): def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file") h = self._get_builtin_handler(log, "file")
if filename: if filename:
@@ -348,7 +348,7 @@ class LogManager(object):
if h: if h:
h.close() h.close()
log.handlers.remove(h) log.handlers.remove(h)
def _get_error_file(self): def _get_error_file(self):
h = self._get_builtin_handler(self.error_log, "file") h = self._get_builtin_handler(self.error_log, "file")
if h: if h:
@@ -358,11 +358,11 @@ class LogManager(object):
self._set_file_handler(self.error_log, newvalue) self._set_file_handler(self.error_log, newvalue)
error_file = property(_get_error_file, _set_error_file, error_file = property(_get_error_file, _set_error_file,
doc="""The filename for self.error_log. doc="""The filename for self.error_log.
If you set this to a string, it'll add the appropriate FileHandler for If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler. you. If you set it to ``None`` or ``''``, it will remove the handler.
""") """)
def _get_access_file(self): def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, "file") h = self._get_builtin_handler(self.access_log, "file")
if h: if h:
@@ -372,13 +372,13 @@ class LogManager(object):
self._set_file_handler(self.access_log, newvalue) self._set_file_handler(self.access_log, newvalue)
access_file = property(_get_access_file, _set_access_file, access_file = property(_get_access_file, _set_access_file,
doc="""The filename for self.access_log. doc="""The filename for self.access_log.
If you set this to a string, it'll add the appropriate FileHandler for If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler. you. If you set it to ``None`` or ``''``, it will remove the handler.
""") """)
# ------------------------- WSGI handlers ------------------------- # # ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable): def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi") h = self._get_builtin_handler(log, "wsgi")
if enable: if enable:
@@ -389,15 +389,15 @@ class LogManager(object):
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_wsgi(self): def _get_wsgi(self):
return bool(self._get_builtin_handler(self.error_log, "wsgi")) return bool(self._get_builtin_handler(self.error_log, "wsgi"))
def _set_wsgi(self, newvalue): def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue) self._set_wsgi_handler(self.error_log, newvalue)
wsgi = property(_get_wsgi, _set_wsgi, wsgi = property(_get_wsgi, _set_wsgi,
doc="""Write errors to wsgi.errors. doc="""Write errors to wsgi.errors.
If you set this to True, it'll add the appropriate If you set this to True, it'll add the appropriate
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
(which writes errors to ``wsgi.errors``). (which writes errors to ``wsgi.errors``).
@@ -407,7 +407,7 @@ class LogManager(object):
class WSGIErrorHandler(logging.Handler): class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']." "A handler class which writes logging records to environ['wsgi.errors']."
def flush(self): def flush(self):
"""Flushes the stream.""" """Flushes the stream."""
try: try:
@@ -416,7 +416,7 @@ class WSGIErrorHandler(logging.Handler):
pass pass
else: else:
stream.flush() stream.flush()
def emit(self, record): def emit(self, record):
"""Emit a record.""" """Emit a record."""
try: try:
+28 -28
View File
@@ -35,12 +35,12 @@ Listen 8080
LoadModule python_module /usr/lib/apache2/modules/mod_python.so LoadModule python_module /usr/lib/apache2/modules/mod_python.so
<Location "/"> <Location "/">
PythonPath "sys.path+['/path/to/my/application']" PythonPath "sys.path+['/path/to/my/application']"
SetHandler python-program SetHandler python-program
PythonHandler cherrypy._cpmodpy::handler PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup myapp::setup_server PythonOption cherrypy.setup myapp::setup_server
PythonDebug On PythonDebug On
</Location> </Location>
# End # End
The actual path to your mod_python.so is dependent on your The actual path to your mod_python.so is dependent on your
@@ -70,7 +70,7 @@ from cherrypy.lib import httputil
def setup(req): def setup(req):
from mod_python import apache from mod_python import apache
# Run any setup functions defined by a "PythonOption cherrypy.setup" directive. # Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
options = req.get_options() options = req.get_options()
if 'cherrypy.setup' in options: if 'cherrypy.setup' in options:
@@ -83,12 +83,12 @@ def setup(req):
mod = __import__(modname, globals(), locals(), [fname]) mod = __import__(modname, globals(), locals(), [fname])
func = getattr(mod, fname) func = getattr(mod, fname)
func() func()
cherrypy.config.update({'log.screen': False, cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True, "tools.ignore_headers.on": True,
"tools.ignore_headers.headers": ['Range'], "tools.ignore_headers.headers": ['Range'],
}) })
engine = cherrypy.engine engine = cherrypy.engine
if hasattr(engine, "signal_handler"): if hasattr(engine, "signal_handler"):
engine.signal_handler.unsubscribe() engine.signal_handler.unsubscribe()
@@ -96,7 +96,7 @@ def setup(req):
engine.console_control_handler.unsubscribe() engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe() engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe() cherrypy.server.unsubscribe()
def _log(msg, level): def _log(msg, level):
newlevel = apache.APLOG_ERR newlevel = apache.APLOG_ERR
if logging.DEBUG >= level: if logging.DEBUG >= level:
@@ -110,9 +110,9 @@ def setup(req):
# Also, "When server is not specified...LogLevel does not apply..." # Also, "When server is not specified...LogLevel does not apply..."
apache.log_error(msg, newlevel, req.server) apache.log_error(msg, newlevel, req.server)
engine.subscribe('log', _log) engine.subscribe('log', _log)
engine.start() engine.start()
def cherrypy_cleanup(data): def cherrypy_cleanup(data):
engine.exit() engine.exit()
try: try:
@@ -139,16 +139,16 @@ def handler(req):
if not _isSetUp: if not _isSetUp:
setup(req) setup(req)
_isSetUp = True _isSetUp = True
# Obtain a Request object from CherryPy # Obtain a Request object from CherryPy
local = req.connection.local_addr local = req.connection.local_addr
local = httputil.Host(local[0], local[1], req.connection.local_host or "") local = httputil.Host(local[0], local[1], req.connection.local_host or "")
remote = req.connection.remote_addr remote = req.connection.remote_addr
remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "") remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
scheme = req.parsed_uri[0] or 'http' scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw() req.get_basic_auth_pw()
try: try:
# apache.mpm_query only became available in mod_python 3.1 # apache.mpm_query only became available in mod_python 3.1
q = apache.mpm_query q = apache.mpm_query
@@ -158,7 +158,7 @@ def handler(req):
bad_value = ("You must provide a PythonOption '%s', " bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version " "either 'on' or 'off', when running a version "
"of mod_python < 3.1") "of mod_python < 3.1")
threaded = options.get('multithread', '').lower() threaded = options.get('multithread', '').lower()
if threaded == 'on': if threaded == 'on':
threaded = True threaded = True
@@ -166,7 +166,7 @@ def handler(req):
threaded = False threaded = False
else: else:
raise ValueError(bad_value % "multithread") raise ValueError(bad_value % "multithread")
forked = options.get('multiprocess', '').lower() forked = options.get('multiprocess', '').lower()
if forked == 'on': if forked == 'on':
forked = True forked = True
@@ -174,7 +174,7 @@ def handler(req):
forked = False forked = False
else: else:
raise ValueError(bad_value % "multiprocess") raise ValueError(bad_value % "multiprocess")
sn = cherrypy.tree.script_name(req.uri or "/") sn = cherrypy.tree.script_name(req.uri or "/")
if sn is None: if sn is None:
send_response(req, '404 Not Found', [], '') send_response(req, '404 Not Found', [], '')
@@ -187,7 +187,7 @@ def handler(req):
headers = copyitems(req.headers_in) headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req) rfile = _ReadOnlyRequest(req)
prev = None prev = None
try: try:
redirections = [] redirections = []
while True: while True:
@@ -198,7 +198,7 @@ def handler(req):
request.multiprocess = bool(forked) request.multiprocess = bool(forked)
request.app = app request.app = app
request.prev = prev request.prev = prev
# Run the CherryPy Request object and obtain the response # Run the CherryPy Request object and obtain the response
try: try:
request.run(method, path, qs, reqproto, headers, rfile) request.run(method, path, qs, reqproto, headers, rfile)
@@ -207,7 +207,7 @@ def handler(req):
ir = sys.exc_info()[1] ir = sys.exc_info()[1]
app.release_serving() app.release_serving()
prev = request prev = request
if not recursive: if not recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the " raise RuntimeError("InternalRedirector visited the "
@@ -217,13 +217,13 @@ def handler(req):
if qs: if qs:
qs = "?" + qs qs = "?" + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = "GET"
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = BytesIO() rfile = BytesIO()
send_response(req, response.output_status, response.header_list, send_response(req, response.output_status, response.header_list,
response.body, response.stream) response.body, response.stream)
finally: finally:
@@ -239,7 +239,7 @@ def handler(req):
def send_response(req, status, headers, body, stream=False): def send_response(req, status, headers, body, stream=False):
# Set response status # Set response status
req.status = int(status[:3]) req.status = int(status[:3])
# Set response headers # Set response headers
req.content_type = "text/plain" req.content_type = "text/plain"
for header, value in headers: for header, value in headers:
@@ -247,11 +247,11 @@ def send_response(req, status, headers, body, stream=False):
req.content_type = value req.content_type = value
continue continue
req.headers_out.add(header, value) req.headers_out.add(header, value)
if stream: if stream:
# Flush now so the status and headers are sent immediately. # Flush now so the status and headers are sent immediately.
req.flush() req.flush()
# Set response body # Set response body
if isinstance(body, basestring): if isinstance(body, basestring):
req.write(body) req.write(body)
@@ -294,7 +294,7 @@ def read_process(cmd, args=""):
class ModPythonServer(object): class ModPythonServer(object):
template = """ template = """
# Apache2 server configuration file for running CherryPy with mod_python. # Apache2 server configuration file for running CherryPy with mod_python.
@@ -309,7 +309,7 @@ LoadModule python_module modules/mod_python.so
%(opts)s %(opts)s
</Location> </Location>
""" """
def __init__(self, loc="/", port=80, opts=None, apache_path="apache", def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
handler="cherrypy._cpmodpy::handler"): handler="cherrypy._cpmodpy::handler"):
self.loc = loc self.loc = loc
@@ -317,7 +317,7 @@ LoadModule python_module modules/mod_python.so
self.opts = opts self.opts = opts
self.apache_path = apache_path self.apache_path = apache_path
self.handler = handler self.handler = handler
def start(self): def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v) opts = "".join([" PythonOption %s %s\n" % (k, v)
for k, v in self.opts]) for k, v in self.opts])
@@ -326,18 +326,18 @@ LoadModule python_module modules/mod_python.so
"opts": opts, "opts": opts,
"handler": self.handler, "handler": self.handler,
} }
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf") mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
f = open(mpconf, 'wb') f = open(mpconf, 'wb')
try: try:
f.write(conf_data) f.write(conf_data)
finally: finally:
f.close() f.close()
response = read_process(self.apache_path, "-k start -f %s" % mpconf) response = read_process(self.apache_path, "-k start -f %s" % mpconf)
self.ready = True self.ready = True
return response return response
def stop(self): def stop(self):
os.popen("apache -k stop") os.popen("apache -k stop")
self.ready = False self.ready = False
+18 -18
View File
@@ -11,9 +11,9 @@ from cherrypy import wsgiserver
class NativeGateway(wsgiserver.Gateway): class NativeGateway(wsgiserver.Gateway):
recursive = False recursive = False
def respond(self): def respond(self):
req = self.req req = self.req
try: try:
@@ -22,7 +22,7 @@ class NativeGateway(wsgiserver.Gateway):
local = httputil.Host(local[0], local[1], "") local = httputil.Host(local[0], local[1], "")
remote = req.conn.remote_addr, req.conn.remote_port remote = req.conn.remote_addr, req.conn.remote_port
remote = httputil.Host(remote[0], remote[1], "") remote = httputil.Host(remote[0], remote[1], "")
scheme = req.scheme scheme = req.scheme
sn = cherrypy.tree.script_name(req.uri or "/") sn = cherrypy.tree.script_name(req.uri or "/")
if sn is None: if sn is None:
@@ -35,7 +35,7 @@ class NativeGateway(wsgiserver.Gateway):
headers = req.inheaders.items() headers = req.inheaders.items()
rfile = req.rfile rfile = req.rfile
prev = None prev = None
try: try:
redirections = [] redirections = []
while True: while True:
@@ -45,7 +45,7 @@ class NativeGateway(wsgiserver.Gateway):
request.multiprocess = False request.multiprocess = False
request.app = app request.app = app
request.prev = prev request.prev = prev
# Run the CherryPy Request object and obtain the response # Run the CherryPy Request object and obtain the response
try: try:
request.run(method, path, qs, req.request_protocol, headers, rfile) request.run(method, path, qs, req.request_protocol, headers, rfile)
@@ -54,7 +54,7 @@ class NativeGateway(wsgiserver.Gateway):
ir = sys.exc_info()[1] ir = sys.exc_info()[1]
app.release_serving() app.release_serving()
prev = request prev = request
if not self.recursive: if not self.recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the " raise RuntimeError("InternalRedirector visited the "
@@ -64,13 +64,13 @@ class NativeGateway(wsgiserver.Gateway):
if qs: if qs:
qs = "?" + qs qs = "?" + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = "GET"
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = BytesIO() rfile = BytesIO()
self.send_response( self.send_response(
response.output_status, response.header_list, response.output_status, response.header_list,
response.body) response.body)
@@ -82,20 +82,20 @@ class NativeGateway(wsgiserver.Gateway):
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
s, h, b = bare_error() s, h, b = bare_error()
self.send_response(s, h, b) self.send_response(s, h, b)
def send_response(self, status, headers, body): def send_response(self, status, headers, body):
req = self.req req = self.req
# Set response status # Set response status
req.status = str(status or "500 Server Error") req.status = str(status or "500 Server Error")
# Set response headers # Set response headers
for header, value in headers: for header, value in headers:
req.outheaders.append((header, value)) req.outheaders.append((header, value))
if (req.ready and not req.sent_headers): if (req.ready and not req.sent_headers):
req.sent_headers = True req.sent_headers = True
req.send_headers() req.send_headers()
# Set response body # Set response body
for seg in body: for seg in body:
req.write(seg) req.write(seg)
@@ -103,26 +103,26 @@ class NativeGateway(wsgiserver.Gateway):
class CPHTTPServer(wsgiserver.HTTPServer): class CPHTTPServer(wsgiserver.HTTPServer):
"""Wrapper for wsgiserver.HTTPServer. """Wrapper for wsgiserver.HTTPServer.
wsgiserver has been designed to not reference CherryPy in any way, wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. so that it can be used in other frameworks and applications.
Therefore, we wrap it here, so we can apply some attributes Therefore, we wrap it here, so we can apply some attributes
from config -> cherrypy.server -> HTTPServer. from config -> cherrypy.server -> HTTPServer.
""" """
def __init__(self, server_adapter=cherrypy.server): def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter self.server_adapter = server_adapter
server_name = (self.server_adapter.socket_host or server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or self.server_adapter.socket_file or
None) None)
wsgiserver.HTTPServer.__init__( wsgiserver.HTTPServer.__init__(
self, server_adapter.bind_addr, NativeGateway, self, server_adapter.bind_addr, NativeGateway,
minthreads=server_adapter.thread_pool, minthreads=server_adapter.thread_pool,
maxthreads=server_adapter.thread_pool_max, maxthreads=server_adapter.thread_pool_max,
server_name=server_name) server_name=server_name)
self.max_request_header_size = self.server_adapter.max_request_header_size or 0 self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0 self.max_request_body_size = self.server_adapter.max_request_body_size or 0
self.request_queue_size = self.server_adapter.socket_queue_size self.request_queue_size = self.server_adapter.socket_queue_size
@@ -130,7 +130,7 @@ class CPHTTPServer(wsgiserver.HTTPServer):
self.shutdown_timeout = self.server_adapter.shutdown_timeout self.shutdown_timeout = self.server_adapter.shutdown_timeout
self.protocol = self.server_adapter.protocol_version self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay self.nodelay = self.server_adapter.nodelay
ssl_module = self.server_adapter.ssl_module or 'pyopenssl' ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context: if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+117 -117
View File
@@ -53,7 +53,7 @@ Custom Processors
You can add your own processors for any specific or major MIME type. Simply add You can add your own processors for any specific or major MIME type. Simply add
it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
hook/tool that runs at ``on_start_resource`` or ``before_request_body``. hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
Here's the built-in JSON tool for an example:: Here's the built-in JSON tool for an example::
def json_in(force=True, debug=False): def json_in(force=True, debug=False):
@@ -62,7 +62,7 @@ Here's the built-in JSON tool for an example::
\"""Read application/json data into request.json.\""" \"""Read application/json data into request.json.\"""
if not entity.headers.get("Content-Length", ""): if not entity.headers.get("Content-Length", ""):
raise cherrypy.HTTPError(411) raise cherrypy.HTTPError(411)
body = entity.fp.read() body = entity.fp.read()
try: try:
request.json = json_decode(body) request.json = json_decode(body)
@@ -141,14 +141,14 @@ def process_urlencoded(entity):
for pair in aparam.split(ntob(';')): for pair in aparam.split(ntob(';')):
if not pair: if not pair:
continue continue
atoms = pair.split(ntob('='), 1) atoms = pair.split(ntob('='), 1)
if len(atoms) == 1: if len(atoms) == 1:
atoms.append(ntob('')) atoms.append(ntob(''))
key = unquote_plus(atoms[0]).decode(charset) key = unquote_plus(atoms[0]).decode(charset)
value = unquote_plus(atoms[1]).decode(charset) value = unquote_plus(atoms[1]).decode(charset)
if key in params: if key in params:
if not isinstance(params[key], list): if not isinstance(params[key], list):
params[key] = [params[key]] params[key] = [params[key]]
@@ -164,7 +164,7 @@ def process_urlencoded(entity):
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following " 400, "The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(entity.attempt_charsets)) "charsets were attempted: %s" % repr(entity.attempt_charsets))
# Now that all values have been successfully parsed and decoded, # Now that all values have been successfully parsed and decoded,
# apply them to the entity.params dict. # apply them to the entity.params dict.
for key, value in params.items(): for key, value in params.items():
@@ -185,22 +185,22 @@ def process_multipart(entity):
# is often necessary to enclose the boundary parameter values in quotes # is often necessary to enclose the boundary parameter values in quotes
# on the Content-type line" # on the Content-type line"
ib = entity.content_type.params['boundary'].strip('"') ib = entity.content_type.params['boundary'].strip('"')
if not re.match("^[ -~]{0,200}[!-~]$", ib): if not re.match("^[ -~]{0,200}[!-~]$", ib):
raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
ib = ('--' + ib).encode('ascii') ib = ('--' + ib).encode('ascii')
# Find the first marker # Find the first marker
while True: while True:
b = entity.readline() b = entity.readline()
if not b: if not b:
return return
b = b.strip() b = b.strip()
if b == ib: if b == ib:
break break
# Read all parts # Read all parts
while True: while True:
part = entity.part_class.from_fp(entity.fp, ib) part = entity.part_class.from_fp(entity.fp, ib)
@@ -212,7 +212,7 @@ def process_multipart(entity):
def process_multipart_form_data(entity): def process_multipart_form_data(entity):
"""Read all multipart/form-data parts into entity.parts or entity.params.""" """Read all multipart/form-data parts into entity.parts or entity.params."""
process_multipart(entity) process_multipart(entity)
kept_parts = [] kept_parts = []
for part in entity.parts: for part in entity.parts:
if part.name is None: if part.name is None:
@@ -225,28 +225,28 @@ def process_multipart_form_data(entity):
# It's a file upload. Retain the whole part so consumer code # It's a file upload. Retain the whole part so consumer code
# has access to its .file and .filename attributes. # has access to its .file and .filename attributes.
value = part value = part
if part.name in entity.params: if part.name in entity.params:
if not isinstance(entity.params[part.name], list): if not isinstance(entity.params[part.name], list):
entity.params[part.name] = [entity.params[part.name]] entity.params[part.name] = [entity.params[part.name]]
entity.params[part.name].append(value) entity.params[part.name].append(value)
else: else:
entity.params[part.name] = value entity.params[part.name] = value
entity.parts = kept_parts entity.parts = kept_parts
def _old_process_multipart(entity): def _old_process_multipart(entity):
"""The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
process_multipart(entity) process_multipart(entity)
params = entity.params params = entity.params
for part in entity.parts: for part in entity.parts:
if part.name is None: if part.name is None:
key = ntou('parts') key = ntou('parts')
else: else:
key = part.name key = part.name
if part.filename is None: if part.filename is None:
# It's a regular field # It's a regular field
value = part.fullvalue() value = part.fullvalue()
@@ -254,7 +254,7 @@ def _old_process_multipart(entity):
# It's a file upload. Retain the whole part so consumer code # It's a file upload. Retain the whole part so consumer code
# has access to its .file and .filename attributes. # has access to its .file and .filename attributes.
value = part value = part
if key in params: if key in params:
if not isinstance(params[key], list): if not isinstance(params[key], list):
params[key] = [params[key]] params[key] = [params[key]]
@@ -269,12 +269,12 @@ def _old_process_multipart(entity):
class Entity(object): class Entity(object):
"""An HTTP request body, or MIME multipart body. """An HTTP request body, or MIME multipart body.
This class collects information about the HTTP request entity. When a This class collects information about the HTTP request entity. When a
given entity is of MIME type "multipart", each part is parsed into its own given entity is of MIME type "multipart", each part is parsed into its own
Entity instance, and the set of parts stored in Entity instance, and the set of parts stored in
:attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`. :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
Between the ``before_request_body`` and ``before_handler`` tools, CherryPy Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
tries to process the request body (if any) by calling tries to process the request body (if any) by calling
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`. :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
@@ -287,7 +287,7 @@ class Entity(object):
processor is still not found, then the processor is still not found, then the
:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the
Entity is called (which does nothing by default; you can override this too). Entity is called (which does nothing by default; you can override this too).
CherryPy includes processors for the "application/x-www-form-urlencoded" CherryPy includes processors for the "application/x-www-form-urlencoded"
type, the "multipart/form-data" type, and the "multipart" major type. type, the "multipart/form-data" type, and the "multipart" major type.
CherryPy 3.2 processes these types almost exactly as older versions. CherryPy 3.2 processes these types almost exactly as older versions.
@@ -298,43 +298,43 @@ class Entity(object):
case it will have ``file`` and ``filename`` attributes, or possibly a case it will have ``file`` and ``filename`` attributes, or possibly a
``value`` attribute). Each Part is itself a subclass of ``value`` attribute). Each Part is itself a subclass of
Entity, and has its own ``process`` method and ``processors`` dict. Entity, and has its own ``process`` method and ``processors`` dict.
There is a separate processor for the "multipart" major type which is more There is a separate processor for the "multipart" major type which is more
flexible, and simply stores all multipart parts in flexible, and simply stores all multipart parts in
:attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
enable it with:: enable it with::
cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
in an ``on_start_resource`` tool. in an ``on_start_resource`` tool.
""" """
# http://tools.ietf.org/html/rfc2046#section-4.1.2: # http://tools.ietf.org/html/rfc2046#section-4.1.2:
# "The default character set, which must be assumed in the # "The default character set, which must be assumed in the
# absence of a charset parameter, is US-ASCII." # absence of a charset parameter, is US-ASCII."
# However, many browsers send data in utf-8 with no charset. # However, many browsers send data in utf-8 with no charset.
attempt_charsets = ['utf-8'] attempt_charsets = ['utf-8']
"""A list of strings, each of which should be a known encoding. """A list of strings, each of which should be a known encoding.
When the Content-Type of the request body warrants it, each of the given When the Content-Type of the request body warrants it, each of the given
encodings will be tried in order. The first one to successfully decode the encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts. but ``['us-ascii', 'utf-8']`` for multipart parts.
""" """
charset = None charset = None
"""The successful decoding; see "attempt_charsets" above.""" """The successful decoding; see "attempt_charsets" above."""
content_type = None content_type = None
"""The value of the Content-Type request header. """The value of the Content-Type request header.
If the Entity is part of a multipart payload, this will be the Content-Type If the Entity is part of a multipart payload, this will be the Content-Type
given in the MIME headers for this part. given in the MIME headers for this part.
""" """
default_content_type = 'application/x-www-form-urlencoded' default_content_type = 'application/x-www-form-urlencoded'
"""This defines a default ``Content-Type`` to use if no Content-Type header """This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the is given. The empty string is used for RequestBody, which results in the
@@ -344,26 +344,26 @@ class Entity(object):
declares that a part with no Content-Type defaults to "text/plain" declares that a part with no Content-Type defaults to "text/plain"
(see :class:`Part<cherrypy._cpreqbody.Part>`). (see :class:`Part<cherrypy._cpreqbody.Part>`).
""" """
filename = None filename = None
"""The ``Content-Disposition.filename`` header, if available.""" """The ``Content-Disposition.filename`` header, if available."""
fp = None fp = None
"""The readable socket file object.""" """The readable socket file object."""
headers = None headers = None
"""A dict of request/multipart header names and values. """A dict of request/multipart header names and values.
This is a copy of the ``request.headers`` for the ``request.body``; This is a copy of the ``request.headers`` for the ``request.body``;
for multipart parts, it is the set of headers for that part. for multipart parts, it is the set of headers for that part.
""" """
length = None length = None
"""The value of the ``Content-Length`` header, if provided.""" """The value of the ``Content-Length`` header, if provided."""
name = None name = None
"""The "name" parameter of the ``Content-Disposition`` header, if any.""" """The "name" parameter of the ``Content-Disposition`` header, if any."""
params = None params = None
""" """
If the request Content-Type is 'application/x-www-form-urlencoded' or If the request Content-Type is 'application/x-www-form-urlencoded' or
@@ -373,39 +373,39 @@ class Entity(object):
can be sent with various HTTP method verbs). This value is set between can be sent with various HTTP method verbs). This value is set between
the 'before_request_body' and 'before_handler' hooks (assuming that the 'before_request_body' and 'before_handler' hooks (assuming that
process_request_body is True).""" process_request_body is True)."""
processors = {'application/x-www-form-urlencoded': process_urlencoded, processors = {'application/x-www-form-urlencoded': process_urlencoded,
'multipart/form-data': process_multipart_form_data, 'multipart/form-data': process_multipart_form_data,
'multipart': process_multipart, 'multipart': process_multipart,
} }
"""A dict of Content-Type names to processor methods.""" """A dict of Content-Type names to processor methods."""
parts = None parts = None
"""A list of Part instances if ``Content-Type`` is of major type "multipart".""" """A list of Part instances if ``Content-Type`` is of major type "multipart"."""
part_class = None part_class = None
"""The class used for multipart parts. """The class used for multipart parts.
You can replace this with custom subclasses to alter the processing of You can replace this with custom subclasses to alter the processing of
multipart parts. multipart parts.
""" """
def __init__(self, fp, headers, params=None, parts=None): def __init__(self, fp, headers, params=None, parts=None):
# Make an instance-specific copy of the class processors # Make an instance-specific copy of the class processors
# so Tools, etc. can replace them per-request. # so Tools, etc. can replace them per-request.
self.processors = self.processors.copy() self.processors = self.processors.copy()
self.fp = fp self.fp = fp
self.headers = headers self.headers = headers
if params is None: if params is None:
params = {} params = {}
self.params = params self.params = params
if parts is None: if parts is None:
parts = [] parts = []
self.parts = parts self.parts = parts
# Content-Type # Content-Type
self.content_type = headers.elements('Content-Type') self.content_type = headers.elements('Content-Type')
if self.content_type: if self.content_type:
@@ -413,7 +413,7 @@ class Entity(object):
else: else:
self.content_type = httputil.HeaderElement.from_str( self.content_type = httputil.HeaderElement.from_str(
self.default_content_type) self.default_content_type)
# Copy the class 'attempt_charsets', prepending any Content-Type charset # Copy the class 'attempt_charsets', prepending any Content-Type charset
dec = self.content_type.params.get("charset", None) dec = self.content_type.params.get("charset", None)
if dec: if dec:
@@ -421,7 +421,7 @@ class Entity(object):
if c != dec] if c != dec]
else: else:
self.attempt_charsets = self.attempt_charsets[:] self.attempt_charsets = self.attempt_charsets[:]
# Length # Length
self.length = None self.length = None
clen = headers.get('Content-Length', None) clen = headers.get('Content-Length', None)
@@ -431,7 +431,7 @@ class Entity(object):
self.length = int(clen) self.length = int(clen)
except ValueError: except ValueError:
pass pass
# Content-Disposition # Content-Disposition
self.name = None self.name = None
self.filename = None self.filename = None
@@ -446,23 +446,23 @@ class Entity(object):
self.filename = disp.params['filename'] self.filename = disp.params['filename']
if self.filename.startswith('"') and self.filename.endswith('"'): if self.filename.startswith('"') and self.filename.endswith('"'):
self.filename = self.filename[1:-1] self.filename = self.filename[1:-1]
# The 'type' attribute is deprecated in 3.2; remove it in 3.3. # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
type = property(lambda self: self.content_type, type = property(lambda self: self.content_type,
doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""") doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
def read(self, size=None, fp_out=None): def read(self, size=None, fp_out=None):
return self.fp.read(size, fp_out) return self.fp.read(size, fp_out)
def readline(self, size=None): def readline(self, size=None):
return self.fp.readline(size) return self.fp.readline(size)
def readlines(self, sizehint=None): def readlines(self, sizehint=None):
return self.fp.readlines(sizehint) return self.fp.readlines(sizehint)
def __iter__(self): def __iter__(self):
return self return self
def __next__(self): def __next__(self):
line = self.readline() line = self.readline()
if not line: if not line:
@@ -471,21 +471,21 @@ class Entity(object):
def next(self): def next(self):
return self.__next__() return self.__next__()
def read_into_file(self, fp_out=None): def read_into_file(self, fp_out=None):
"""Read the request body into fp_out (or make_file() if None). Return fp_out.""" """Read the request body into fp_out (or make_file() if None). Return fp_out."""
if fp_out is None: if fp_out is None:
fp_out = self.make_file() fp_out = self.make_file()
self.read(fp_out=fp_out) self.read(fp_out=fp_out)
return fp_out return fp_out
def make_file(self): def make_file(self):
"""Return a file-like object into which the request body will be read. """Return a file-like object into which the request body will be read.
By default, this will return a TemporaryFile. Override as needed. By default, this will return a TemporaryFile. Override as needed.
See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
return tempfile.TemporaryFile() return tempfile.TemporaryFile()
def fullvalue(self): def fullvalue(self):
"""Return this entity as a string, whether stored in a file or not.""" """Return this entity as a string, whether stored in a file or not."""
if self.file: if self.file:
@@ -496,7 +496,7 @@ class Entity(object):
else: else:
value = self.value value = self.value
return value return value
def process(self): def process(self):
"""Execute the best-match processor for the given media type.""" """Execute the best-match processor for the given media type."""
proc = None proc = None
@@ -513,7 +513,7 @@ class Entity(object):
self.default_proc() self.default_proc()
else: else:
proc(self) proc(self)
def default_proc(self): def default_proc(self):
"""Called if a more-specific processor is not found for the ``Content-Type``.""" """Called if a more-specific processor is not found for the ``Content-Type``."""
# Leave the fp alone for someone else to read. This works fine # Leave the fp alone for someone else to read. This works fine
@@ -524,24 +524,24 @@ class Entity(object):
class Part(Entity): class Part(Entity):
"""A MIME part entity, part of a multipart entity.""" """A MIME part entity, part of a multipart entity."""
# "The default character set, which must be assumed in the absence of a # "The default character set, which must be assumed in the absence of a
# charset parameter, is US-ASCII." # charset parameter, is US-ASCII."
attempt_charsets = ['us-ascii', 'utf-8'] attempt_charsets = ['us-ascii', 'utf-8']
"""A list of strings, each of which should be a known encoding. """A list of strings, each of which should be a known encoding.
When the Content-Type of the request body warrants it, each of the given When the Content-Type of the request body warrants it, each of the given
encodings will be tried in order. The first one to successfully decode the encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts. but ``['us-ascii', 'utf-8']`` for multipart parts.
""" """
boundary = None boundary = None
"""The MIME multipart boundary.""" """The MIME multipart boundary."""
default_content_type = 'text/plain' default_content_type = 'text/plain'
"""This defines a default ``Content-Type`` to use if no Content-Type header """This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the is given. The empty string is used for RequestBody, which results in the
@@ -551,7 +551,7 @@ class Part(Entity):
the MIME spec declares that a part with no Content-Type defaults to the MIME spec declares that a part with no Content-Type defaults to
"text/plain". "text/plain".
""" """
# This is the default in stdlib cgi. We may want to increase it. # This is the default in stdlib cgi. We may want to increase it.
maxrambytes = 1000 maxrambytes = 1000
"""The threshold of bytes after which point the ``Part`` will store its data """The threshold of bytes after which point the ``Part`` will store its data
@@ -559,18 +559,18 @@ class Part(Entity):
instead of a string. Defaults to 1000, just like the :mod:`cgi` module in instead of a string. Defaults to 1000, just like the :mod:`cgi` module in
Python's standard library. Python's standard library.
""" """
def __init__(self, fp, headers, boundary): def __init__(self, fp, headers, boundary):
Entity.__init__(self, fp, headers) Entity.__init__(self, fp, headers)
self.boundary = boundary self.boundary = boundary
self.file = None self.file = None
self.value = None self.value = None
def from_fp(cls, fp, boundary): def from_fp(cls, fp, boundary):
headers = cls.read_headers(fp) headers = cls.read_headers(fp)
return cls(fp, headers, boundary) return cls(fp, headers, boundary)
from_fp = classmethod(from_fp) from_fp = classmethod(from_fp)
def read_headers(cls, fp): def read_headers(cls, fp):
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
while True: while True:
@@ -578,13 +578,13 @@ class Part(Entity):
if not line: if not line:
# No more data--illegal end of headers # No more data--illegal end of headers
raise EOFError("Illegal end of headers.") raise EOFError("Illegal end of headers.")
if line == ntob('\r\n'): if line == ntob('\r\n'):
# Normal end of headers # Normal end of headers
break break
if not line.endswith(ntob('\r\n')): if not line.endswith(ntob('\r\n')):
raise ValueError("MIME requires CRLF terminators: %r" % line) raise ValueError("MIME requires CRLF terminators: %r" % line)
if line[0] in ntob(' \t'): if line[0] in ntob(' \t'):
# It's a continuation line. # It's a continuation line.
v = line.strip().decode('ISO-8859-1') v = line.strip().decode('ISO-8859-1')
@@ -592,21 +592,21 @@ class Part(Entity):
k, v = line.split(ntob(":"), 1) k, v = line.split(ntob(":"), 1)
k = k.strip().decode('ISO-8859-1') k = k.strip().decode('ISO-8859-1')
v = v.strip().decode('ISO-8859-1') v = v.strip().decode('ISO-8859-1')
existing = headers.get(k) existing = headers.get(k)
if existing: if existing:
v = ", ".join((existing, v)) v = ", ".join((existing, v))
headers[k] = v headers[k] = v
return headers return headers
read_headers = classmethod(read_headers) read_headers = classmethod(read_headers)
def read_lines_to_boundary(self, fp_out=None): def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file. """Read bytes from self.fp and return or write them to a file.
If the 'fp_out' argument is None (the default), all bytes read are If the 'fp_out' argument is None (the default), all bytes read are
returned in a single byte string. returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like object that If the 'fp_out' argument is not None, it must be a file-like object that
supports the 'write' method; all bytes read will be written to the fp, supports the 'write' method; all bytes read will be written to the fp,
and that fp is returned. and that fp is returned.
@@ -627,9 +627,9 @@ class Part(Entity):
if strippedline == endmarker: if strippedline == endmarker:
self.fp.finish() self.fp.finish()
break break
line = delim + line line = delim + line
if line.endswith(ntob("\r\n")): if line.endswith(ntob("\r\n")):
delim = ntob("\r\n") delim = ntob("\r\n")
line = line[:-2] line = line[:-2]
@@ -641,7 +641,7 @@ class Part(Entity):
else: else:
delim = ntob("") delim = ntob("")
prev_lf = False prev_lf = False
if fp_out is None: if fp_out is None:
lines.append(line) lines.append(line)
seen += len(line) seen += len(line)
@@ -651,7 +651,7 @@ class Part(Entity):
fp_out.write(line) fp_out.write(line)
else: else:
fp_out.write(line) fp_out.write(line)
if fp_out is None: if fp_out is None:
result = ntob('').join(lines) result = ntob('').join(lines)
for charset in self.attempt_charsets: for charset in self.attempt_charsets:
@@ -669,7 +669,7 @@ class Part(Entity):
else: else:
fp_out.seek(0) fp_out.seek(0)
return fp_out return fp_out
def default_proc(self): def default_proc(self):
"""Called if a more-specific processor is not found for the ``Content-Type``.""" """Called if a more-specific processor is not found for the ``Content-Type``."""
if self.filename: if self.filename:
@@ -681,7 +681,7 @@ class Part(Entity):
self.value = result self.value = result
else: else:
self.file = result self.file = result
def read_into_file(self, fp_out=None): def read_into_file(self, fp_out=None):
"""Read the request body into fp_out (or make_file() if None). Return fp_out.""" """Read the request body into fp_out (or make_file() if None). Return fp_out."""
if fp_out is None: if fp_out is None:
@@ -711,7 +711,7 @@ comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
class SizedReader: class SizedReader:
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False): def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False):
# Wrap our fp in a buffer so peek() works # Wrap our fp in a buffer so peek() works
self.fp = fp self.fp = fp
@@ -722,25 +722,25 @@ class SizedReader:
self.bytes_read = 0 self.bytes_read = 0
self.done = False self.done = False
self.has_trailers = has_trailers self.has_trailers = has_trailers
def read(self, size=None, fp_out=None): def read(self, size=None, fp_out=None):
"""Read bytes from the request body and return or write them to a file. """Read bytes from the request body and return or write them to a file.
A number of bytes less than or equal to the 'size' argument are read A number of bytes less than or equal to the 'size' argument are read
off the socket. The actual number of bytes read are tracked in off the socket. The actual number of bytes read are tracked in
self.bytes_read. The number may be smaller than 'size' when 1) the self.bytes_read. The number may be smaller than 'size' when 1) the
client sends fewer bytes, 2) the 'Content-Length' request header client sends fewer bytes, 2) the 'Content-Length' request header
specifies fewer bytes than requested, or 3) the number of bytes read specifies fewer bytes than requested, or 3) the number of bytes read
exceeds self.maxbytes (in which case, 413 is raised). exceeds self.maxbytes (in which case, 413 is raised).
If the 'fp_out' argument is None (the default), all bytes read are If the 'fp_out' argument is None (the default), all bytes read are
returned in a single byte string. returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like object that If the 'fp_out' argument is not None, it must be a file-like object that
supports the 'write' method; all bytes read will be written to the fp, supports the 'write' method; all bytes read will be written to the fp,
and None is returned. and None is returned.
""" """
if self.length is None: if self.length is None:
if size is None: if size is None:
remaining = inf remaining = inf
@@ -756,9 +756,9 @@ class SizedReader:
return ntob('') return ntob('')
else: else:
return None return None
chunks = [] chunks = []
# Read bytes from the buffer. # Read bytes from the buffer.
if self.buffer: if self.buffer:
if remaining is inf: if remaining is inf:
@@ -769,18 +769,18 @@ class SizedReader:
self.buffer = self.buffer[remaining:] self.buffer = self.buffer[remaining:]
datalen = len(data) datalen = len(data)
remaining -= datalen remaining -= datalen
# Check lengths. # Check lengths.
self.bytes_read += datalen self.bytes_read += datalen
if self.maxbytes and self.bytes_read > self.maxbytes: if self.maxbytes and self.bytes_read > self.maxbytes:
raise cherrypy.HTTPError(413) raise cherrypy.HTTPError(413)
# Store the data. # Store the data.
if fp_out is None: if fp_out is None:
chunks.append(data) chunks.append(data)
else: else:
fp_out.write(data) fp_out.write(data)
# Read bytes from the socket. # Read bytes from the socket.
while remaining > 0: while remaining > 0:
chunksize = min(remaining, self.bufsize) chunksize = min(remaining, self.bufsize)
@@ -799,21 +799,21 @@ class SizedReader:
break break
datalen = len(data) datalen = len(data)
remaining -= datalen remaining -= datalen
# Check lengths. # Check lengths.
self.bytes_read += datalen self.bytes_read += datalen
if self.maxbytes and self.bytes_read > self.maxbytes: if self.maxbytes and self.bytes_read > self.maxbytes:
raise cherrypy.HTTPError(413) raise cherrypy.HTTPError(413)
# Store the data. # Store the data.
if fp_out is None: if fp_out is None:
chunks.append(data) chunks.append(data)
else: else:
fp_out.write(data) fp_out.write(data)
if fp_out is None: if fp_out is None:
return ntob('').join(chunks) return ntob('').join(chunks)
def readline(self, size=None): def readline(self, size=None):
"""Read a line from the request body and return it.""" """Read a line from the request body and return it."""
chunks = [] chunks = []
@@ -834,7 +834,7 @@ class SizedReader:
else: else:
chunks.append(data) chunks.append(data)
return ntob('').join(chunks) return ntob('').join(chunks)
def readlines(self, sizehint=None): def readlines(self, sizehint=None):
"""Read lines from the request body and return them.""" """Read lines from the request body and return them."""
if self.length is not None: if self.length is not None:
@@ -842,7 +842,7 @@ class SizedReader:
sizehint = self.length - self.bytes_read sizehint = self.length - self.bytes_read
else: else:
sizehint = min(sizehint, self.length - self.bytes_read) sizehint = min(sizehint, self.length - self.bytes_read)
lines = [] lines = []
seen = 0 seen = 0
while True: while True:
@@ -854,12 +854,12 @@ class SizedReader:
if seen >= sizehint: if seen >= sizehint:
break break
return lines return lines
def finish(self): def finish(self):
self.done = True self.done = True
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
self.trailers = {} self.trailers = {}
try: try:
for line in self.fp.read_trailer_lines(): for line in self.fp.read_trailer_lines():
if line[0] in ntob(' \t'): if line[0] in ntob(' \t'):
@@ -872,7 +872,7 @@ class SizedReader:
raise ValueError("Illegal header line.") raise ValueError("Illegal header line.")
k = k.strip().title() k = k.strip().title()
v = v.strip() v = v.strip()
if k in comma_separated_headers: if k in comma_separated_headers:
existing = self.trailers.get(envname) existing = self.trailers.get(envname)
if existing: if existing:
@@ -890,10 +890,10 @@ class SizedReader:
class RequestBody(Entity): class RequestBody(Entity):
"""The entity of the HTTP request.""" """The entity of the HTTP request."""
bufsize = 8 * 1024 bufsize = 8 * 1024
"""The buffer size used when reading the socket.""" """The buffer size used when reading the socket."""
# Don't parse the request body at all if the client didn't provide # Don't parse the request body at all if the client didn't provide
# a Content-Type header. See http://www.cherrypy.org/ticket/790 # a Content-Type header. See http://www.cherrypy.org/ticket/790
default_content_type = '' default_content_type = ''
@@ -905,13 +905,13 @@ class RequestBody(Entity):
declares that a part with no Content-Type defaults to "text/plain" declares that a part with no Content-Type defaults to "text/plain"
(see :class:`Part<cherrypy._cpreqbody.Part>`). (see :class:`Part<cherrypy._cpreqbody.Part>`).
""" """
maxbytes = None maxbytes = None
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket.""" """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
def __init__(self, fp, headers, params=None, request_params=None): def __init__(self, fp, headers, params=None, request_params=None):
Entity.__init__(self, fp, headers, params) Entity.__init__(self, fp, headers, params)
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
# When no explicit charset parameter is provided by the # When no explicit charset parameter is provided by the
# sender, media subtypes of the "text" type are defined # sender, media subtypes of the "text" type are defined
@@ -923,14 +923,14 @@ class RequestBody(Entity):
break break
else: else:
self.attempt_charsets.append('ISO-8859-1') self.attempt_charsets.append('ISO-8859-1')
# Temporary fix while deprecating passing .parts as .params. # Temporary fix while deprecating passing .parts as .params.
self.processors['multipart'] = _old_process_multipart self.processors['multipart'] = _old_process_multipart
if request_params is None: if request_params is None:
request_params = {} request_params = {}
self.request_params = request_params self.request_params = request_params
def process(self): def process(self):
"""Process the request entity based on its Content-Type.""" """Process the request entity based on its Content-Type."""
# "The presence of a message-body in a request is signaled by the # "The presence of a message-body in a request is signaled by the
@@ -942,12 +942,12 @@ class RequestBody(Entity):
h = cherrypy.serving.request.headers h = cherrypy.serving.request.headers
if 'Content-Length' not in h and 'Transfer-Encoding' not in h: if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
raise cherrypy.HTTPError(411) raise cherrypy.HTTPError(411)
self.fp = SizedReader(self.fp, self.length, self.fp = SizedReader(self.fp, self.length,
self.maxbytes, bufsize=self.bufsize, self.maxbytes, bufsize=self.bufsize,
has_trailers='Trailer' in h) has_trailers='Trailer' in h)
super(RequestBody, self).process() super(RequestBody, self).process()
# Body params should also be a part of the request_params # Body params should also be a part of the request_params
# add them in here. # add them in here.
request_params = self.request_params request_params = self.request_params
@@ -956,7 +956,7 @@ class RequestBody(Entity):
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
if isinstance(key, unicode): if isinstance(key, unicode):
key = key.encode('ISO-8859-1') key = key.encode('ISO-8859-1')
if key in request_params: if key in request_params:
if not isinstance(request_params[key], list): if not isinstance(request_params[key], list):
request_params[key] = [request_params[key]] request_params[key] = [request_params[key]]
+146 -146
View File
@@ -14,41 +14,41 @@ from cherrypy.lib import httputil, file_generator
class Hook(object): class Hook(object):
"""A callback and its metadata: failsafe, priority, and kwargs.""" """A callback and its metadata: failsafe, priority, and kwargs."""
callback = None callback = None
""" """
The bare callable that this Hook object is wrapping, which will The bare callable that this Hook object is wrapping, which will
be called when the Hook is called.""" be called when the Hook is called."""
failsafe = False failsafe = False
""" """
If True, the callback is guaranteed to run even if other callbacks If True, the callback is guaranteed to run even if other callbacks
from the same call point raise exceptions.""" from the same call point raise exceptions."""
priority = 50 priority = 50
""" """
Defines the order of execution for a list of Hooks. Priority numbers Defines the order of execution for a list of Hooks. Priority numbers
should be limited to the closed interval [0, 100], but values outside should be limited to the closed interval [0, 100], but values outside
this range are acceptable, as are fractional values.""" this range are acceptable, as are fractional values."""
kwargs = {} kwargs = {}
""" """
A set of keyword arguments that will be passed to the A set of keyword arguments that will be passed to the
callable on each call.""" callable on each call."""
def __init__(self, callback, failsafe=None, priority=None, **kwargs): def __init__(self, callback, failsafe=None, priority=None, **kwargs):
self.callback = callback self.callback = callback
if failsafe is None: if failsafe is None:
failsafe = getattr(callback, "failsafe", False) failsafe = getattr(callback, "failsafe", False)
self.failsafe = failsafe self.failsafe = failsafe
if priority is None: if priority is None:
priority = getattr(callback, "priority", 50) priority = getattr(callback, "priority", 50)
self.priority = priority self.priority = priority
self.kwargs = kwargs self.kwargs = kwargs
def __lt__(self, other): def __lt__(self, other):
# Python 3 # Python 3
return self.priority < other.priority return self.priority < other.priority
@@ -56,11 +56,11 @@ class Hook(object):
def __cmp__(self, other): def __cmp__(self, other):
# Python 2 # Python 2
return cmp(self.priority, other.priority) return cmp(self.priority, other.priority)
def __call__(self): def __call__(self):
"""Run self.callback(**self.kwargs).""" """Run self.callback(**self.kwargs)."""
return self.callback(**self.kwargs) return self.callback(**self.kwargs)
def __repr__(self): def __repr__(self):
cls = self.__class__ cls = self.__class__
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)" return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
@@ -72,20 +72,20 @@ class Hook(object):
class HookMap(dict): class HookMap(dict):
"""A map of call points to lists of callbacks (Hook objects).""" """A map of call points to lists of callbacks (Hook objects)."""
def __new__(cls, points=None): def __new__(cls, points=None):
d = dict.__new__(cls) d = dict.__new__(cls)
for p in points or []: for p in points or []:
d[p] = [] d[p] = []
return d return d
def __init__(self, *a, **kw): def __init__(self, *a, **kw):
pass pass
def attach(self, point, callback, failsafe=None, priority=None, **kwargs): def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
"""Append a new Hook made from the supplied arguments.""" """Append a new Hook made from the supplied arguments."""
self[point].append(Hook(callback, failsafe, priority, **kwargs)) self[point].append(Hook(callback, failsafe, priority, **kwargs))
def run(self, point): def run(self, point):
"""Execute all registered Hooks (callbacks) for the given point.""" """Execute all registered Hooks (callbacks) for the given point."""
exc = None exc = None
@@ -110,7 +110,7 @@ class HookMap(dict):
cherrypy.log(traceback=True, severity=40) cherrypy.log(traceback=True, severity=40)
if exc: if exc:
raise exc raise exc
def __copy__(self): def __copy__(self):
newmap = self.__class__() newmap = self.__class__()
# We can't just use 'update' because we want copies of the # We can't just use 'update' because we want copies of the
@@ -119,7 +119,7 @@ class HookMap(dict):
newmap[k] = v[:] newmap[k] = v[:]
return newmap return newmap
copy = __copy__ copy = __copy__
def __repr__(self): def __repr__(self):
cls = self.__class__ cls = self.__class__
return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self)) return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
@@ -171,7 +171,7 @@ hookpoints = ['on_start_resource', 'before_request_body',
class Request(object): class Request(object):
"""An HTTP request. """An HTTP request.
This object represents the metadata of an HTTP request message; This object represents the metadata of an HTTP request message;
that is, it contains attributes which describe the environment that is, it contains attributes which describe the environment
in which the request URL, headers, and body were sent (if you in which the request URL, headers, and body were sent (if you
@@ -181,43 +181,43 @@ class Request(object):
also contains data regarding the configuration in effect for also contains data regarding the configuration in effect for
the given URL, and the execution plan for generating a response. the given URL, and the execution plan for generating a response.
""" """
prev = None prev = None
""" """
The previous Request object (if any). This should be None The previous Request object (if any). This should be None
unless we are processing an InternalRedirect.""" unless we are processing an InternalRedirect."""
# Conversation/connection attributes # Conversation/connection attributes
local = httputil.Host("127.0.0.1", 80) local = httputil.Host("127.0.0.1", 80)
"An httputil.Host(ip, port, hostname) object for the server socket." "An httputil.Host(ip, port, hostname) object for the server socket."
remote = httputil.Host("127.0.0.1", 1111) remote = httputil.Host("127.0.0.1", 1111)
"An httputil.Host(ip, port, hostname) object for the client socket." "An httputil.Host(ip, port, hostname) object for the client socket."
scheme = "http" scheme = "http"
""" """
The protocol used between client and server. In most cases, The protocol used between client and server. In most cases,
this will be either 'http' or 'https'.""" this will be either 'http' or 'https'."""
server_protocol = "HTTP/1.1" server_protocol = "HTTP/1.1"
""" """
The HTTP version for which the HTTP server is at least The HTTP version for which the HTTP server is at least
conditionally compliant.""" conditionally compliant."""
base = "" base = ""
"""The (scheme://host) portion of the requested URL. """The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but path segments which cherrypy.url uses when constructing url's, but
which otherwise are ignored by CherryPy. Regardless, this value which otherwise are ignored by CherryPy. Regardless, this value
MUST NOT end in a slash.""" MUST NOT end in a slash."""
# Request-Line attributes # Request-Line attributes
request_line = "" request_line = ""
""" """
The complete Request-Line received from the client. This is a The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed.""" version (joined by spaces). Any final CRLF is removed."""
method = "GET" method = "GET"
""" """
Indicates the HTTP method to be performed on the resource identified Indicates the HTTP method to be performed on the resource identified
@@ -225,7 +225,7 @@ class Request(object):
DELETE. CherryPy allows any extension method; however, various HTTP DELETE. CherryPy allows any extension method; however, various HTTP
servers and gateways may restrict the set of allowable methods. servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis).""" CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = "" query_string = ""
""" """
The query component of the Request-URI, a string of information to be The query component of the Request-URI, a string of information to be
@@ -233,7 +233,7 @@ class Request(object):
path component, and is separated by a '?'. For example, the URI path component, and is separated by a '?'. For example, the URI
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component, 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
'a=3&b=4'.""" 'a=3&b=4'."""
query_string_encoding = 'utf8' query_string_encoding = 'utf8'
""" """
The encoding expected for query string arguments after % HEX HEX decoding). The encoding expected for query string arguments after % HEX HEX decoding).
@@ -242,7 +242,7 @@ class Request(object):
arbitrary encodings to not error, set this to 'Latin-1'; you can then arbitrary encodings to not error, set this to 'Latin-1'; you can then
encode back to bytes and re-decode to whatever encoding you like later. encode back to bytes and re-decode to whatever encoding you like later.
""" """
protocol = (1, 1) protocol = (1, 1)
"""The HTTP protocol version corresponding to the set """The HTTP protocol version corresponding to the set
of features which should be allowed in the response. If BOTH of features which should be allowed in the response. If BOTH
@@ -250,20 +250,20 @@ class Request(object):
compliance is HTTP/1.1, this attribute will be the tuple (1, 1). compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
If either is 1.0, this attribute will be the tuple (1, 0). If either is 1.0, this attribute will be the tuple (1, 0).
Lower HTTP protocol versions are not explicitly supported.""" Lower HTTP protocol versions are not explicitly supported."""
params = {} params = {}
""" """
A dict which combines query string (GET) and request entity (POST) A dict which combines query string (GET) and request entity (POST)
variables. This is populated in two stages: GET params are added variables. This is populated in two stages: GET params are added
before the 'on_start_resource' hook, and POST params are added before the 'on_start_resource' hook, and POST params are added
between the 'before_request_body' and 'before_handler' hooks.""" between the 'before_request_body' and 'before_handler' hooks."""
# Message attributes # Message attributes
header_list = [] header_list = []
""" """
A list of the HTTP request headers as (name, value) tuples. A list of the HTTP request headers as (name, value) tuples.
In general, you should use request.headers (a dict) instead.""" In general, you should use request.headers (a dict) instead."""
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
""" """
A dict-like object containing the request headers. Keys are header A dict-like object containing the request headers. Keys are header
@@ -272,10 +272,10 @@ class Request(object):
headers['content-type'] refer to the same value. Values are header headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary). See also: values (decoded according to :rfc:`2047` if necessary). See also:
httputil.HeaderMap, httputil.HeaderElement.""" httputil.HeaderMap, httputil.HeaderElement."""
cookie = SimpleCookie() cookie = SimpleCookie()
"""See help(Cookie).""" """See help(Cookie)."""
rfile = None rfile = None
""" """
If the request included an entity (body), it will be available If the request included an entity (body), it will be available
@@ -283,11 +283,11 @@ class Request(object):
be read for you between the 'before_request_body' hook and the be read for you between the 'before_request_body' hook and the
'before_handler' hook, and the resulting string is placed into 'before_handler' hook, and the resulting string is placed into
either request.params or the request.body attribute. either request.params or the request.body attribute.
You may disable the automatic consumption of the rfile by setting You may disable the automatic consumption of the rfile by setting
request.process_request_body to False, either in config for the desired request.process_request_body to False, either in config for the desired
path, or in an 'on_start_resource' or 'before_request_body' hook. path, or in an 'on_start_resource' or 'before_request_body' hook.
WARNING: In almost every case, you should not attempt to read from the WARNING: In almost every case, you should not attempt to read from the
rfile stream after CherryPy's automatic mechanism has read it. If you rfile stream after CherryPy's automatic mechanism has read it. If you
turn off the automatic parsing of rfile, you should read exactly the turn off the automatic parsing of rfile, you should read exactly the
@@ -295,17 +295,17 @@ class Request(object):
Ignoring either of these warnings may result in a hung request thread Ignoring either of these warnings may result in a hung request thread
or in corruption of the next (pipelined) request. or in corruption of the next (pipelined) request.
""" """
process_request_body = True process_request_body = True
""" """
If True, the rfile (if any) is automatically read and parsed, If True, the rfile (if any) is automatically read and parsed,
and the result placed into request.params or request.body.""" and the result placed into request.params or request.body."""
methods_with_bodies = ("POST", "PUT") methods_with_bodies = ("POST", "PUT")
""" """
A sequence of HTTP methods for which CherryPy will automatically A sequence of HTTP methods for which CherryPy will automatically
attempt to read a body from the rfile.""" attempt to read a body from the rfile."""
body = None body = None
""" """
If the request Content-Type is 'application/x-www-form-urlencoded' If the request Content-Type is 'application/x-www-form-urlencoded'
@@ -313,7 +313,7 @@ class Request(object):
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
can .read()); this value is set between the 'before_request_body' and can .read()); this value is set between the 'before_request_body' and
'before_handler' hooks (assuming that process_request_body is True).""" 'before_handler' hooks (assuming that process_request_body is True)."""
# Dispatch attributes # Dispatch attributes
dispatch = cherrypy.dispatch.Dispatcher() dispatch = cherrypy.dispatch.Dispatcher()
""" """
@@ -322,19 +322,19 @@ class Request(object):
request attributes, and the application architecture. The core request attributes, and the application architecture. The core
calls the dispatcher as early as possible, passing it a 'path_info' calls the dispatcher as early as possible, passing it a 'path_info'
argument. argument.
The default dispatcher discovers the page handler by matching path_info The default dispatcher discovers the page handler by matching path_info
to a hierarchical arrangement of objects, starting at request.app.root. to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information.""" See help(cherrypy.dispatch) for more information."""
script_name = "" script_name = ""
""" """
The 'mount point' of the application which is handling this request. The 'mount point' of the application which is handling this request.
This attribute MUST NOT end in a slash. If the script_name refers to This attribute MUST NOT end in a slash. If the script_name refers to
the root of the URI, it MUST be an empty string (not "/"). the root of the URI, it MUST be an empty string (not "/").
""" """
path_info = "/" path_info = "/"
""" """
The 'relative path' portion of the Request-URI. This is relative The 'relative path' portion of the Request-URI. This is relative
@@ -346,12 +346,12 @@ class Request(object):
When authentication is used during the request processing this is When authentication is used during the request processing this is
set to 'False' if it failed and to the 'username' value if it succeeded. set to 'False' if it failed and to the 'username' value if it succeeded.
The default 'None' implies that no authentication happened.""" The default 'None' implies that no authentication happened."""
# Note that cherrypy.url uses "if request.app:" to determine whether # Note that cherrypy.url uses "if request.app:" to determine whether
# the call is during a real HTTP request or not. So leave this None. # the call is during a real HTTP request or not. So leave this None.
app = None app = None
"""The cherrypy.Application object which is handling this request.""" """The cherrypy.Application object which is handling this request."""
handler = None handler = None
""" """
The function, method, or other callable which CherryPy will call to The function, method, or other callable which CherryPy will call to
@@ -360,12 +360,12 @@ class Request(object):
By default, the handler is discovered by walking a tree of objects By default, the handler is discovered by walking a tree of objects
starting at request.app.root, and is then passed all HTTP params starting at request.app.root, and is then passed all HTTP params
(from the query string and POST body) as keyword arguments.""" (from the query string and POST body) as keyword arguments."""
toolmaps = {} toolmaps = {}
""" """
A nested dict of all Toolboxes and Tools in effect for this request, A nested dict of all Toolboxes and Tools in effect for this request,
of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
config = None config = None
""" """
A flat dict of all configuration entries which apply to the A flat dict of all configuration entries which apply to the
@@ -375,7 +375,7 @@ class Request(object):
effect for this request; by default, handler config can be attached effect for this request; by default, handler config can be attached
anywhere in the tree between request.app.root and the final handler, anywhere in the tree between request.app.root and the final handler,
and inherits downward).""" and inherits downward)."""
is_index = None is_index = None
""" """
This will be True if the current request is mapped to an 'index' This will be True if the current request is mapped to an 'index'
@@ -383,7 +383,7 @@ class Request(object):
a slash). The value may be used to automatically redirect the a slash). The value may be used to automatically redirect the
user-agent to a 'more canonical' URL which either adds or removes user-agent to a 'more canonical' URL which either adds or removes
the trailing slash. See cherrypy.tools.trailing_slash.""" the trailing slash. See cherrypy.tools.trailing_slash."""
hooks = HookMap(hookpoints) hooks = HookMap(hookpoints)
""" """
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
@@ -392,7 +392,7 @@ class Request(object):
The list of hooks is generally populated as early as possible (mostly The list of hooks is generally populated as early as possible (mostly
from Tools specified in config), but may be extended at any time. from Tools specified in config), but may be extended at any time.
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
error_response = cherrypy.HTTPError(500).set_response error_response = cherrypy.HTTPError(500).set_response
""" """
The no-arg callable which will handle unexpected, untrapped errors The no-arg callable which will handle unexpected, untrapped errors
@@ -402,31 +402,31 @@ class Request(object):
via request.error_page or by overriding HTTPError.set_response). via request.error_page or by overriding HTTPError.set_response).
By default, error_response uses HTTPError(500) to return a generic By default, error_response uses HTTPError(500) to return a generic
error response to the user-agent.""" error response to the user-agent."""
error_page = {} error_page = {}
""" """
A dict of {error code: response filename or callable} pairs. A dict of {error code: response filename or callable} pairs.
The error code must be an int representing a given HTTP error code, The error code must be an int representing a given HTTP error code,
or the string 'default', which will be used if no matching entry or the string 'default', which will be used if no matching entry
is found for a given numeric code. is found for a given numeric code.
If a filename is provided, the file should contain a Python string- If a filename is provided, the file should contain a Python string-
formatting template, and can expect by default to receive format formatting template, and can expect by default to receive format
values with the mapping keys %(status)s, %(message)s, %(traceback)s, values with the mapping keys %(status)s, %(message)s, %(traceback)s,
and %(version)s. The set of format mappings can be extended by and %(version)s. The set of format mappings can be extended by
overriding HTTPError.set_response. overriding HTTPError.set_response.
If a callable is provided, it will be called by default with keyword If a callable is provided, it will be called by default with keyword
arguments 'status', 'message', 'traceback', and 'version', as for a arguments 'status', 'message', 'traceback', and 'version', as for a
string-formatting template. The callable must return a string or iterable of string-formatting template. The callable must return a string or iterable of
strings which will be set to response.body. It may also override headers or strings which will be set to response.body. It may also override headers or
perform any other processing. perform any other processing.
If no entry is given for an error code, and no 'default' entry exists, If no entry is given for an error code, and no 'default' entry exists,
a default template will be used. a default template will be used.
""" """
show_tracebacks = True show_tracebacks = True
""" """
If True, unexpected errors encountered during request processing will If True, unexpected errors encountered during request processing will
@@ -436,23 +436,23 @@ class Request(object):
""" """
If True, mismatched parameters encountered during PageHandler invocation If True, mismatched parameters encountered during PageHandler invocation
processing will be included in the response body.""" processing will be included in the response body."""
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
"""The sequence of exceptions which Request.run does not trap.""" """The sequence of exceptions which Request.run does not trap."""
throw_errors = False throw_errors = False
""" """
If True, Request.run will not trap any errors (except HTTPRedirect and If True, Request.run will not trap any errors (except HTTPRedirect and
HTTPError, which are more properly called 'exceptions', not errors).""" HTTPError, which are more properly called 'exceptions', not errors)."""
closed = False closed = False
"""True once the close method has been called, False otherwise.""" """True once the close method has been called, False otherwise."""
stage = None stage = None
""" """
A string containing the stage reached in the request-handling process. A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests.""" This is useful when debugging a live server with hung requests."""
namespaces = _cpconfig.NamespaceSet( namespaces = _cpconfig.NamespaceSet(
**{"hooks": hooks_namespace, **{"hooks": hooks_namespace,
"request": request_namespace, "request": request_namespace,
@@ -460,11 +460,11 @@ class Request(object):
"error_page": error_page_namespace, "error_page": error_page_namespace,
"tools": cherrypy.tools, "tools": cherrypy.tools,
}) })
def __init__(self, local_host, remote_host, scheme="http", def __init__(self, local_host, remote_host, scheme="http",
server_protocol="HTTP/1.1"): server_protocol="HTTP/1.1"):
"""Populate a new Request object. """Populate a new Request object.
local_host should be an httputil.Host object with the server info. local_host should be an httputil.Host object with the server info.
remote_host should be an httputil.Host object with the client info. remote_host should be an httputil.Host object with the client info.
scheme should be a string, either "http" or "https". scheme should be a string, either "http" or "https".
@@ -473,17 +473,17 @@ class Request(object):
self.remote = remote_host self.remote = remote_host
self.scheme = scheme self.scheme = scheme
self.server_protocol = server_protocol self.server_protocol = server_protocol
self.closed = False self.closed = False
# Put a *copy* of the class error_page into self. # Put a *copy* of the class error_page into self.
self.error_page = self.error_page.copy() self.error_page = self.error_page.copy()
# Put a *copy* of the class namespaces into self. # Put a *copy* of the class namespaces into self.
self.namespaces = self.namespaces.copy() self.namespaces = self.namespaces.copy()
self.stage = None self.stage = None
def close(self): def close(self):
"""Run cleanup code. (Core)""" """Run cleanup code. (Core)"""
if not self.closed: if not self.closed:
@@ -491,49 +491,49 @@ class Request(object):
self.stage = 'on_end_request' self.stage = 'on_end_request'
self.hooks.run('on_end_request') self.hooks.run('on_end_request')
self.stage = 'close' self.stage = 'close'
def run(self, method, path, query_string, req_protocol, headers, rfile): def run(self, method, path, query_string, req_protocol, headers, rfile):
r"""Process the Request. (Core) r"""Process the Request. (Core)
method, path, query_string, and req_protocol should be pulled directly method, path, query_string, and req_protocol should be pulled directly
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
path path
This should be %XX-unquoted, but query_string should not be. This should be %XX-unquoted, but query_string should not be.
When using Python 2, they both MUST be byte strings, When using Python 2, they both MUST be byte strings,
not unicode strings. not unicode strings.
When using Python 3, they both MUST be unicode strings, When using Python 3, they both MUST be unicode strings,
not byte strings, and preferably not bytes \x00-\xFF not byte strings, and preferably not bytes \x00-\xFF
disguised as unicode. disguised as unicode.
headers headers
A list of (name, value) tuples. A list of (name, value) tuples.
rfile rfile
A file-like object containing the HTTP request entity. A file-like object containing the HTTP request entity.
When run() is done, the returned object should have 3 attributes: When run() is done, the returned object should have 3 attributes:
* status, e.g. "200 OK" * status, e.g. "200 OK"
* header_list, a list of (name, value) tuples * header_list, a list of (name, value) tuples
* body, an iterable yielding strings * body, an iterable yielding strings
Consumer code (HTTP servers) should then access these response Consumer code (HTTP servers) should then access these response
attributes to build the outbound stream. attributes to build the outbound stream.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
self.stage = 'run' self.stage = 'run'
try: try:
self.error_response = cherrypy.HTTPError(500).set_response self.error_response = cherrypy.HTTPError(500).set_response
self.method = method self.method = method
path = path or "/" path = path or "/"
self.query_string = query_string or '' self.query_string = query_string or ''
self.params = {} self.params = {}
# Compare request and server HTTP protocol versions, in case our # Compare request and server HTTP protocol versions, in case our
# server does not support the requested protocol. Limit our output # server does not support the requested protocol. Limit our output
# to min(req, server). We want the following output: # to min(req, server). We want the following output:
@@ -550,30 +550,30 @@ class Request(object):
sp = int(self.server_protocol[5]), int(self.server_protocol[7]) sp = int(self.server_protocol[5]), int(self.server_protocol[7])
self.protocol = min(rp, sp) self.protocol = min(rp, sp)
response.headers.protocol = self.protocol response.headers.protocol = self.protocol
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
url = path url = path
if query_string: if query_string:
url += '?' + query_string url += '?' + query_string
self.request_line = '%s %s %s' % (method, url, req_protocol) self.request_line = '%s %s %s' % (method, url, req_protocol)
self.header_list = list(headers) self.header_list = list(headers)
self.headers = httputil.HeaderMap() self.headers = httputil.HeaderMap()
self.rfile = rfile self.rfile = rfile
self.body = None self.body = None
self.cookie = SimpleCookie() self.cookie = SimpleCookie()
self.handler = None self.handler = None
# path_info should be the path from the # path_info should be the path from the
# app root (script_name) to the handler. # app root (script_name) to the handler.
self.script_name = self.app.script_name self.script_name = self.app.script_name
self.path_info = pi = path[len(self.script_name):] self.path_info = pi = path[len(self.script_name):]
self.stage = 'respond' self.stage = 'respond'
self.respond(pi) self.respond(pi)
except self.throws: except self.throws:
raise raise
except: except:
@@ -589,24 +589,24 @@ class Request(object):
body = "" body = ""
r = bare_error(body) r = bare_error(body)
response.output_status, response.header_list, response.body = r response.output_status, response.header_list, response.body = r
if self.method == "HEAD": if self.method == "HEAD":
# HEAD requests MUST NOT return a message-body in the response. # HEAD requests MUST NOT return a message-body in the response.
response.body = [] response.body = []
try: try:
cherrypy.log.access() cherrypy.log.access()
except: except:
cherrypy.log.error(traceback=True) cherrypy.log.error(traceback=True)
if response.timed_out: if response.timed_out:
raise cherrypy.TimeoutError() raise cherrypy.TimeoutError()
return response return response
# Uncomment for stage debugging # Uncomment for stage debugging
# stage = property(lambda self: self._stage, lambda self, v: print(v)) # stage = property(lambda self: self._stage, lambda self, v: print(v))
def respond(self, path_info): def respond(self, path_info):
"""Generate a response for the resource at self.path_info. (Core)""" """Generate a response for the resource at self.path_info. (Core)"""
response = cherrypy.serving.response response = cherrypy.serving.response
@@ -615,30 +615,30 @@ class Request(object):
try: try:
if self.app is None: if self.app is None:
raise cherrypy.NotFound() raise cherrypy.NotFound()
# Get the 'Host' header, so we can HTTPRedirect properly. # Get the 'Host' header, so we can HTTPRedirect properly.
self.stage = 'process_headers' self.stage = 'process_headers'
self.process_headers() self.process_headers()
# Make a copy of the class hooks # Make a copy of the class hooks
self.hooks = self.__class__.hooks.copy() self.hooks = self.__class__.hooks.copy()
self.toolmaps = {} self.toolmaps = {}
self.stage = 'get_resource' self.stage = 'get_resource'
self.get_resource(path_info) self.get_resource(path_info)
self.body = _cpreqbody.RequestBody( self.body = _cpreqbody.RequestBody(
self.rfile, self.headers, request_params=self.params) self.rfile, self.headers, request_params=self.params)
self.namespaces(self.config) self.namespaces(self.config)
self.stage = 'on_start_resource' self.stage = 'on_start_resource'
self.hooks.run('on_start_resource') self.hooks.run('on_start_resource')
# Parse the querystring # Parse the querystring
self.stage = 'process_query_string' self.stage = 'process_query_string'
self.process_query_string() self.process_query_string()
# Process the body # Process the body
if self.process_request_body: if self.process_request_body:
if self.method not in self.methods_with_bodies: if self.method not in self.methods_with_bodies:
@@ -647,14 +647,14 @@ class Request(object):
self.hooks.run('before_request_body') self.hooks.run('before_request_body')
if self.process_request_body: if self.process_request_body:
self.body.process() self.body.process()
# Run the handler # Run the handler
self.stage = 'before_handler' self.stage = 'before_handler'
self.hooks.run('before_handler') self.hooks.run('before_handler')
if self.handler: if self.handler:
self.stage = 'handler' self.stage = 'handler'
response.body = self.handler() response.body = self.handler()
# Finalize # Finalize
self.stage = 'before_finalize' self.stage = 'before_finalize'
self.hooks.run('before_finalize') self.hooks.run('before_finalize')
@@ -674,7 +674,7 @@ class Request(object):
if self.throw_errors: if self.throw_errors:
raise raise
self.handle_error() self.handle_error()
def process_query_string(self): def process_query_string(self):
"""Parse the query string into Python structures. (Core)""" """Parse the query string into Python structures. (Core)"""
try: try:
@@ -685,7 +685,7 @@ class Request(object):
404, "The given query string could not be processed. Query " 404, "The given query string could not be processed. Query "
"strings for this resource must be encoded with %r." % "strings for this resource must be encoded with %r." %
self.query_string_encoding) self.query_string_encoding)
# Python 2 only: keyword arguments must be byte strings (type 'str'). # Python 2 only: keyword arguments must be byte strings (type 'str').
if not py3k: if not py3k:
for key, value in p.items(): for key, value in p.items():
@@ -693,7 +693,7 @@ class Request(object):
del p[key] del p[key]
p[key.encode(self.query_string_encoding)] = value p[key.encode(self.query_string_encoding)] = value
self.params.update(p) self.params.update(p)
def process_headers(self): def process_headers(self):
"""Parse HTTP header data into Python structures. (Core)""" """Parse HTTP header data into Python structures. (Core)"""
# Process the headers into self.headers # Process the headers into self.headers
@@ -703,7 +703,7 @@ class Request(object):
# so title doesn't have to be called twice. # so title doesn't have to be called twice.
name = name.title() name = name.title()
value = value.strip() value = value.strip()
# Warning: if there is more than one header entry for cookies (AFAIK, # Warning: if there is more than one header entry for cookies (AFAIK,
# only Konqueror does that), only the last one will remain in headers # only Konqueror does that), only the last one will remain in headers
# (but they will be correctly stored in request.cookie). # (but they will be correctly stored in request.cookie).
@@ -711,7 +711,7 @@ class Request(object):
dict.__setitem__(headers, name, httputil.decode_TEXT(value)) dict.__setitem__(headers, name, httputil.decode_TEXT(value))
else: else:
dict.__setitem__(headers, name, value) dict.__setitem__(headers, name, value)
# Handle cookies differently because on Konqueror, multiple # Handle cookies differently because on Konqueror, multiple
# cookies come on different lines with the same key # cookies come on different lines with the same key
if name == 'Cookie': if name == 'Cookie':
@@ -720,7 +720,7 @@ class Request(object):
except CookieError: except CookieError:
msg = "Illegal cookie name %s" % value.split('=')[0] msg = "Illegal cookie name %s" % value.split('=')[0]
raise cherrypy.HTTPError(400, msg) raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'): if not dict.__contains__(headers, 'Host'):
# All Internet-based HTTP/1.1 servers MUST respond with a 400 # All Internet-based HTTP/1.1 servers MUST respond with a 400
# (Bad Request) status code to any HTTP/1.1 request message # (Bad Request) status code to any HTTP/1.1 request message
@@ -732,17 +732,17 @@ class Request(object):
if not host: if not host:
host = self.local.name or self.local.ip host = self.local.name or self.local.ip
self.base = "%s://%s" % (self.scheme, host) self.base = "%s://%s" % (self.scheme, host)
def get_resource(self, path): def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)""" """Call a dispatcher (which sets self.handler and .config). (Core)"""
# First, see if there is a custom dispatch at this URI. Custom # First, see if there is a custom dispatch at this URI. Custom
# dispatchers can only be specified in app.config, not in _cp_config # dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root). # (since custom dispatchers may not even have an app.root).
dispatch = self.app.find_config(path, "request.dispatch", self.dispatch) dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
# dispatch() should set self.handler and self.config # dispatch() should set self.handler and self.config
dispatch(path) dispatch(path)
def handle_error(self): def handle_error(self):
"""Handle the last unanticipated exception. (Core)""" """Handle the last unanticipated exception. (Core)"""
try: try:
@@ -755,9 +755,9 @@ class Request(object):
inst = sys.exc_info()[1] inst = sys.exc_info()[1]
inst.set_response() inst.set_response()
cherrypy.serving.response.finalize() cherrypy.serving.response.finalize()
# ------------------------- Properties ------------------------- # # ------------------------- Properties ------------------------- #
def _get_body_params(self): def _get_body_params(self):
warnings.warn( warnings.warn(
"body_params is deprecated in CherryPy 3.2, will be removed in " "body_params is deprecated in CherryPy 3.2, will be removed in "
@@ -774,30 +774,30 @@ class Request(object):
can be sent with various HTTP method verbs). This value is set between can be sent with various HTTP method verbs). This value is set between
the 'before_request_body' and 'before_handler' hooks (assuming that the 'before_request_body' and 'before_handler' hooks (assuming that
process_request_body is True). process_request_body is True).
Deprecated in 3.2, will be removed for 3.3 in favor of Deprecated in 3.2, will be removed for 3.3 in favor of
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""") :attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
class ResponseBody(object): class ResponseBody(object):
"""The body of the HTTP response (the response entity).""" """The body of the HTTP response (the response entity)."""
if py3k: if py3k:
unicode_err = ("Page handlers MUST return bytes. Use tools.encode " unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
"if you wish to return unicode.") "if you wish to return unicode.")
def __get__(self, obj, objclass=None): def __get__(self, obj, objclass=None):
if obj is None: if obj is None:
# When calling on the class instead of an instance... # When calling on the class instead of an instance...
return self return self
else: else:
return obj._body return obj._body
def __set__(self, obj, value): def __set__(self, obj, value):
# Convert the given value to an iterable object. # Convert the given value to an iterable object.
if py3k and isinstance(value, str): if py3k and isinstance(value, str):
raise ValueError(self.unicode_err) raise ValueError(self.unicode_err)
if isinstance(value, basestring): if isinstance(value, basestring):
# strings get wrapped in a list because iterating over a single # strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character # item list is much faster than iterating over every character
@@ -808,7 +808,7 @@ class ResponseBody(object):
# [''] doesn't evaluate to False, so replace it with []. # [''] doesn't evaluate to False, so replace it with [].
value = [] value = []
elif py3k and isinstance(value, list): elif py3k and isinstance(value, list):
# every item in a list must be bytes... # every item in a list must be bytes...
for i, item in enumerate(value): for i, item in enumerate(value):
if isinstance(item, str): if isinstance(item, str):
raise ValueError(self.unicode_err) raise ValueError(self.unicode_err)
@@ -823,17 +823,17 @@ class ResponseBody(object):
class Response(object): class Response(object):
"""An HTTP Response, including status, headers, and body.""" """An HTTP Response, including status, headers, and body."""
status = "" status = ""
"""The HTTP Status-Code and Reason-Phrase.""" """The HTTP Status-Code and Reason-Phrase."""
header_list = [] header_list = []
""" """
A list of the HTTP response headers as (name, value) tuples. A list of the HTTP response headers as (name, value) tuples.
In general, you should use response.headers (a dict) instead. This In general, you should use response.headers (a dict) instead. This
attribute is generated from response.headers and is not valid until attribute is generated from response.headers and is not valid until
after the finalize phase.""" after the finalize phase."""
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
""" """
A dict-like object containing the response headers. Keys are header A dict-like object containing the response headers. Keys are header
@@ -841,36 +841,36 @@ class Response(object):
a case-insensitive manner. That is, headers['Content-Type'] and a case-insensitive manner. That is, headers['Content-Type'] and
headers['content-type'] refer to the same value. Values are header headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary). values (decoded according to :rfc:`2047` if necessary).
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
""" """
cookie = SimpleCookie() cookie = SimpleCookie()
"""See help(Cookie).""" """See help(Cookie)."""
body = ResponseBody() body = ResponseBody()
"""The body (entity) of the HTTP response.""" """The body (entity) of the HTTP response."""
time = None time = None
"""The value of time.time() when created. Use in HTTP dates.""" """The value of time.time() when created. Use in HTTP dates."""
timeout = 300 timeout = 300
"""Seconds after which the response will be aborted.""" """Seconds after which the response will be aborted."""
timed_out = False timed_out = False
""" """
Flag to indicate the response should be aborted, because it has Flag to indicate the response should be aborted, because it has
exceeded its timeout.""" exceeded its timeout."""
stream = False stream = False
"""If False, buffer the response body.""" """If False, buffer the response body."""
def __init__(self): def __init__(self):
self.status = None self.status = None
self.header_list = None self.header_list = None
self._body = [] self._body = []
self.time = time.time() self.time = time.time()
self.headers = httputil.HeaderMap() self.headers = httputil.HeaderMap()
# Since we know all our keys are titled strings, we can # Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost. # bypass HeaderMap.update and get a big speed boost.
@@ -880,34 +880,34 @@ class Response(object):
"Date": httputil.HTTPDate(self.time), "Date": httputil.HTTPDate(self.time),
}) })
self.cookie = SimpleCookie() self.cookie = SimpleCookie()
def collapse_body(self): def collapse_body(self):
"""Collapse self.body to a single string; replace it and return it.""" """Collapse self.body to a single string; replace it and return it."""
if isinstance(self.body, basestring): if isinstance(self.body, basestring):
return self.body return self.body
newbody = [] newbody = []
for chunk in self.body: for chunk in self.body:
if py3k and not isinstance(chunk, bytes): if py3k and not isinstance(chunk, bytes):
raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk)) raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk))
newbody.append(chunk) newbody.append(chunk)
newbody = ntob('').join(newbody) newbody = ntob('').join(newbody)
self.body = newbody self.body = newbody
return newbody return newbody
def finalize(self): def finalize(self):
"""Transform headers (and cookies) into self.header_list. (Core)""" """Transform headers (and cookies) into self.header_list. (Core)"""
try: try:
code, reason, _ = httputil.valid_status(self.status) code, reason, _ = httputil.valid_status(self.status)
except ValueError: except ValueError:
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
headers = self.headers headers = self.headers
self.status = "%s %s" % (code, reason) self.status = "%s %s" % (code, reason)
self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason) self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
if self.stream: if self.stream:
# The upshot: wsgiserver will chunk the response if # The upshot: wsgiserver will chunk the response if
# you pop Content-Length (or set it explicitly to None). # you pop Content-Length (or set it explicitly to None).
@@ -926,10 +926,10 @@ class Response(object):
if dict.get(headers, 'Content-Length') is None: if dict.get(headers, 'Content-Length') is None:
content = self.collapse_body() content = self.collapse_body()
dict.__setitem__(headers, 'Content-Length', len(content)) dict.__setitem__(headers, 'Content-Length', len(content))
# Transform our header dict into a list of tuples. # Transform our header dict into a list of tuples.
self.header_list = h = headers.output() self.header_list = h = headers.output()
cookie = self.cookie.output() cookie = self.cookie.output()
if cookie: if cookie:
for line in cookie.split("\n"): for line in cookie.split("\n"):
@@ -942,10 +942,10 @@ class Response(object):
if isinstance(value, unicodestr): if isinstance(value, unicodestr):
value = headers.encode(value) value = headers.encode(value)
h.append((name, value)) h.append((name, value))
def check_timeout(self): def check_timeout(self):
"""If now > self.time + self.timeout, set self.timed_out. """If now > self.time + self.timeout, set self.timed_out.
This purposefully sets a flag, rather than raising an error, This purposefully sets a flag, rather than raising an error,
so that a monitor thread can interrupt the Response thread. so that a monitor thread can interrupt the Response thread.
""" """
+33 -33
View File
@@ -13,18 +13,18 @@ from cherrypy.process.servers import *
class Server(ServerAdapter): class Server(ServerAdapter):
"""An adapter for an HTTP server. """An adapter for an HTTP server.
You can set attributes (like socket_host and socket_port) You can set attributes (like socket_host and socket_port)
on *this* object (which is probably cherrypy.server), and call on *this* object (which is probably cherrypy.server), and call
quickstart. For example:: quickstart. For example::
cherrypy.server.socket_port = 80 cherrypy.server.socket_port = 80
cherrypy.quickstart() cherrypy.quickstart()
""" """
socket_port = 8080 socket_port = 8080
"""The TCP port on which to listen for connections.""" """The TCP port on which to listen for connections."""
_socket_host = '127.0.0.1' _socket_host = '127.0.0.1'
def _get_socket_host(self): def _get_socket_host(self):
return self._socket_host return self._socket_host
@@ -36,68 +36,68 @@ class Server(ServerAdapter):
self._socket_host = value self._socket_host = value
socket_host = property(_get_socket_host, _set_socket_host, socket_host = property(_get_socket_host, _set_socket_host,
doc="""The hostname or IP address on which to listen for connections. doc="""The hostname or IP address on which to listen for connections.
Host values may be any IPv4 or IPv6 address, or any valid hostname. Host values may be any IPv4 or IPv6 address, or any valid hostname.
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
your hosts file prefers IPv6). The string '0.0.0.0' is a special your hosts file prefers IPv6). The string '0.0.0.0' is a special
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
is the similar IN6ADDR_ANY for IPv6. The empty string or None are is the similar IN6ADDR_ANY for IPv6. The empty string or None are
not allowed.""") not allowed.""")
socket_file = None socket_file = None
"""If given, the name of the UNIX socket to use instead of TCP/IP. """If given, the name of the UNIX socket to use instead of TCP/IP.
When this option is not None, the `socket_host` and `socket_port` options When this option is not None, the `socket_host` and `socket_port` options
are ignored.""" are ignored."""
socket_queue_size = 5 socket_queue_size = 5
"""The 'backlog' argument to socket.listen(); specifies the maximum number """The 'backlog' argument to socket.listen(); specifies the maximum number
of queued connections (default 5).""" of queued connections (default 5)."""
socket_timeout = 10 socket_timeout = 10
"""The timeout in seconds for accepted connections (default 10).""" """The timeout in seconds for accepted connections (default 10)."""
shutdown_timeout = 5 shutdown_timeout = 5
"""The time to wait for HTTP worker threads to clean up.""" """The time to wait for HTTP worker threads to clean up."""
protocol_version = 'HTTP/1.1' protocol_version = 'HTTP/1.1'
"""The version string to write in the Status-Line of all HTTP responses, """The version string to write in the Status-Line of all HTTP responses,
for example, "HTTP/1.1" (the default). Depending on the HTTP server used, for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
this should also limit the supported features used in the response.""" this should also limit the supported features used in the response."""
thread_pool = 10 thread_pool = 10
"""The number of worker threads to start up in the pool.""" """The number of worker threads to start up in the pool."""
thread_pool_max = -1 thread_pool_max = -1
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" """The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
max_request_header_size = 500 * 1024 max_request_header_size = 500 * 1024
"""The maximum number of bytes allowable in the request headers. If exceeded, """The maximum number of bytes allowable in the request headers. If exceeded,
the HTTP server should return "413 Request Entity Too Large".""" the HTTP server should return "413 Request Entity Too Large"."""
max_request_body_size = 100 * 1024 * 1024 max_request_body_size = 100 * 1024 * 1024
"""The maximum number of bytes allowable in the request body. If exceeded, """The maximum number of bytes allowable in the request body. If exceeded,
the HTTP server should return "413 Request Entity Too Large".""" the HTTP server should return "413 Request Entity Too Large"."""
instance = None instance = None
"""If not None, this should be an HTTP server instance (such as """If not None, this should be an HTTP server instance (such as
CPWSGIServer) which cherrypy.server will control. Use this when you need CPWSGIServer) which cherrypy.server will control. Use this when you need
more control over object instantiation than is available in the various more control over object instantiation than is available in the various
configuration options.""" configuration options."""
ssl_context = None ssl_context = None
"""When using PyOpenSSL, an instance of SSL.Context.""" """When using PyOpenSSL, an instance of SSL.Context."""
ssl_certificate = None ssl_certificate = None
"""The filename of the SSL certificate to use.""" """The filename of the SSL certificate to use."""
ssl_certificate_chain = None ssl_certificate_chain = None
"""When using PyOpenSSL, the certificate chain to pass to """When using PyOpenSSL, the certificate chain to pass to
Context.load_verify_locations.""" Context.load_verify_locations."""
ssl_private_key = None ssl_private_key = None
"""The filename of the private key to use with SSL.""" """The filename of the private key to use with SSL."""
if py3k: if py3k:
ssl_module = 'builtin' ssl_module = 'builtin'
"""The name of a registered SSL adaptation module to use with the builtin """The name of a registered SSL adaptation module to use with the builtin
@@ -111,13 +111,13 @@ class Server(ServerAdapter):
into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
project, which you must install separately). You may also register your project, which you must install separately). You may also register your
own classes in the wsgiserver.ssl_adapters dict.""" own classes in the wsgiserver.ssl_adapters dict."""
statistics = False statistics = False
"""Turns statistics-gathering on or off for aware HTTP servers.""" """Turns statistics-gathering on or off for aware HTTP servers."""
nodelay = True nodelay = True
"""If True (the default since 3.1), sets the TCP_NODELAY socket option.""" """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
wsgi_version = (1, 0) wsgi_version = (1, 0)
"""The WSGI version tuple to use with the builtin WSGI server. """The WSGI version tuple to use with the builtin WSGI server.
The provided options are (1, 0) [which includes support for PEP 3333, The provided options are (1, 0) [which includes support for PEP 3333,
@@ -125,13 +125,13 @@ class Server(ServerAdapter):
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
You may create and register your own experimental versions of the WSGI You may create and register your own experimental versions of the WSGI
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
def __init__(self): def __init__(self):
self.bus = cherrypy.engine self.bus = cherrypy.engine
self.httpserver = None self.httpserver = None
self.interrupt = None self.interrupt = None
self.running = False self.running = False
def httpserver_from_self(self, httpserver=None): def httpserver_from_self(self, httpserver=None):
"""Return a (httpserver, bind_addr) pair based on self attributes.""" """Return a (httpserver, bind_addr) pair based on self attributes."""
if httpserver is None: if httpserver is None:
@@ -143,14 +143,14 @@ class Server(ServerAdapter):
# Is anyone using this? Can I add an arg? # Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self) httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr return httpserver, self.bind_addr
def start(self): def start(self):
"""Start the HTTP server.""" """Start the HTTP server."""
if not self.httpserver: if not self.httpserver:
self.httpserver, self.bind_addr = self.httpserver_from_self() self.httpserver, self.bind_addr = self.httpserver_from_self()
ServerAdapter.start(self) ServerAdapter.start(self)
start.priority = 75 start.priority = 75
def _get_bind_addr(self): def _get_bind_addr(self):
if self.socket_file: if self.socket_file:
return self.socket_file return self.socket_file
@@ -176,12 +176,12 @@ class Server(ServerAdapter):
"domain sockets), not %r" % value) "domain sockets), not %r" % value)
bind_addr = property(_get_bind_addr, _set_bind_addr, bind_addr = property(_get_bind_addr, _set_bind_addr,
doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
def base(self): def base(self):
"""Return the base (scheme://host[:port] or sock file) for this server.""" """Return the base (scheme://host[:port] or sock file) for this server."""
if self.socket_file: if self.socket_file:
return self.socket_file return self.socket_file
host = self.socket_host host = self.socket_host
if host in ('0.0.0.0', '::'): if host in ('0.0.0.0', '::'):
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
@@ -189,9 +189,9 @@ class Server(ServerAdapter):
# safest thing to spit out in a URL. # safest thing to spit out in a URL.
import socket import socket
host = socket.gethostname() host = socket.gethostname()
port = self.socket_port port = self.socket_port
if self.ssl_certificate: if self.ssl_certificate:
scheme = "https" scheme = "https"
if port != 443: if port != 443:
@@ -200,6 +200,6 @@ class Server(ServerAdapter):
scheme = "http" scheme = "http"
if port != 80: if port != 80:
host += ":%s" % port host += ":%s" % port
return "%s://%s" % (scheme, host) return "%s://%s" % (scheme, host)
+74 -74
View File
@@ -2,18 +2,18 @@
Tools are usually designed to be used in a variety of ways (although some Tools are usually designed to be used in a variety of ways (although some
may only offer one if they choose): may only offer one if they choose):
Library calls Library calls
All tools are callables that can be used wherever needed. All tools are callables that can be used wherever needed.
The arguments are straightforward and should be detailed within the The arguments are straightforward and should be detailed within the
docstring. docstring.
Function decorators Function decorators
All tools, when called, may be used as decorators which configure All tools, when called, may be used as decorators which configure
individual CherryPy page handlers (methods on the CherryPy tree). individual CherryPy page handlers (methods on the CherryPy tree).
That is, "@tools.anytool()" should "turn on" the tool via the That is, "@tools.anytool()" should "turn on" the tool via the
decorated function's _cp_config attribute. decorated function's _cp_config attribute.
CherryPy config CherryPy config
If a tool exposes a "_setup" callable, it will be called If a tool exposes a "_setup" callable, it will be called
once per Request (if the feature is "turned on" via config). once per Request (if the feature is "turned on" via config).
@@ -48,12 +48,12 @@ _attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them
class Tool(object): class Tool(object):
"""A registered function for use with CherryPy request-processing hooks. """A registered function for use with CherryPy request-processing hooks.
help(tool.callable) should give you more information about this Tool. help(tool.callable) should give you more information about this Tool.
""" """
namespace = "tools" namespace = "tools"
def __init__(self, point, callable, name=None, priority=50): def __init__(self, point, callable, name=None, priority=50):
self._point = point self._point = point
self.callable = callable self.callable = callable
@@ -61,13 +61,13 @@ class Tool(object):
self._priority = priority self._priority = priority
self.__doc__ = self.callable.__doc__ self.__doc__ = self.callable.__doc__
self._setargs() self._setargs()
def _get_on(self): def _get_on(self):
raise AttributeError(_attr_error) raise AttributeError(_attr_error)
def _set_on(self, value): def _set_on(self, value):
raise AttributeError(_attr_error) raise AttributeError(_attr_error)
on = property(_get_on, _set_on) on = property(_get_on, _set_on)
def _setargs(self): def _setargs(self):
"""Copy func parameter names to obj attributes.""" """Copy func parameter names to obj attributes."""
try: try:
@@ -86,28 +86,28 @@ class Tool(object):
# but if we trap it here it doesn't prevent CP from working. # but if we trap it here it doesn't prevent CP from working.
except IndexError: except IndexError:
pass pass
def _merged_args(self, d=None): def _merged_args(self, d=None):
"""Return a dict of configuration entries for this Tool.""" """Return a dict of configuration entries for this Tool."""
if d: if d:
conf = d.copy() conf = d.copy()
else: else:
conf = {} conf = {}
tm = cherrypy.serving.request.toolmaps[self.namespace] tm = cherrypy.serving.request.toolmaps[self.namespace]
if self._name in tm: if self._name in tm:
conf.update(tm[self._name]) conf.update(tm[self._name])
if "on" in conf: if "on" in conf:
del conf["on"] del conf["on"]
return conf return conf
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Compile-time decorator (turn on the tool in config). """Compile-time decorator (turn on the tool in config).
For example:: For example::
@tools.proxy() @tools.proxy()
def whats_my_base(self): def whats_my_base(self):
return cherrypy.request.base return cherrypy.request.base
@@ -126,10 +126,10 @@ class Tool(object):
f._cp_config[subspace + k] = v f._cp_config[subspace + k] = v
return f return f
return tool_decorator return tool_decorator
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
@@ -143,7 +143,7 @@ class Tool(object):
class HandlerTool(Tool): class HandlerTool(Tool):
"""Tool which is called 'before main', that may skip normal handlers. """Tool which is called 'before main', that may skip normal handlers.
If the tool successfully handles the request (by setting response.body), If the tool successfully handles the request (by setting response.body),
if should return True. This will cause CherryPy to skip any 'normal' page if should return True. This will cause CherryPy to skip any 'normal' page
handler. If the tool did not handle the request, it should return False handler. If the tool did not handle the request, it should return False
@@ -151,15 +151,15 @@ class HandlerTool(Tool):
tool is declared AS a page handler (see the 'handler' method), returning tool is declared AS a page handler (see the 'handler' method), returning
False will raise NotFound. False will raise NotFound.
""" """
def __init__(self, callable, name=None): def __init__(self, callable, name=None):
Tool.__init__(self, 'before_handler', callable, name) Tool.__init__(self, 'before_handler', callable, name)
def handler(self, *args, **kwargs): def handler(self, *args, **kwargs):
"""Use this tool as a CherryPy page handler. """Use this tool as a CherryPy page handler.
For example:: For example::
class Root: class Root:
nav = tools.staticdir.handler(section="/nav", dir="nav", nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir) root=absDir)
@@ -171,14 +171,14 @@ class HandlerTool(Tool):
return cherrypy.serving.response.body return cherrypy.serving.response.body
handle_func.exposed = True handle_func.exposed = True
return handle_func return handle_func
def _wrapper(self, **kwargs): def _wrapper(self, **kwargs):
if self.callable(**kwargs): if self.callable(**kwargs):
cherrypy.serving.request.handler = None cherrypy.serving.request.handler = None
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
@@ -192,15 +192,15 @@ class HandlerTool(Tool):
class HandlerWrapperTool(Tool): class HandlerWrapperTool(Tool):
"""Tool which wraps request.handler in a provided wrapper function. """Tool which wraps request.handler in a provided wrapper function.
The 'newhandler' arg must be a handler wrapper function that takes a The 'newhandler' arg must be a handler wrapper function that takes a
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
page handler page handler
functions, it must return an iterable for use as cherrypy.response.body. functions, it must return an iterable for use as cherrypy.response.body.
For example, to allow your 'inner' page handlers to return dicts For example, to allow your 'inner' page handlers to return dicts
which then get interpolated into a template:: which then get interpolated into a template::
def interpolator(next_handler, *args, **kwargs): def interpolator(next_handler, *args, **kwargs):
filename = cherrypy.request.config.get('template') filename = cherrypy.request.config.get('template')
cherrypy.response.template = env.get_template(filename) cherrypy.response.template = env.get_template(filename)
@@ -208,13 +208,13 @@ class HandlerWrapperTool(Tool):
return cherrypy.response.template.render(**response_dict) return cherrypy.response.template.render(**response_dict)
cherrypy.tools.jinja = HandlerWrapperTool(interpolator) cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
""" """
def __init__(self, newhandler, point='before_handler', name=None, priority=50): def __init__(self, newhandler, point='before_handler', name=None, priority=50):
self.newhandler = newhandler self.newhandler = newhandler
self._point = point self._point = point
self._name = name self._name = name
self._priority = priority self._priority = priority
def callable(self, debug=False): def callable(self, debug=False):
innerfunc = cherrypy.serving.request.handler innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs): def wrap(*args, **kwargs):
@@ -224,16 +224,16 @@ class HandlerWrapperTool(Tool):
class ErrorTool(Tool): class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response.""" """Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None): def __init__(self, callable, name=None):
Tool.__init__(self, None, callable, name) Tool.__init__(self, None, callable, name)
def _wrapper(self): def _wrapper(self):
self.callable(**self._merged_args()) self.callable(**self._merged_args())
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
@@ -250,44 +250,44 @@ from cherrypy.lib import auth_basic, auth_digest
class SessionTool(Tool): class SessionTool(Tool):
"""Session Tool for CherryPy. """Session Tool for CherryPy.
sessions.locking sessions.locking
When 'implicit' (the default), the session will be locked for you, When 'implicit' (the default), the session will be locked for you,
just before running the page handler. just before running the page handler.
When 'early', the session will be locked before reading the request When 'early', the session will be locked before reading the request
body. This is off by default for safety reasons; for example, body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX a large upload would block the session, denying an AJAX
progress meter (see http://www.cherrypy.org/ticket/630). progress meter (see http://www.cherrypy.org/ticket/630).
When 'explicit' (or any other value), you need to call When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using cherrypy.session.acquire_lock() yourself before using
session data. session data.
""" """
def __init__(self): def __init__(self):
# _sessions.init must be bound after headers are read # _sessions.init must be bound after headers are read
Tool.__init__(self, 'before_request_body', _sessions.init) Tool.__init__(self, 'before_request_body', _sessions.init)
def _lock_session(self): def _lock_session(self):
cherrypy.serving.session.acquire_lock() cherrypy.serving.session.acquire_lock()
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
hooks = cherrypy.serving.request.hooks hooks = cherrypy.serving.request.hooks
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop("priority", None)
if p is None: if p is None:
p = getattr(self.callable, "priority", self._priority) p = getattr(self.callable, "priority", self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf) hooks.attach(self._point, self.callable, priority=p, **conf)
locking = conf.pop('locking', 'implicit') locking = conf.pop('locking', 'implicit')
if locking == 'implicit': if locking == 'implicit':
hooks.attach('before_handler', self._lock_session) hooks.attach('before_handler', self._lock_session)
@@ -298,15 +298,15 @@ class SessionTool(Tool):
else: else:
# Don't lock # Don't lock
pass pass
hooks.attach('before_finalize', _sessions.save) hooks.attach('before_finalize', _sessions.save)
hooks.attach('on_end_request', _sessions.close) hooks.attach('on_end_request', _sessions.close)
def regenerate(self): def regenerate(self):
"""Drop the current session and make a new one (with a new id).""" """Drop the current session and make a new one (with a new id)."""
sess = cherrypy.serving.session sess = cherrypy.serving.session
sess.regenerate() sess.regenerate()
# Grab cookie-relevant tool args # Grab cookie-relevant tool args
conf = dict([(k, v) for k, v in self._merged_args().items() conf = dict([(k, v) for k, v in self._merged_args().items()
if k in ('path', 'path_header', 'name', 'timeout', if k in ('path', 'path_header', 'name', 'timeout',
@@ -318,15 +318,15 @@ class SessionTool(Tool):
class XMLRPCController(object): class XMLRPCController(object):
"""A Controller (page handler collection) for XML-RPC. """A Controller (page handler collection) for XML-RPC.
To use it, have your controllers subclass this base class (it will To use it, have your controllers subclass this base class (it will
turn on the tool for you). turn on the tool for you).
You can also supply the following optional config entries:: You can also supply the following optional config entries::
tools.xmlrpc.encoding: 'utf-8' tools.xmlrpc.encoding: 'utf-8'
tools.xmlrpc.allow_none: 0 tools.xmlrpc.allow_none: 0
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
appropriate handler must first be performed according to the URL, and appropriate handler must first be performed according to the URL, and
then a second dispatch step must take place according to the RPC method then a second dispatch step must take place according to the RPC method
@@ -334,42 +334,42 @@ class XMLRPCController(object):
prefix in the URL, supplies its own handler args in the body, and prefix in the URL, supplies its own handler args in the body, and
requires a 200 OK "Fault" response instead of 404 when the desired requires a 200 OK "Fault" response instead of 404 when the desired
method is not found. method is not found.
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
This Controller acts as the dispatch target for the first half (based This Controller acts as the dispatch target for the first half (based
on the URL); it then reads the RPC method from the request body and on the URL); it then reads the RPC method from the request body and
does its own second dispatch step based on that method. It also reads does its own second dispatch step based on that method. It also reads
body params, and returns a Fault on error. body params, and returns a Fault on error.
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
in your URL's, you can safely skip turning on the XMLRPCDispatcher. in your URL's, you can safely skip turning on the XMLRPCDispatcher.
Otherwise, you need to use declare it in config:: Otherwise, you need to use declare it in config::
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
""" """
# Note we're hard-coding this into the 'tools' namespace. We could do # Note we're hard-coding this into the 'tools' namespace. We could do
# a huge amount of work to make it relocatable, but the only reason why # a huge amount of work to make it relocatable, but the only reason why
# would be if someone actually disabled the default_toolbox. Meh. # would be if someone actually disabled the default_toolbox. Meh.
_cp_config = {'tools.xmlrpc.on': True} _cp_config = {'tools.xmlrpc.on': True}
def default(self, *vpath, **params): def default(self, *vpath, **params):
rpcparams, rpcmethod = _xmlrpc.process_body() rpcparams, rpcmethod = _xmlrpc.process_body()
subhandler = self subhandler = self
for attr in str(rpcmethod).split('.'): for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None) subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False): if subhandler and getattr(subhandler, "exposed", False):
body = subhandler(*(vpath + rpcparams), **params) body = subhandler(*(vpath + rpcparams), **params)
else: else:
# http://www.cherrypy.org/ticket/533 # http://www.cherrypy.org/ticket/533
# if a method is not found, an xmlrpclib.Fault should be returned # if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see # raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error # cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr) raise Exception('method "%s" is not supported' % attr)
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {}) conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
_xmlrpc.respond(body, _xmlrpc.respond(body,
conf.get('encoding', 'utf-8'), conf.get('encoding', 'utf-8'),
@@ -379,7 +379,7 @@ class XMLRPCController(object):
class SessionAuthTool(HandlerTool): class SessionAuthTool(HandlerTool):
def _setargs(self): def _setargs(self):
for name in dir(cptools.SessionAuth): for name in dir(cptools.SessionAuth):
if not name.startswith("__"): if not name.startswith("__"):
@@ -388,7 +388,7 @@ class SessionAuthTool(HandlerTool):
class CachingTool(Tool): class CachingTool(Tool):
"""Caching Tool for CherryPy.""" """Caching Tool for CherryPy."""
def _wrapper(self, **kwargs): def _wrapper(self, **kwargs):
request = cherrypy.serving.request request = cherrypy.serving.request
if _caching.get(**kwargs): if _caching.get(**kwargs):
@@ -399,11 +399,11 @@ class CachingTool(Tool):
request.hooks.attach('before_finalize', _caching.tee_output, request.hooks.attach('before_finalize', _caching.tee_output,
priority = 90) priority = 90)
_wrapper.priority = 20 _wrapper.priority = 20
def _setup(self): def _setup(self):
"""Hook caching into cherrypy.request.""" """Hook caching into cherrypy.request."""
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop("priority", None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf) priority=p, **conf)
@@ -412,14 +412,14 @@ class CachingTool(Tool):
class Toolbox(object): class Toolbox(object):
"""A collection of Tools. """A collection of Tools.
This object also functions as a config namespace handler for itself. This object also functions as a config namespace handler for itself.
Custom toolboxes should be added to each Application's toolboxes dict. Custom toolboxes should be added to each Application's toolboxes dict.
""" """
def __init__(self, namespace): def __init__(self, namespace):
self.namespace = namespace self.namespace = namespace
def __setattr__(self, name, value): def __setattr__(self, name, value):
# If the Tool._name is None, supply it from the attribute name. # If the Tool._name is None, supply it from the attribute name.
if isinstance(value, Tool): if isinstance(value, Tool):
@@ -427,7 +427,7 @@ class Toolbox(object):
value._name = name value._name = name
value.namespace = self.namespace value.namespace = self.namespace
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
def __enter__(self): def __enter__(self):
"""Populate request.toolmaps from tools specified in config.""" """Populate request.toolmaps from tools specified in config."""
cherrypy.serving.request.toolmaps[self.namespace] = map = {} cherrypy.serving.request.toolmaps[self.namespace] = map = {}
@@ -436,7 +436,7 @@ class Toolbox(object):
bucket = map.setdefault(toolname, {}) bucket = map.setdefault(toolname, {})
bucket[arg] = v bucket[arg] = v
return populate return populate
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
"""Run tool._setup() for each tool in our toolmap.""" """Run tool._setup() for each tool in our toolmap."""
map = cherrypy.serving.request.toolmaps.get(self.namespace) map = cherrypy.serving.request.toolmaps.get(self.namespace)
@@ -448,21 +448,21 @@ class Toolbox(object):
class DeprecatedTool(Tool): class DeprecatedTool(Tool):
_name = None _name = None
warnmsg = "This Tool is deprecated." warnmsg = "This Tool is deprecated."
def __init__(self, point, warnmsg=None): def __init__(self, point, warnmsg=None):
self.point = point self.point = point
if warnmsg is not None: if warnmsg is not None:
self.warnmsg = warnmsg self.warnmsg = warnmsg
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
warnings.warn(self.warnmsg) warnings.warn(self.warnmsg)
def tool_decorator(f): def tool_decorator(f):
return f return f
return tool_decorator return tool_decorator
def _setup(self): def _setup(self):
warnings.warn(self.warnmsg) warnings.warn(self.warnmsg)
+55 -55
View File
@@ -11,69 +11,69 @@ from cherrypy.lib import httputil
class Application(object): class Application(object):
"""A CherryPy Application. """A CherryPy Application.
Servers and gateways should not instantiate Request objects directly. Servers and gateways should not instantiate Request objects directly.
Instead, they should ask an Application object for a request object. Instead, they should ask an Application object for a request object.
An instance of this class may also be used as a WSGI callable An instance of this class may also be used as a WSGI callable
(WSGI application object) for itself. (WSGI application object) for itself.
""" """
root = None root = None
"""The top-most container of page handlers for this app. Handlers should """The top-most container of page handlers for this app. Handlers should
be arranged in a hierarchy of attributes, matching the expected URI be arranged in a hierarchy of attributes, matching the expected URI
hierarchy; the default dispatcher then searches this hierarchy for a hierarchy; the default dispatcher then searches this hierarchy for a
matching handler. When using a dispatcher other than the default, matching handler. When using a dispatcher other than the default,
this value may be None.""" this value may be None."""
config = {} config = {}
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
of {key: value} pairs.""" of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet() namespaces = _cpconfig.NamespaceSet()
toolboxes = {'tools': cherrypy.tools} toolboxes = {'tools': cherrypy.tools}
log = None log = None
"""A LogManager instance. See _cplogging.""" """A LogManager instance. See _cplogging."""
wsgiapp = None wsgiapp = None
"""A CPWSGIApp instance. See _cpwsgi.""" """A CPWSGIApp instance. See _cpwsgi."""
request_class = _cprequest.Request request_class = _cprequest.Request
response_class = _cprequest.Response response_class = _cprequest.Response
relative_urls = False relative_urls = False
def __init__(self, root, script_name="", config=None): def __init__(self, root, script_name="", config=None):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root self.root = root
self.script_name = script_name self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self) self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy() self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy() self.config = self.__class__.config.copy()
if config: if config:
self.merge(config) self.merge(config)
def __repr__(self): def __repr__(self):
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
self.root, self.script_name) self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point is that portion of script_name_doc = """The URI "mount point" for this app. A mount point is that portion of
the URI which is constant for all URIs that are serviced by this the URI which is constant for all URIs that are serviced by this
application; it does not include scheme, host, or proxy ("virtual host") application; it does not include scheme, host, or proxy ("virtual host")
portions of the URI. portions of the URI.
For example, if script_name is "/my/cool/app", then the URL For example, if script_name is "/my/cool/app", then the URL
"http://www.example.com/my/cool/app/page1" might be handled by a "http://www.example.com/my/cool/app/page1" might be handled by a
"page1" method on the root object. "page1" method on the root object.
The value of script_name MUST NOT end in a slash. If the script_name The value of script_name MUST NOT end in a slash. If the script_name
refers to the root of the URI, it MUST be an empty string (not "/"). refers to the root of the URI, it MUST be an empty string (not "/").
If script_name is explicitly set to None, then the script_name will be If script_name is explicitly set to None, then the script_name will be
provided for each call from request.wsgi_environ['SCRIPT_NAME']. provided for each call from request.wsgi_environ['SCRIPT_NAME'].
""" """
@@ -88,23 +88,23 @@ class Application(object):
self._script_name = value self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name, script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc) doc=script_name_doc)
def merge(self, config): def merge(self, config):
"""Merge the given config into self.config.""" """Merge the given config into self.config."""
_cpconfig.merge(self.config, config) _cpconfig.merge(self.config, config)
# Handle namespaces specified in config. # Handle namespaces specified in config.
self.namespaces(self.config.get("/", {})) self.namespaces(self.config.get("/", {}))
def find_config(self, path, key, default=None): def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default.""" """Return the most-specific value for key along path, or default."""
trail = path or "/" trail = path or "/"
while trail: while trail:
nodeconf = self.config.get(trail, {}) nodeconf = self.config.get(trail, {})
if key in nodeconf: if key in nodeconf:
return nodeconf[key] return nodeconf[key]
lastslash = trail.rfind("/") lastslash = trail.rfind("/")
if lastslash == -1: if lastslash == -1:
break break
@@ -112,78 +112,78 @@ class Application(object):
trail = "/" trail = "/"
else: else:
trail = trail[:lastslash] trail = trail[:lastslash]
return default return default
def get_serving(self, local, remote, scheme, sproto): def get_serving(self, local, remote, scheme, sproto):
"""Create and return a Request and Response object.""" """Create and return a Request and Response object."""
req = self.request_class(local, remote, scheme, sproto) req = self.request_class(local, remote, scheme, sproto)
req.app = self req.app = self
for name, toolbox in self.toolboxes.items(): for name, toolbox in self.toolboxes.items():
req.namespaces[name] = toolbox req.namespaces[name] = toolbox
resp = self.response_class() resp = self.response_class()
cherrypy.serving.load(req, resp) cherrypy.serving.load(req, resp)
cherrypy.engine.publish('acquire_thread') cherrypy.engine.publish('acquire_thread')
cherrypy.engine.publish('before_request') cherrypy.engine.publish('before_request')
return req, resp return req, resp
def release_serving(self): def release_serving(self):
"""Release the current serving (request and response).""" """Release the current serving (request and response)."""
req = cherrypy.serving.request req = cherrypy.serving.request
cherrypy.engine.publish('after_request') cherrypy.engine.publish('after_request')
try: try:
req.close() req.close()
except: except:
cherrypy.log(traceback=True, severity=40) cherrypy.log(traceback=True, severity=40)
cherrypy.serving.clear() cherrypy.serving.clear()
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
return self.wsgiapp(environ, start_response) return self.wsgiapp(environ, start_response)
class Tree(object): class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points. """A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable An instance of this class may also be used as a WSGI callable
(WSGI application object), in which case it dispatches to all (WSGI application object), in which case it dispatches to all
mounted apps. mounted apps.
""" """
apps = {} apps = {}
""" """
A dict of the form {script name: application}, where "script name" A dict of the form {script name: application}, where "script name"
is a string declaring the URI mount point (no trailing slash), and is a string declaring the URI mount point (no trailing slash), and
"application" is an instance of cherrypy.Application (or an arbitrary "application" is an instance of cherrypy.Application (or an arbitrary
WSGI callable if you happen to be using a WSGI server).""" WSGI callable if you happen to be using a WSGI server)."""
def __init__(self): def __init__(self):
self.apps = {} self.apps = {}
def mount(self, root, script_name="", config=None): def mount(self, root, script_name="", config=None):
"""Mount a new app from a root object, script_name, and config. """Mount a new app from a root object, script_name, and config.
root root
An instance of a "controller class" (a collection of page An instance of a "controller class" (a collection of page
handler methods) which represents the root of the application. handler methods) which represents the root of the application.
This may also be an Application instance, or None if using This may also be an Application instance, or None if using
a dispatcher other than the default. a dispatcher other than the default.
script_name script_name
A string containing the "mount point" of the application. A string containing the "mount point" of the application.
This should start with a slash, and be the path portion of the This should start with a slash, and be the path portion of the
URL at which to mount the given root. For example, if root.index() URL at which to mount the given root. For example, if root.index()
will handle requests to "http://www.example.com:8080/dept/app1/", will handle requests to "http://www.example.com:8080/dept/app1/",
then the script_name argument would be "/dept/app1". then the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the It MUST NOT end in a slash. If the script_name refers to the
root of the URI, it MUST be an empty string (not "/"). root of the URI, it MUST be an empty string (not "/").
config config
A file or dict containing application config. A file or dict containing application config.
""" """
@@ -194,10 +194,10 @@ class Tree(object):
"order to inpect the WSGI environ for SCRIPT_NAME upon each " "order to inpect the WSGI environ for SCRIPT_NAME upon each "
"request). You cannot mount such Applications on this Tree; " "request). You cannot mount such Applications on this Tree; "
"you must pass them to a WSGI server interface directly.") "you must pass them to a WSGI server interface directly.")
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/") script_name = script_name.rstrip("/")
if isinstance(root, Application): if isinstance(root, Application):
app = root app = root
if script_name != "" and script_name != app.script_name: if script_name != "" and script_name != app.script_name:
@@ -206,30 +206,30 @@ class Tree(object):
script_name = app.script_name script_name = app.script_name
else: else:
app = Application(root, script_name) app = Application(root, script_name)
# If mounted at "", add favicon.ico # If mounted at "", add favicon.ico
if (script_name == "" and root is not None if (script_name == "" and root is not None
and not hasattr(root, "favicon_ico")): and not hasattr(root, "favicon_ico")):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
"favicon.ico") "favicon.ico")
root.favicon_ico = tools.staticfile.handler(favicon) root.favicon_ico = tools.staticfile.handler(favicon)
if config: if config:
app.merge(config) app.merge(config)
self.apps[script_name] = app self.apps[script_name] = app
return app return app
def graft(self, wsgi_callable, script_name=""): def graft(self, wsgi_callable, script_name=""):
"""Mount a wsgi callable at the given script_name.""" """Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/") script_name = script_name.rstrip("/")
self.apps[script_name] = wsgi_callable self.apps[script_name] = wsgi_callable
def script_name(self, path=None): def script_name(self, path=None):
"""The script_name of the app at the given path, or None. """The script_name of the app at the given path, or None.
If path is None, cherrypy.request is used. If path is None, cherrypy.request is used.
""" """
if path is None: if path is None:
@@ -239,17 +239,17 @@ class Tree(object):
request.path_info) request.path_info)
except AttributeError: except AttributeError:
return None return None
while True: while True:
if path in self.apps: if path in self.apps:
return path return path
if path == "": if path == "":
return None return None
# Move one node up the tree and try again. # Move one node up the tree and try again.
path = path[:path.rfind("/")] path = path[:path.rfind("/")]
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME # If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to ''). # to '' (some WSGI servers always set SCRIPT_NAME to '').
@@ -263,9 +263,9 @@ class Tree(object):
if sn is None: if sn is None:
start_response('404 Not Found', []) start_response('404 Not Found', [])
return [] return []
app = self.apps[sn] app = self.apps[sn]
# Correct the SCRIPT_NAME and PATH_INFO environ entries. # Correct the SCRIPT_NAME and PATH_INFO environ entries.
environ = environ.copy() environ = environ.copy()
if not py3k: if not py3k:
+49 -49
View File
@@ -18,7 +18,7 @@ from cherrypy.lib import httputil
def downgrade_wsgi_ux_to_1x(environ): def downgrade_wsgi_ux_to_1x(environ):
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.""" """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
env1x = {} env1x = {}
url_encoding = environ[ntou('wsgi.url_encoding')] url_encoding = environ[ntou('wsgi.url_encoding')]
for k, v in list(environ.items()): for k, v in list(environ.items()):
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
@@ -26,36 +26,36 @@ def downgrade_wsgi_ux_to_1x(environ):
elif isinstance(v, unicodestr): elif isinstance(v, unicodestr):
v = v.encode('ISO-8859-1') v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v env1x[k.encode('ISO-8859-1')] = v
return env1x return env1x
class VirtualHost(object): class VirtualHost(object):
"""Select a different WSGI application based on the Host header. """Select a different WSGI application based on the Host header.
This can be useful when running multiple sites within one CP server. This can be useful when running multiple sites within one CP server.
It allows several domains to point to different applications. For example:: It allows several domains to point to different applications. For example::
root = Root() root = Root()
RootApp = cherrypy.Application(root) RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root) Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure()) SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp, vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App, domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp, 'www.domain2.example:443': SecureApp,
}) })
cherrypy.tree.graft(vhost) cherrypy.tree.graft(vhost)
""" """
default = None default = None
"""Required. The default WSGI application.""" """Required. The default WSGI application."""
use_x_forwarded_host = True use_x_forwarded_host = True
"""If True (the default), any "X-Forwarded-Host" """If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying.""" is commonly added by HTTP servers (such as Apache) when proxying."""
domains = {} domains = {}
"""A dict of {host header value: application} pairs. """A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict, The incoming "Host" request header is looked up in this dict,
@@ -64,17 +64,17 @@ class VirtualHost(object):
separate entries for "example.com" and "www.example.com". separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number. In addition, "Host" headers may contain the port number.
""" """
def __init__(self, default, domains=None, use_x_forwarded_host=True): def __init__(self, default, domains=None, use_x_forwarded_host=True):
self.default = default self.default = default
self.domains = domains or {} self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '') domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host: if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain) domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
nextapp = self.domains.get(domain) nextapp = self.domains.get(domain)
if nextapp is None: if nextapp is None:
nextapp = self.default nextapp = self.default
@@ -83,11 +83,11 @@ class VirtualHost(object):
class InternalRedirector(object): class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect.""" """WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False): def __init__(self, nextapp, recursive=False):
self.nextapp = nextapp self.nextapp = nextapp
self.recursive = recursive self.recursive = recursive
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
redirections = [] redirections = []
while True: while True:
@@ -99,13 +99,13 @@ class InternalRedirector(object):
sn = environ.get('SCRIPT_NAME', '') sn = environ.get('SCRIPT_NAME', '')
path = environ.get('PATH_INFO', '') path = environ.get('PATH_INFO', '')
qs = environ.get('QUERY_STRING', '') qs = environ.get('QUERY_STRING', '')
# Add the *previous* path_info + qs to redirections. # Add the *previous* path_info + qs to redirections.
old_uri = sn + path old_uri = sn + path
if qs: if qs:
old_uri += "?" + qs old_uri += "?" + qs
redirections.append(old_uri) redirections.append(old_uri)
if not self.recursive: if not self.recursive:
# Check to see if the new URI has been redirected to already # Check to see if the new URI has been redirected to already
new_uri = sn + ir.path new_uri = sn + ir.path
@@ -115,7 +115,7 @@ class InternalRedirector(object):
ir.request.close() ir.request.close()
raise RuntimeError("InternalRedirector visited the " raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % new_uri) "same URL twice: %r" % new_uri)
# Munge the environment and try again. # Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET" environ['REQUEST_METHOD'] = "GET"
environ['PATH_INFO'] = ir.path environ['PATH_INFO'] = ir.path
@@ -127,19 +127,19 @@ class InternalRedirector(object):
class ExceptionTrapper(object): class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions.""" """WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
self.nextapp = nextapp self.nextapp = nextapp
self.throws = throws self.throws = throws
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
return _TrappedResponse(self.nextapp, environ, start_response, self.throws) return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
class _TrappedResponse(object): class _TrappedResponse(object):
response = iter([]) response = iter([])
def __init__(self, nextapp, environ, start_response, throws): def __init__(self, nextapp, environ, start_response, throws):
self.nextapp = nextapp self.nextapp = nextapp
self.environ = environ self.environ = environ
@@ -148,22 +148,22 @@ class _TrappedResponse(object):
self.started_response = False self.started_response = False
self.response = self.trap(self.nextapp, self.environ, self.start_response) self.response = self.trap(self.nextapp, self.environ, self.start_response)
self.iter_response = iter(self.response) self.iter_response = iter(self.response)
def __iter__(self): def __iter__(self):
self.started_response = True self.started_response = True
return self return self
if py3k: if py3k:
def __next__(self): def __next__(self):
return self.trap(next, self.iter_response) return self.trap(next, self.iter_response)
else: else:
def next(self): def next(self):
return self.trap(self.iter_response.next) return self.trap(self.iter_response.next)
def close(self): def close(self):
if hasattr(self.response, 'close'): if hasattr(self.response, 'close'):
self.response.close() self.response.close()
def trap(self, func, *args, **kwargs): def trap(self, func, *args, **kwargs):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
@@ -188,7 +188,7 @@ class _TrappedResponse(object):
self.iter_response = iter([]) self.iter_response = iter([])
else: else:
self.iter_response = iter(b) self.iter_response = iter(b)
try: try:
self.start_response(s, h, _sys.exc_info()) self.start_response(s, h, _sys.exc_info())
except: except:
@@ -199,7 +199,7 @@ class _TrappedResponse(object):
# But we still log and call close() to clean up ourselves. # But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40) _cherrypy.log(traceback=True, severity=40)
raise raise
if self.started_response: if self.started_response:
return ntob("").join(b) return ntob("").join(b)
else: else:
@@ -211,7 +211,7 @@ class _TrappedResponse(object):
class AppResponse(object): class AppResponse(object):
"""WSGI response iterable for CherryPy applications.""" """WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp): def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp self.cpapp = cpapp
try: try:
@@ -226,7 +226,7 @@ class AppResponse(object):
outstatus = r.output_status outstatus = r.output_status
if not isinstance(outstatus, bytestr): if not isinstance(outstatus, bytestr):
raise TypeError("response.output_status is not a byte string.") raise TypeError("response.output_status is not a byte string.")
outheaders = [] outheaders = []
for k, v in r.header_list: for k, v in r.header_list:
if not isinstance(k, bytestr): if not isinstance(k, bytestr):
@@ -234,7 +234,7 @@ class AppResponse(object):
if not isinstance(v, bytestr): if not isinstance(v, bytestr):
raise TypeError("response.header_list value %r is not a byte string." % v) raise TypeError("response.header_list value %r is not a byte string." % v)
outheaders.append((k, v)) outheaders.append((k, v))
if py3k: if py3k:
# According to PEP 3333, when using Python 3, the response status # According to PEP 3333, when using Python 3, the response status
# and headers must be bytes masquerading as unicode; that is, they # and headers must be bytes masquerading as unicode; that is, they
@@ -249,25 +249,25 @@ class AppResponse(object):
except: except:
self.close() self.close()
raise raise
def __iter__(self): def __iter__(self):
return self return self
if py3k: if py3k:
def __next__(self): def __next__(self):
return next(self.iter_response) return next(self.iter_response)
else: else:
def next(self): def next(self):
return self.iter_response.next() return self.iter_response.next()
def close(self): def close(self):
"""Close and de-reference the current request and response. (Core)""" """Close and de-reference the current request and response. (Core)"""
self.cpapp.release_serving() self.cpapp.release_serving()
def run(self): def run(self):
"""Create a Request object using environ.""" """Create a Request object using environ."""
env = self.environ.get env = self.environ.get
local = httputil.Host('', int(env('SERVER_PORT', 80)), local = httputil.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', '')) env('SERVER_NAME', ''))
remote = httputil.Host(env('REMOTE_ADDR', ''), remote = httputil.Host(env('REMOTE_ADDR', ''),
@@ -276,7 +276,7 @@ class AppResponse(object):
scheme = env('wsgi.url_scheme') scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the # LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account. # user after having been mapped to a local account.
# Both IIS and Apache set REMOTE_USER, when possible. # Both IIS and Apache set REMOTE_USER, when possible.
@@ -285,9 +285,9 @@ class AppResponse(object):
request.multiprocess = self.environ['wsgi.multiprocess'] request.multiprocess = self.environ['wsgi.multiprocess']
request.wsgi_environ = self.environ request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None) request.prev = env('cherrypy.previous_request', None)
meth = self.environ['REQUEST_METHOD'] meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', '')) self.environ.get('PATH_INFO', ''))
qs = self.environ.get('QUERY_STRING', '') qs = self.environ.get('QUERY_STRING', '')
@@ -313,19 +313,19 @@ class AppResponse(object):
# Only set transcoded values if they both succeed. # Only set transcoded values if they both succeed.
path = u_path path = u_path
qs = u_qs qs = u_qs
rproto = self.environ.get('SERVER_PROTOCOL') rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ) headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input'] rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile) request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length', 'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type', 'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host', 'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr', 'REMOTE_ADDR': 'Remote-Addr',
} }
def translate_headers(self, environ): def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names.""" """Translate CGI-environ header names to HTTP header names."""
for cgiName in environ: for cgiName in environ:
@@ -340,7 +340,7 @@ class AppResponse(object):
class CPWSGIApp(object): class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application.""" """A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper), pipeline = [('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector), ('InternalRedirector', InternalRedirector),
] ]
@@ -349,35 +349,35 @@ class CPWSGIApp(object):
plus optional keyword arguments, and returns a WSGI application plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can (that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config.""" be any you choose, and will correspond to keys in self.config."""
head = None head = None
"""Rather than nest all apps in the pipeline on each call, it's only """Rather than nest all apps in the pipeline on each call, it's only
done the first time, and the result is memoized into self.head. Set done the first time, and the result is memoized into self.head. Set
this to None again if you change self.pipeline after calling self.""" this to None again if you change self.pipeline after calling self."""
config = {} config = {}
"""A dict whose keys match names listed in the pipeline. Each """A dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments.""" named WSGI callable (from the pipeline) as keyword arguments."""
response_class = AppResponse response_class = AppResponse
"""The class to instantiate and return as the next app in the WSGI chain.""" """The class to instantiate and return as the next app in the WSGI chain."""
def __init__(self, cpapp, pipeline=None): def __init__(self, cpapp, pipeline=None):
self.cpapp = cpapp self.cpapp = cpapp
self.pipeline = self.pipeline[:] self.pipeline = self.pipeline[:]
if pipeline: if pipeline:
self.pipeline.extend(pipeline) self.pipeline.extend(pipeline)
self.config = self.config.copy() self.config = self.config.copy()
def tail(self, environ, start_response): def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application. """WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead, You probably shouldn't call this; call self.__call__ instead,
so that any WSGI middleware in self.pipeline can run first. so that any WSGI middleware in self.pipeline can run first.
""" """
return self.response_class(environ, start_response, self.cpapp) return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
head = self.head head = self.head
if head is None: if head is None:
@@ -389,7 +389,7 @@ class CPWSGIApp(object):
head = callable(head, **conf) head = callable(head, **conf)
self.head = head self.head = head
return head(environ, start_response) return head(environ, start_response)
def namespace_handler(self, k, v): def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace.""" """Config handler for the 'wsgi' namespace."""
if k == "pipeline": if k == "pipeline":
+5 -5
View File
@@ -9,22 +9,22 @@ from cherrypy import wsgiserver
class CPWSGIServer(wsgiserver.CherryPyWSGIServer): class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
"""Wrapper for wsgiserver.CherryPyWSGIServer. """Wrapper for wsgiserver.CherryPyWSGIServer.
wsgiserver has been designed to not reference CherryPy in any way, wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. Therefore, so that it can be used in other frameworks and applications. Therefore,
we wrap it here, so we can set our own mount points from cherrypy.tree we wrap it here, so we can set our own mount points from cherrypy.tree
and apply some attributes from config -> cherrypy.server -> wsgiserver. and apply some attributes from config -> cherrypy.server -> wsgiserver.
""" """
def __init__(self, server_adapter=cherrypy.server): def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter self.server_adapter = server_adapter
self.max_request_header_size = self.server_adapter.max_request_header_size or 0 self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0 self.max_request_body_size = self.server_adapter.max_request_body_size or 0
server_name = (self.server_adapter.socket_host or server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or self.server_adapter.socket_file or
None) None)
self.wsgi_version = self.server_adapter.wsgi_version self.wsgi_version = self.server_adapter.wsgi_version
s = wsgiserver.CherryPyWSGIServer s = wsgiserver.CherryPyWSGIServer
s.__init__(self, server_adapter.bind_addr, cherrypy.tree, s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
@@ -55,7 +55,7 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
self.server_adapter.ssl_certificate, self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key, self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain) self.server_adapter.ssl_certificate_chain)
self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False) self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False)
def error_log(self, msg="", level=20, traceback=False): def error_log(self, msg="", level=20, traceback=False):
+3 -3
View File
@@ -5,14 +5,14 @@ from cherrypy.lib.reprconf import unrepr, modules, attributes
class file_generator(object): class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)""" """Yield the given input (a file object) in chunks (default 64k). (Core)"""
def __init__(self, input, chunkSize=65536): def __init__(self, input, chunkSize=65536):
self.input = input self.input = input
self.chunkSize = chunkSize self.chunkSize = chunkSize
def __iter__(self): def __iter__(self):
return self return self
def __next__(self): def __next__(self):
chunk = self.input.read(self.chunkSize) chunk = self.input.read(self.chunkSize)
if chunk: if chunk:
+16 -16
View File
@@ -10,18 +10,18 @@ def check_auth(users, encrypt=None, realm=None):
ah = httpauth.parseAuthorization(request.headers['authorization']) ah = httpauth.parseAuthorization(request.headers['authorization'])
if ah is None: if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request') raise cherrypy.HTTPError(400, 'Bad Request')
if not encrypt: if not encrypt:
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5] encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if hasattr(users, '__call__'): if hasattr(users, '__call__'):
try: try:
# backward compatibility # backward compatibility
users = users() # expect it to return a dictionary users = users() # expect it to return a dictionary
if not isinstance(users, dict): if not isinstance(users, dict):
raise ValueError("Authentication users must be a dictionary") raise ValueError("Authentication users must be a dictionary")
# fetch the user password # fetch the user password
password = users.get(ah["username"], None) password = users.get(ah["username"], None)
except TypeError: except TypeError:
@@ -30,47 +30,47 @@ def check_auth(users, encrypt=None, realm=None):
else: else:
if not isinstance(users, dict): if not isinstance(users, dict):
raise ValueError("Authentication users must be a dictionary") raise ValueError("Authentication users must be a dictionary")
# fetch the user password # fetch the user password
password = users.get(ah["username"], None) password = users.get(ah["username"], None)
# validate the authorization by re-computing it here # validate the authorization by re-computing it here
# and compare it with what the user-agent provided # and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=request.method, if httpauth.checkResponse(ah, password, method=request.method,
encrypt=encrypt, realm=realm): encrypt=encrypt, realm=realm):
request.login = ah["username"] request.login = ah["username"]
return True return True
request.login = False request.login = False
return False return False
def basic_auth(realm, users, encrypt=None, debug=False): def basic_auth(realm, users, encrypt=None, debug=False):
"""If auth fails, raise 401 with a basic authentication header. """If auth fails, raise 401 with a basic authentication header.
realm realm
A string containing the authentication realm. A string containing the authentication realm.
users users
A dict of the form: {username: password} or a callable returning a dict. A dict of the form: {username: password} or a callable returning a dict.
encrypt encrypt
callable used to encrypt the password returned from the user-agent. callable used to encrypt the password returned from the user-agent.
if None it defaults to a md5 encryption. if None it defaults to a md5 encryption.
""" """
if check_auth(users, encrypt): if check_auth(users, encrypt):
if debug: if debug:
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH') cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
return return
# inform the user-agent this path is protected # inform the user-agent this path is protected
cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm) cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource") raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
def digest_auth(realm, users, debug=False): def digest_auth(realm, users, debug=False):
"""If auth fails, raise 401 with a digest authentication header. """If auth fails, raise 401 with a digest authentication header.
realm realm
A string containing the authentication realm. A string containing the authentication realm.
users users
@@ -80,8 +80,8 @@ def digest_auth(realm, users, debug=False):
if debug: if debug:
cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH') cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
return return
# inform the user-agent this path is protected # inform the user-agent this path is protected
cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm) cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource") raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
+4 -4
View File
@@ -60,13 +60,13 @@ def basic_auth(realm, checkpassword, debug=False):
username and password are the values obtained from the request's username and password are the values obtained from the request's
'authorization' header. If authentication succeeds, checkpassword 'authorization' header. If authentication succeeds, checkpassword
returns True, else it returns False. returns True, else it returns False.
""" """
if '"' in realm: if '"' in realm:
raise ValueError('Realm cannot contain the " (quote) character.') raise ValueError('Realm cannot contain the " (quote) character.')
request = cherrypy.serving.request request = cherrypy.serving.request
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
if auth_header is not None: if auth_header is not None:
try: try:
@@ -80,7 +80,7 @@ def basic_auth(realm, checkpassword, debug=False):
return # successful authentication return # successful authentication
except (ValueError, binascii.Error): # split() error, base64.decodestring() error except (ValueError, binascii.Error): # split() error, base64.decodestring() error
raise cherrypy.HTTPError(400, 'Bad Request') raise cherrypy.HTTPError(400, 'Bad Request')
# Respond with 401 status and a WWW-Authenticate header # Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(401, "You are not authorized to access that resource") raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
+14 -14
View File
@@ -107,10 +107,10 @@ def synthesize_nonce(s, key, timestamp=None):
key key
A secret string known only to the server. A secret string known only to the server.
timestamp timestamp
An integer seconds-since-the-epoch timestamp An integer seconds-since-the-epoch timestamp
""" """
if timestamp is None: if timestamp is None:
timestamp = int(time.time()) timestamp = int(time.time())
@@ -190,10 +190,10 @@ class HttpDigestAuthorization (object):
s s
A string related to the resource, such as the hostname of the server. A string related to the resource, such as the hostname of the server.
key key
A secret string known only to the server. A secret string known only to the server.
Both s and key must be the same values which were used to synthesize the nonce Both s and key must be the same values which were used to synthesize the nonce
we are trying to validate. we are trying to validate.
""" """
@@ -256,7 +256,7 @@ class HttpDigestAuthorization (object):
4.3. This refers to the entity the user agent sent in the request which 4.3. This refers to the entity the user agent sent in the request which
has the Authorization header. Typically GET requests don't have an entity, has the Authorization header. Typically GET requests don't have an entity,
and POST requests do. and POST requests do.
""" """
ha2 = self.HA2(entity_body) ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1 # Request-Digest -- RFC 2617 3.2.2.1
@@ -302,16 +302,16 @@ def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stal
def digest_auth(realm, get_ha1, key, debug=False): def digest_auth(realm, get_ha1, key, debug=False):
"""A CherryPy tool which hooks at before_handler to perform """A CherryPy tool which hooks at before_handler to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`. HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme, this If the request has an 'authorization' header with a 'Digest' scheme, this
tool authenticates the credentials supplied in that header. If tool authenticates the credentials supplied in that header. If
the request has no 'authorization' header, or if it does but the scheme is the request has no 'authorization' header, or if it does but the scheme is
not "Digest", or if authentication fails, the tool sends a 401 response with not "Digest", or if authentication fails, the tool sends a 401 response with
a 'WWW-Authenticate' Digest header. a 'WWW-Authenticate' Digest header.
realm realm
A string containing the authentication realm. A string containing the authentication realm.
get_ha1 get_ha1
A callable which looks up a username in a credentials store A callable which looks up a username in a credentials store
and returns the HA1 string, which is defined in the RFC to be and returns the HA1 string, which is defined in the RFC to be
@@ -320,13 +320,13 @@ def digest_auth(realm, get_ha1, key, debug=False):
where username is obtained from the request's 'authorization' header. where username is obtained from the request's 'authorization' header.
If username is not found in the credentials store, get_ha1() returns If username is not found in the credentials store, get_ha1() returns
None. None.
key key
A secret string known only to the server, used in the synthesis of nonces. A secret string known only to the server, used in the synthesis of nonces.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
nonce_is_stale = False nonce_is_stale = False
if auth_header is not None: if auth_header is not None:
@@ -334,10 +334,10 @@ def digest_auth(realm, get_ha1, key, debug=False):
auth = HttpDigestAuthorization(auth_header, request.method, debug=debug) auth = HttpDigestAuthorization(auth_header, request.method, debug=debug)
except ValueError: except ValueError:
raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.") raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
if debug: if debug:
TRACE(str(auth)) TRACE(str(auth))
if auth.validate_nonce(realm, key): if auth.validate_nonce(realm, key):
ha1 = get_ha1(realm, auth.username) ha1 = get_ha1(realm, auth.username)
if ha1 is not None: if ha1 is not None:
@@ -355,7 +355,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
if debug: if debug:
TRACE("authentication of %s successful" % auth.username) TRACE("authentication of %s successful" % auth.username)
return return
# Respond with 401 status and a WWW-Authenticate header # Respond with 401 status and a WWW-Authenticate header
header = www_authenticate(realm, key, stale=nonce_is_stale) header = www_authenticate(realm, key, stale=nonce_is_stale)
if debug: if debug:
+59 -59
View File
@@ -44,19 +44,19 @@ from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
class Cache(object): class Cache(object):
"""Base class for Cache implementations.""" """Base class for Cache implementations."""
def get(self): def get(self):
"""Return the current variant if in the cache, else None.""" """Return the current variant if in the cache, else None."""
raise NotImplemented raise NotImplemented
def put(self, obj, size): def put(self, obj, size):
"""Store the current variant in the cache.""" """Store the current variant in the cache."""
raise NotImplemented raise NotImplemented
def delete(self): def delete(self):
"""Remove ALL cached variants of the current resource.""" """Remove ALL cached variants of the current resource."""
raise NotImplemented raise NotImplemented
def clear(self): def clear(self):
"""Reset the cache to its initial, empty state.""" """Reset the cache to its initial, empty state."""
raise NotImplemented raise NotImplemented
@@ -68,16 +68,16 @@ class Cache(object):
class AntiStampedeCache(dict): class AntiStampedeCache(dict):
"""A storage system for cached items which reduces stampede collisions.""" """A storage system for cached items which reduces stampede collisions."""
def wait(self, key, timeout=5, debug=False): def wait(self, key, timeout=5, debug=False):
"""Return the cached value for the given key, or None. """Return the cached value for the given key, or None.
If timeout is not None, and the value is already If timeout is not None, and the value is already
being calculated by another thread, wait until the given timeout has being calculated by another thread, wait until the given timeout has
elapsed. If the value is available before the timeout expires, it is elapsed. If the value is available before the timeout expires, it is
returned. If not, None is returned, and a sentinel placed in the cache returned. If not, None is returned, and a sentinel placed in the cache
to signal other threads to wait. to signal other threads to wait.
If timeout is None, no waiting is performed nor sentinels used. If timeout is None, no waiting is performed nor sentinels used.
""" """
value = self.get(key) value = self.get(key)
@@ -87,7 +87,7 @@ class AntiStampedeCache(dict):
if debug: if debug:
cherrypy.log('No timeout', 'TOOLS.CACHING') cherrypy.log('No timeout', 'TOOLS.CACHING')
return None return None
# Wait until it's done or times out. # Wait until it's done or times out.
if debug: if debug:
cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING') cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
@@ -104,7 +104,7 @@ class AntiStampedeCache(dict):
e = threading.Event() e = threading.Event()
e.result = None e.result = None
dict.__setitem__(self, key, e) dict.__setitem__(self, key, e)
return None return None
elif value is None: elif value is None:
# Stick an Event in the slot so other threads wait # Stick an Event in the slot so other threads wait
@@ -115,7 +115,7 @@ class AntiStampedeCache(dict):
e.result = None e.result = None
dict.__setitem__(self, key, e) dict.__setitem__(self, key, e)
return value return value
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""Set the cached value for the given key.""" """Set the cached value for the given key."""
existing = self.get(key) existing = self.get(key)
@@ -129,48 +129,48 @@ class AntiStampedeCache(dict):
class MemoryCache(Cache): class MemoryCache(Cache):
"""An in-memory cache for varying response content. """An in-memory cache for varying response content.
Each key in self.store is a URI, and each value is an AntiStampedeCache. Each key in self.store is a URI, and each value is an AntiStampedeCache.
The response for any given URI may vary based on the values of The response for any given URI may vary based on the values of
"selecting request headers"; that is, those named in the Vary "selecting request headers"; that is, those named in the Vary
response header. We assume the list of header names to be constant response header. We assume the list of header names to be constant
for each URI throughout the lifetime of the application, and store for each URI throughout the lifetime of the application, and store
that list in ``self.store[uri].selecting_headers``. that list in ``self.store[uri].selecting_headers``.
The items contained in ``self.store[uri]`` have keys which are tuples of The items contained in ``self.store[uri]`` have keys which are tuples of
request header values (in the same order as the names in its request header values (in the same order as the names in its
selecting_headers), and values which are the actual responses. selecting_headers), and values which are the actual responses.
""" """
maxobjects = 1000 maxobjects = 1000
"""The maximum number of cached objects; defaults to 1000.""" """The maximum number of cached objects; defaults to 1000."""
maxobj_size = 100000 maxobj_size = 100000
"""The maximum size of each cached object in bytes; defaults to 100 KB.""" """The maximum size of each cached object in bytes; defaults to 100 KB."""
maxsize = 10000000 maxsize = 10000000
"""The maximum size of the entire cache in bytes; defaults to 10 MB.""" """The maximum size of the entire cache in bytes; defaults to 10 MB."""
delay = 600 delay = 600
"""Seconds until the cached content expires; defaults to 600 (10 minutes).""" """Seconds until the cached content expires; defaults to 600 (10 minutes)."""
antistampede_timeout = 5 antistampede_timeout = 5
"""Seconds to wait for other threads to release a cache lock.""" """Seconds to wait for other threads to release a cache lock."""
expire_freq = 0.1 expire_freq = 0.1
"""Seconds to sleep between cache expiration sweeps.""" """Seconds to sleep between cache expiration sweeps."""
debug = False debug = False
def __init__(self): def __init__(self):
self.clear() self.clear()
# Run self.expire_cache in a separate daemon thread. # Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache') t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t self.expiration_thread = t
set_daemon(t, True) set_daemon(t, True)
t.start() t.start()
def clear(self): def clear(self):
"""Reset the cache to its initial, empty state.""" """Reset the cache to its initial, empty state."""
self.store = {} self.store = {}
@@ -181,10 +181,10 @@ class MemoryCache(Cache):
self.tot_expires = 0 self.tot_expires = 0
self.tot_non_modified = 0 self.tot_non_modified = 0
self.cursize = 0 self.cursize = 0
def expire_cache(self): def expire_cache(self):
"""Continuously examine cached objects, expiring stale ones. """Continuously examine cached objects, expiring stale ones.
This function is designed to be run in its own daemon thread, This function is designed to be run in its own daemon thread,
referenced at ``self.expiration_thread``. referenced at ``self.expiration_thread``.
""" """
@@ -207,17 +207,17 @@ class MemoryCache(Cache):
pass pass
del self.expirations[expiration_time] del self.expirations[expiration_time]
time.sleep(self.expire_freq) time.sleep(self.expire_freq)
def get(self): def get(self):
"""Return the current variant if in the cache, else None.""" """Return the current variant if in the cache, else None."""
request = cherrypy.serving.request request = cherrypy.serving.request
self.tot_gets += 1 self.tot_gets += 1
uri = cherrypy.url(qs=request.query_string) uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri) uricache = self.store.get(uri)
if uricache is None: if uricache is None:
return None return None
header_values = [request.headers.get(h, '') header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers] for h in uricache.selecting_headers]
variant = uricache.wait(key=tuple(sorted(header_values)), variant = uricache.wait(key=tuple(sorted(header_values)),
@@ -226,12 +226,12 @@ class MemoryCache(Cache):
if variant is not None: if variant is not None:
self.tot_hist += 1 self.tot_hist += 1
return variant return variant
def put(self, variant, size): def put(self, variant, size):
"""Store the current variant in the cache.""" """Store the current variant in the cache."""
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
uri = cherrypy.url(qs=request.query_string) uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri) uricache = self.store.get(uri)
if uricache is None: if uricache is None:
@@ -239,24 +239,24 @@ class MemoryCache(Cache):
uricache.selecting_headers = [ uricache.selecting_headers = [
e.value for e in response.headers.elements('Vary')] e.value for e in response.headers.elements('Vary')]
self.store[uri] = uricache self.store[uri] = uricache
if len(self.store) < self.maxobjects: if len(self.store) < self.maxobjects:
total_size = self.cursize + size total_size = self.cursize + size
# checks if there's space for the object # checks if there's space for the object
if (size < self.maxobj_size and total_size < self.maxsize): if (size < self.maxobj_size and total_size < self.maxsize):
# add to the expirations list # add to the expirations list
expiration_time = response.time + self.delay expiration_time = response.time + self.delay
bucket = self.expirations.setdefault(expiration_time, []) bucket = self.expirations.setdefault(expiration_time, [])
bucket.append((size, uri, uricache.selecting_headers)) bucket.append((size, uri, uricache.selecting_headers))
# add to the cache # add to the cache
header_values = [request.headers.get(h, '') header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers] for h in uricache.selecting_headers]
uricache[tuple(sorted(header_values))] = variant uricache[tuple(sorted(header_values))] = variant
self.tot_puts += 1 self.tot_puts += 1
self.cursize = total_size self.cursize = total_size
def delete(self): def delete(self):
"""Remove ALL cached variants of the current resource.""" """Remove ALL cached variants of the current resource."""
uri = cherrypy.url(qs=cherrypy.serving.request.query_string) uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
@@ -265,12 +265,12 @@ class MemoryCache(Cache):
def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs): def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304). """Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE: If POST, PUT, or DELETE:
* invalidates (deletes) any cached response for this resource * invalidates (deletes) any cached response for this resource
* sets request.cached = False * sets request.cached = False
* sets request.cacheable = False * sets request.cacheable = False
else if a cached copy exists: else if a cached copy exists:
* sets request.cached = True * sets request.cached = True
* sets request.cacheable = False * sets request.cacheable = False
@@ -280,7 +280,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if necessary. if necessary.
* sets response.status and response.body to the cached values * sets response.status and response.body to the cached values
* returns True * returns True
otherwise: otherwise:
* sets request.cached = False * sets request.cached = False
* sets request.cacheable = True * sets request.cacheable = True
@@ -288,16 +288,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"): if not hasattr(cherrypy, "_cache"):
# Make a process-wide Cache object. # Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)() cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
# Take all remaining kwargs and set them on the Cache object. # Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cherrypy._cache, k, v) setattr(cherrypy._cache, k, v)
cherrypy._cache.debug = debug cherrypy._cache.debug = debug
# POST, PUT, DELETE should invalidate (delete) the cached copy. # POST, PUT, DELETE should invalidate (delete) the cached copy.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
if request.method in invalid_methods: if request.method in invalid_methods:
@@ -308,12 +308,12 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False request.cached = False
request.cacheable = False request.cacheable = False
return False return False
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
request.cached = False request.cached = False
request.cacheable = True request.cacheable = True
return False return False
cache_data = cherrypy._cache.get() cache_data = cherrypy._cache.get()
request.cached = bool(cache_data) request.cached = bool(cache_data)
request.cacheable = not request.cached request.cacheable = not request.cached
@@ -335,7 +335,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False request.cached = False
request.cacheable = True request.cacheable = True
return False return False
if debug: if debug:
cherrypy.log('Reading response from cache', 'TOOLS.CACHING') cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
s, h, b, create_time = cache_data s, h, b, create_time = cache_data
@@ -347,15 +347,15 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False request.cached = False
request.cacheable = True request.cacheable = True
return False return False
# Copy the response headers. See http://www.cherrypy.org/ticket/721. # Copy the response headers. See http://www.cherrypy.org/ticket/721.
response.headers = rh = httputil.HeaderMap() response.headers = rh = httputil.HeaderMap()
for k in h: for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k)) dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header # Add the required Age header
response.headers["Age"] = str(age) response.headers["Age"] = str(age)
try: try:
# Note that validate_since depends on a Last-Modified header; # Note that validate_since depends on a Last-Modified header;
# this was put into the cached copy, and should have been # this was put into the cached copy, and should have been
@@ -366,7 +366,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if x.status == 304: if x.status == 304:
cherrypy._cache.tot_non_modified += 1 cherrypy._cache.tot_non_modified += 1
raise raise
# serve it & get out from the request # serve it & get out from the request
response.status = s response.status = s
response.body = b response.body = b
@@ -379,11 +379,11 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
def tee_output(): def tee_output():
"""Tee response output to cache storage. Internal.""" """Tee response output to cache storage. Internal."""
# Used by CachingTool by attaching to request.hooks # Used by CachingTool by attaching to request.hooks
request = cherrypy.serving.request request = cherrypy.serving.request
if 'no-store' in request.headers.values('Cache-Control'): if 'no-store' in request.headers.values('Cache-Control'):
return return
def tee(body): def tee(body):
"""Tee response.body into a list.""" """Tee response.body into a list."""
if ('no-cache' in response.headers.values('Pragma') or if ('no-cache' in response.headers.values('Pragma') or
@@ -391,17 +391,17 @@ def tee_output():
for chunk in body: for chunk in body:
yield chunk yield chunk
return return
output = [] output = []
for chunk in body: for chunk in body:
output.append(chunk) output.append(chunk)
yield chunk yield chunk
# save the cache data # save the cache data
body = ntob('').join(output) body = ntob('').join(output)
cherrypy._cache.put((response.status, response.headers or {}, cherrypy._cache.put((response.status, response.headers or {},
body, response.time), len(body)) body, response.time), len(body))
response = cherrypy.serving.response response = cherrypy.serving.response
response.body = tee(response.body) response.body = tee(response.body)
@@ -415,25 +415,25 @@ def expires(secs=0, force=False, debug=False):
expire. The 'Expires' header will be set to response.time + secs. expire. The 'Expires' header will be set to response.time + secs.
If secs is zero, the 'Expires' header is set one year in the past, and If secs is zero, the 'Expires' header is set one year in the past, and
the following "cache prevention" headers are also set: the following "cache prevention" headers are also set:
* Pragma: no-cache * Pragma: no-cache
* Cache-Control': no-cache, must-revalidate * Cache-Control': no-cache, must-revalidate
force force
If False, the following headers are checked: If False, the following headers are checked:
* Etag * Etag
* Last-Modified * Last-Modified
* Age * Age
* Expires * Expires
If any are already present, none of the above response headers are set. If any are already present, none of the above response headers are set.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
headers = response.headers headers = response.headers
cacheable = False cacheable = False
if not force: if not force:
# some header names that indicate that the response can be cached # some header names that indicate that the response can be cached
@@ -441,7 +441,7 @@ def expires(secs=0, force=False, debug=False):
if indicator in headers: if indicator in headers:
cacheable = True cacheable = True
break break
if not cacheable and not force: if not cacheable and not force:
if debug: if debug:
cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
@@ -450,7 +450,7 @@ def expires(secs=0, force=False, debug=False):
cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
if isinstance(secs, datetime.timedelta): if isinstance(secs, datetime.timedelta):
secs = (86400 * secs.days) + secs.seconds secs = (86400 * secs.days) + secs.seconds
if secs == 0: if secs == 0:
if force or ("Pragma" not in headers): if force or ("Pragma" not in headers):
headers["Pragma"] = "no-cache" headers["Pragma"] = "no-cache"
+25 -25
View File
@@ -1,7 +1,7 @@
"""Code-coverage tools for CherryPy. """Code-coverage tools for CherryPy.
To use this module, or the coverage tools in the test suite, To use this module, or the coverage tools in the test suite,
you need to download 'coverage.py', either Gareth Rees' `original you need to download 'coverage.py', either Gareth Rees' `original
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_ implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
or Ned Batchelder's `enhanced version: or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_ <http://www.nedbatchelder.com/code/modules/coverage.html>`_
@@ -37,10 +37,10 @@ except ImportError:
# Setting the_coverage to None will raise errors # Setting the_coverage to None will raise errors
# that need to be trapped downstream. # that need to be trapped downstream.
the_coverage = None the_coverage = None
import warnings import warnings
warnings.warn("No code coverage will be performed; coverage.py could not be imported.") warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
def start(): def start():
pass pass
start.priority = 20 start.priority = 20
@@ -69,7 +69,7 @@ TEMPLATE_MENU = """<html>
font-size: small; font-size: small;
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
margin-top: 5px; margin-top: 5px;
} }
input { border: 1px solid #ccc; padding: 2px; } input { border: 1px solid #ccc; padding: 2px; }
.directory { .directory {
@@ -126,7 +126,7 @@ TEMPLATE_FORM = """
<input type='submit' value='Change view' id="submit"/> <input type='submit' value='Change view' id="submit"/>
</form> </form>
</div>""" </div>"""
TEMPLATE_FRAMESET = """<html> TEMPLATE_FRAMESET = """<html>
<head><title>CherryPy coverage data</title></head> <head><title>CherryPy coverage data</title></head>
@@ -184,22 +184,22 @@ def _percent(statements, missing):
def _show_branch(root, base, path, pct=0, showpct=False, exclude="", def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
coverage=the_coverage): coverage=the_coverage):
# Show the directory name and any of our children # Show the directory name and any of our children
dirs = [k for k, v in root.items() if v] dirs = [k for k, v in root.items() if v]
dirs.sort() dirs.sort()
for name in dirs: for name in dirs:
newpath = os.path.join(path, name) newpath = os.path.join(path, name)
if newpath.lower().startswith(base): if newpath.lower().startswith(base):
relpath = newpath[len(base):] relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep) yield "| " * relpath.count(os.sep)
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \ yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
(newpath, quote_plus(exclude), name) (newpath, quote_plus(exclude), name)
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage): for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
yield chunk yield chunk
# Now list the files # Now list the files
if path.lower().startswith(base): if path.lower().startswith(base):
relpath = path[len(base):] relpath = path[len(base):]
@@ -207,7 +207,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
files.sort() files.sort()
for name in files: for name in files:
newpath = os.path.join(path, name) newpath = os.path.join(path, name)
pc_str = "" pc_str = ""
if showpct: if showpct:
try: try:
@@ -222,7 +222,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
pc_str = "<span class='fail'>%s</span>" % pc_str pc_str = "<span class='fail'>%s</span>" % pc_str
else: else:
pc_str = "<span class='pass'>%s</span>" % pc_str pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
pc_str, newpath, name) pc_str, newpath, name)
@@ -232,7 +232,7 @@ def _skip_file(path, exclude):
def _graft(path, tree): def _graft(path, tree):
d = tree d = tree
p = path p = path
atoms = [] atoms = []
while True: while True:
@@ -243,7 +243,7 @@ def _graft(path, tree):
atoms.append(p) atoms.append(p)
if p != "/": if p != "/":
atoms.append("/") atoms.append("/")
atoms.reverse() atoms.reverse()
for node in atoms: for node in atoms:
if node: if node:
@@ -259,7 +259,7 @@ def get_tree(base, exclude, coverage=the_coverage):
return tree return tree
class CoverStats(object): class CoverStats(object):
def __init__(self, coverage, root=None): def __init__(self, coverage, root=None):
self.coverage = coverage self.coverage = coverage
if root is None: if root is None:
@@ -268,20 +268,20 @@ class CoverStats(object):
import cherrypy import cherrypy
root = os.path.dirname(cherrypy.__file__) root = os.path.dirname(cherrypy.__file__)
self.root = root self.root = root
def index(self): def index(self):
return TEMPLATE_FRAMESET % self.root.lower() return TEMPLATE_FRAMESET % self.root.lower()
index.exposed = True index.exposed = True
def menu(self, base="/", pct="50", showpct="", def menu(self, base="/", pct="50", showpct="",
exclude=r'python\d\.\d|test|tut\d|tutorial'): exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names. # The coverage module uses all-lower-case names.
base = base.lower().rstrip(os.sep) base = base.lower().rstrip(os.sep)
yield TEMPLATE_MENU yield TEMPLATE_MENU
yield TEMPLATE_FORM % locals() yield TEMPLATE_FORM % locals()
# Start by showing links for parent paths # Start by showing links for parent paths
yield "<div id='crumbs'>" yield "<div id='crumbs'>"
path = "" path = ""
@@ -292,9 +292,9 @@ class CoverStats(object):
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep)) % (path, quote_plus(exclude), atom, os.sep))
yield "</div>" yield "</div>"
yield "<div id='tree'>" yield "<div id='tree'>"
# Then display the tree # Then display the tree
tree = get_tree(base, exclude, self.coverage) tree = get_tree(base, exclude, self.coverage)
if not tree: if not tree:
@@ -303,11 +303,11 @@ class CoverStats(object):
for chunk in _show_branch(tree, base, "/", pct, for chunk in _show_branch(tree, base, "/", pct,
showpct=='checked', exclude, coverage=self.coverage): showpct=='checked', exclude, coverage=self.coverage):
yield chunk yield chunk
yield "</div>" yield "</div>"
yield "</body></html>" yield "</body></html>"
menu.exposed = True menu.exposed = True
def annotated_file(self, filename, statements, excluded, missing): def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r') source = open(filename, 'r')
buffer = [] buffer = []
@@ -329,7 +329,7 @@ class CoverStats(object):
yield template % (lno, cgi.escape(pastline)) yield template % (lno, cgi.escape(pastline))
buffer = [] buffer = []
yield template % (lineno, cgi.escape(line)) yield template % (lineno, cgi.escape(line))
def report(self, name): def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2(name) filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
pc = _percent(statements, missing) pc = _percent(statements, missing)
@@ -352,7 +352,7 @@ def serve(path=localFile, port=8080, root=None):
from coverage import coverage from coverage import coverage
cov = coverage(data_file = path) cov = coverage(data_file = path)
cov.load() cov.load()
import cherrypy import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
+35 -35
View File
@@ -235,21 +235,21 @@ proc_time = lambda s: time.time() - s['Start Time']
class ByteCountWrapper(object): class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read.""" """Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile): def __init__(self, rfile):
self.rfile = rfile self.rfile = rfile
self.bytes_read = 0 self.bytes_read = 0
def read(self, size=-1): def read(self, size=-1):
data = self.rfile.read(size) data = self.rfile.read(size)
self.bytes_read += len(data) self.bytes_read += len(data)
return data return data
def readline(self, size=-1): def readline(self, size=-1):
data = self.rfile.readline(size) data = self.rfile.readline(size)
self.bytes_read += len(data) self.bytes_read += len(data)
return data return data
def readlines(self, sizehint=0): def readlines(self, sizehint=0):
# Shamelessly stolen from StringIO # Shamelessly stolen from StringIO
total = 0 total = 0
@@ -262,13 +262,13 @@ class ByteCountWrapper(object):
break break
line = self.readline() line = self.readline()
return lines return lines
def close(self): def close(self):
self.rfile.close() self.rfile.close()
def __iter__(self): def __iter__(self):
return self return self
def next(self): def next(self):
data = self.rfile.next() data = self.rfile.next()
self.bytes_read += len(data) self.bytes_read += len(data)
@@ -280,29 +280,29 @@ average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
class StatsTool(cherrypy.Tool): class StatsTool(cherrypy.Tool):
"""Record various information about the current request.""" """Record various information about the current request."""
def __init__(self): def __init__(self):
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
def _setup(self): def _setup(self):
"""Hook this tool into cherrypy.request. """Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
if appstats.get('Enabled', False): if appstats.get('Enabled', False):
cherrypy.Tool._setup(self) cherrypy.Tool._setup(self)
self.record_start() self.record_start()
def record_start(self): def record_start(self):
"""Record the beginning of a request.""" """Record the beginning of a request."""
request = cherrypy.serving.request request = cherrypy.serving.request
if not hasattr(request.rfile, 'bytes_read'): if not hasattr(request.rfile, 'bytes_read'):
request.rfile = ByteCountWrapper(request.rfile) request.rfile = ByteCountWrapper(request.rfile)
request.body.fp = request.rfile request.body.fp = request.rfile
r = request.remote r = request.remote
appstats['Current Requests'] += 1 appstats['Current Requests'] += 1
appstats['Total Requests'] += 1 appstats['Total Requests'] += 1
appstats['Requests'][threading._get_ident()] = { appstats['Requests'][threading._get_ident()] = {
@@ -322,30 +322,30 @@ class StatsTool(cherrypy.Tool):
"""Record the end of a request.""" """Record the end of a request."""
resp = cherrypy.serving.response resp = cherrypy.serving.response
w = appstats['Requests'][threading._get_ident()] w = appstats['Requests'][threading._get_ident()]
r = cherrypy.request.rfile.bytes_read r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r w['Bytes Read'] = r
appstats['Total Bytes Read'] += r appstats['Total Bytes Read'] += r
if resp.stream: if resp.stream:
w['Bytes Written'] = 'chunked' w['Bytes Written'] = 'chunked'
else: else:
cl = int(resp.headers.get('Content-Length', 0)) cl = int(resp.headers.get('Content-Length', 0))
w['Bytes Written'] = cl w['Bytes Written'] = cl
appstats['Total Bytes Written'] += cl appstats['Total Bytes Written'] += cl
w['Response Status'] = getattr(resp, 'output_status', None) or resp.status w['Response Status'] = getattr(resp, 'output_status', None) or resp.status
w['End Time'] = time.time() w['End Time'] = time.time()
p = w['End Time'] - w['Start Time'] p = w['End Time'] - w['Start Time']
w['Processing Time'] = p w['Processing Time'] = p
appstats['Total Time'] += p appstats['Total Time'] += p
appstats['Current Requests'] -= 1 appstats['Current Requests'] -= 1
if debug: if debug:
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
if uriset: if uriset:
rs = appstats.setdefault('URI Set Tracking', {}) rs = appstats.setdefault('URI Set Tracking', {})
r = rs.setdefault(uriset, { r = rs.setdefault(uriset, {
@@ -357,7 +357,7 @@ class StatsTool(cherrypy.Tool):
r['Max'] = p r['Max'] = p
r['Count'] += 1 r['Count'] += 1
r['Sum'] += p r['Sum'] += p
if slow_queries and p > slow_queries: if slow_queries and p > slow_queries:
sq = appstats.setdefault('Slow Queries', []) sq = appstats.setdefault('Slow Queries', [])
sq.append(w.copy()) sq.append(w.copy())
@@ -410,7 +410,7 @@ def pause_resume(ns):
class StatsPage(object): class StatsPage(object):
formatting = { formatting = {
'CherryPy Applications': { 'CherryPy Applications': {
'Enabled': pause_resume('CherryPy Applications'), 'Enabled': pause_resume('CherryPy Applications'),
@@ -448,8 +448,8 @@ class StatsPage(object):
'Start time': iso_format, 'Start time': iso_format,
}, },
} }
def index(self): def index(self):
# Transform the raw data into pretty output for HTML # Transform the raw data into pretty output for HTML
yield """ yield """
@@ -506,7 +506,7 @@ table.stats2 th {
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars() <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
if colnum == 2: yield """ if colnum == 2: yield """
</tr>""" </tr>"""
if colnum == 0: yield """ if colnum == 0: yield """
<th></th><td></td> <th></th><td></td>
<th></th><td></td> <th></th><td></td>
@@ -547,7 +547,7 @@ table.stats2 th {
</html> </html>
""" """
index.exposed = True index.exposed = True
def get_namespaces(self): def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace.""" """Yield (title, scalars, collections) for each namespace."""
s = extrapolate_statistics(logging.statistics) s = extrapolate_statistics(logging.statistics)
@@ -574,7 +574,7 @@ table.stats2 th {
v = format % v v = format % v
scalars.append((k, v)) scalars.append((k, v))
yield title, scalars, collections yield title, scalars, collections
def get_dict_collection(self, v, formatting): def get_dict_collection(self, v, formatting):
"""Return ([headers], [rows]) for the given collection.""" """Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict. # E.g., the 'Requests' dict.
@@ -588,7 +588,7 @@ table.stats2 th {
if k3 not in headers: if k3 not in headers:
headers.append(k3) headers.append(k3)
headers.sort() headers.sort()
subrows = [] subrows = []
for k2, record in sorted(v.items()): for k2, record in sorted(v.items()):
subrow = [k2] subrow = [k2]
@@ -604,9 +604,9 @@ table.stats2 th {
v3 = format % v3 v3 = format % v3
subrow.append(v3) subrow.append(v3)
subrows.append(subrow) subrows.append(subrow)
return headers, subrows return headers, subrows
def get_list_collection(self, v, formatting): def get_list_collection(self, v, formatting):
"""Return ([headers], [subrows]) for the given collection.""" """Return ([headers], [subrows]) for the given collection."""
# E.g., the 'Slow Queries' list. # E.g., the 'Slow Queries' list.
@@ -620,7 +620,7 @@ table.stats2 th {
if k3 not in headers: if k3 not in headers:
headers.append(k3) headers.append(k3)
headers.sort() headers.sort()
subrows = [] subrows = []
for record in v: for record in v:
subrow = [] subrow = []
@@ -636,23 +636,23 @@ table.stats2 th {
v3 = format % v3 v3 = format % v3
subrow.append(v3) subrow.append(v3)
subrows.append(subrow) subrows.append(subrow)
return headers, subrows return headers, subrows
if json is not None: if json is not None:
def data(self): def data(self):
s = extrapolate_statistics(logging.statistics) s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json' cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4) return json.dumps(s, sort_keys=True, indent=4)
data.exposed = True data.exposed = True
def pause(self, namespace): def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./') raise cherrypy.HTTPRedirect('./')
pause.exposed = True pause.exposed = True
pause.cp_config = {'tools.allow.on': True, pause.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']} 'tools.allow.methods': ['POST']}
def resume(self, namespace): def resume(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = True logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./') raise cherrypy.HTTPRedirect('./')
+64 -64
View File
@@ -12,12 +12,12 @@ from cherrypy.lib import httputil as _httputil
def validate_etags(autotags=False, debug=False): def validate_etags(autotags=False, debug=False):
"""Validate the current ETag against If-Match, If-None-Match headers. """Validate the current ETag against If-Match, If-None-Match headers.
If autotags is True, an ETag response-header value will be provided If autotags is True, an ETag response-header value will be provided
from an MD5 hash of the response body (unless some other code has from an MD5 hash of the response body (unless some other code has
already provided an ETag header). If False (the default), the ETag already provided an ETag header). If False (the default), the ETag
will not be automatic. will not be automatic.
WARNING: the autotags feature is not designed for URL's which allow WARNING: the autotags feature is not designed for URL's which allow
methods other than GET. For example, if a POST to the same URL returns methods other than GET. For example, if a POST to the same URL returns
no content, the automatic ETag will be incorrect, breaking a fundamental no content, the automatic ETag will be incorrect, breaking a fundamental
@@ -27,15 +27,15 @@ def validate_etags(autotags=False, debug=False):
See :rfc:`2616` Section 14.24. See :rfc:`2616` Section 14.24.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against being run twice. # Guard against being run twice.
if hasattr(response, "ETag"): if hasattr(response, "ETag"):
return return
status, reason, msg = _httputil.valid_status(response.status) status, reason, msg = _httputil.valid_status(response.status)
etag = response.headers.get('ETag') etag = response.headers.get('ETag')
# Automatic ETag generation. See warning in docstring. # Automatic ETag generation. See warning in docstring.
if etag: if etag:
if debug: if debug:
@@ -52,9 +52,9 @@ def validate_etags(autotags=False, debug=False):
if debug: if debug:
cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
response.headers['ETag'] = etag response.headers['ETag'] = etag
response.ETag = etag response.ETag = etag
# "If the request would, without the If-Match header field, result in # "If the request would, without the If-Match header field, result in
# anything other than a 2xx or 412 status, then the If-Match header # anything other than a 2xx or 412 status, then the If-Match header
# MUST be ignored." # MUST be ignored."
@@ -62,7 +62,7 @@ def validate_etags(autotags=False, debug=False):
cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
if status >= 200 and status <= 299: if status >= 200 and status <= 299:
request = cherrypy.serving.request request = cherrypy.serving.request
conditions = request.headers.elements('If-Match') or [] conditions = request.headers.elements('If-Match') or []
conditions = [str(x) for x in conditions] conditions = [str(x) for x in conditions]
if debug: if debug:
@@ -71,7 +71,7 @@ def validate_etags(autotags=False, debug=False):
if conditions and not (conditions == ["*"] or etag in conditions): if conditions and not (conditions == ["*"] or etag in conditions):
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
"not match %r" % (etag, conditions)) "not match %r" % (etag, conditions))
conditions = request.headers.elements('If-None-Match') or [] conditions = request.headers.elements('If-None-Match') or []
conditions = [str(x) for x in conditions] conditions = [str(x) for x in conditions]
if debug: if debug:
@@ -88,7 +88,7 @@ def validate_etags(autotags=False, debug=False):
def validate_since(): def validate_since():
"""Validate the current Last-Modified against If-Modified-Since headers. """Validate the current Last-Modified against If-Modified-Since headers.
If no code has set the Last-Modified response header, then no validation If no code has set the Last-Modified response header, then no validation
will be performed. will be performed.
""" """
@@ -96,14 +96,14 @@ def validate_since():
lastmod = response.headers.get('Last-Modified') lastmod = response.headers.get('Last-Modified')
if lastmod: if lastmod:
status, reason, msg = _httputil.valid_status(response.status) status, reason, msg = _httputil.valid_status(response.status)
request = cherrypy.serving.request request = cherrypy.serving.request
since = request.headers.get('If-Unmodified-Since') since = request.headers.get('If-Unmodified-Since')
if since and since != lastmod: if since and since != lastmod:
if (status >= 200 and status <= 299) or status == 412: if (status >= 200 and status <= 299) or status == 412:
raise cherrypy.HTTPError(412) raise cherrypy.HTTPError(412)
since = request.headers.get('If-Modified-Since') since = request.headers.get('If-Modified-Since')
if since and since == lastmod: if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304: if (status >= 200 and status <= 299) or status == 304:
@@ -117,11 +117,11 @@ def validate_since():
def allow(methods=None, debug=False): def allow(methods=None, debug=False):
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']). """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
The given methods are case-insensitive, and may be in any order. The given methods are case-insensitive, and may be in any order.
If only one method is allowed, you may supply a single string; If only one method is allowed, you may supply a single string;
if more than one, supply a list of strings. if more than one, supply a list of strings.
Regardless of whether the current method is allowed or not, this Regardless of whether the current method is allowed or not, this
also emits an 'Allow' response header, containing the given methods. also emits an 'Allow' response header, containing the given methods.
""" """
@@ -132,7 +132,7 @@ def allow(methods=None, debug=False):
methods = ['GET', 'HEAD'] methods = ['GET', 'HEAD']
elif 'GET' in methods and 'HEAD' not in methods: elif 'GET' in methods and 'HEAD' not in methods:
methods.append('HEAD') methods.append('HEAD')
cherrypy.response.headers['Allow'] = ', '.join(methods) cherrypy.response.headers['Allow'] = ', '.join(methods)
if cherrypy.request.method not in methods: if cherrypy.request.method not in methods:
if debug: if debug:
@@ -148,27 +148,27 @@ def allow(methods=None, debug=False):
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme='X-Forwarded-Proto', debug=False): scheme='X-Forwarded-Proto', debug=False):
"""Change the base URL (scheme://host[:port][/path]). """Change the base URL (scheme://host[:port][/path]).
For running a CP server behind Apache, lighttpd, or other HTTP server. For running a CP server behind Apache, lighttpd, or other HTTP server.
For Apache and lighttpd, you should leave the 'local' argument at the For Apache and lighttpd, you should leave the 'local' argument at the
default value of 'X-Forwarded-Host'. For Squid, you probably want to set default value of 'X-Forwarded-Host'. For Squid, you probably want to set
tools.proxy.local = 'Origin'. tools.proxy.local = 'Origin'.
If you want the new request.base to include path info (not just the host), If you want the new request.base to include path info (not just the host),
you must explicitly set base to the full base path, and ALSO set 'local' you must explicitly set base to the full base path, and ALSO set 'local'
to '', so that the X-Forwarded-Host request header (which never includes to '', so that the X-Forwarded-Host request header (which never includes
path info) does not override it. Regardless, the value for 'base' MUST path info) does not override it. Regardless, the value for 'base' MUST
NOT end in a slash. NOT end in a slash.
cherrypy.request.remote.ip (the IP address of the client) will be cherrypy.request.remote.ip (the IP address of the client) will be
rewritten if the header specified by the 'remote' arg is valid. rewritten if the header specified by the 'remote' arg is valid.
By default, 'remote' is set to 'X-Forwarded-For'. If you do not By default, 'remote' is set to 'X-Forwarded-For'. If you do not
want to rewrite remote.ip, set the 'remote' arg to an empty string. want to rewrite remote.ip, set the 'remote' arg to an empty string.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
if scheme: if scheme:
s = request.headers.get(scheme, None) s = request.headers.get(scheme, None)
if debug: if debug:
@@ -181,7 +181,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme = s scheme = s
if not scheme: if not scheme:
scheme = request.base[:request.base.find("://")] scheme = request.base[:request.base.find("://")]
if local: if local:
lbase = request.headers.get(local, None) lbase = request.headers.get(local, None)
if debug: if debug:
@@ -194,13 +194,13 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
base = '127.0.0.1' base = '127.0.0.1'
else: else:
base = '127.0.0.1:%s' % port base = '127.0.0.1:%s' % port
if base.find("://") == -1: if base.find("://") == -1:
# add http:// or https:// if needed # add http:// or https:// if needed
base = scheme + "://" + base base = scheme + "://" + base
request.base = base request.base = base
if remote: if remote:
xff = request.headers.get(remote) xff = request.headers.get(remote)
if debug: if debug:
@@ -214,7 +214,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
def ignore_headers(headers=('Range',), debug=False): def ignore_headers(headers=('Range',), debug=False):
"""Delete request headers whose field names are included in 'headers'. """Delete request headers whose field names are included in 'headers'.
This is a useful tool for working behind certain HTTP servers; This is a useful tool for working behind certain HTTP servers;
for example, Apache duplicates the work that CP does for 'Range' for example, Apache duplicates the work that CP does for 'Range'
headers, and will doubly-truncate the response. headers, and will doubly-truncate the response.
@@ -241,10 +241,10 @@ response_headers.failsafe = True
def referer(pattern, accept=True, accept_missing=False, error=403, def referer(pattern, accept=True, accept_missing=False, error=403,
message='Forbidden Referer header.', debug=False): message='Forbidden Referer header.', debug=False):
"""Raise HTTPError if Referer header does/does not match the given pattern. """Raise HTTPError if Referer header does/does not match the given pattern.
pattern pattern
A regular expression pattern to test against the Referer. A regular expression pattern to test against the Referer.
accept accept
If True, the Referer must match the pattern; if False, If True, the Referer must match the pattern; if False,
the Referer must NOT match the pattern. the Referer must NOT match the pattern.
@@ -254,10 +254,10 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
error error
The HTTP error code to return to the client on failure. The HTTP error code to return to the client on failure.
message message
A string to include in the response body on failure. A string to include in the response body on failure.
""" """
try: try:
ref = cherrypy.serving.request.headers['Referer'] ref = cherrypy.serving.request.headers['Referer']
@@ -272,32 +272,32 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
cherrypy.log('No Referer header', 'TOOLS.REFERER') cherrypy.log('No Referer header', 'TOOLS.REFERER')
if accept_missing: if accept_missing:
return return
raise cherrypy.HTTPError(error, message) raise cherrypy.HTTPError(error, message)
class SessionAuth(object): class SessionAuth(object):
"""Assert that the user is logged in.""" """Assert that the user is logged in."""
session_key = "username" session_key = "username"
debug = False debug = False
def check_username_and_password(self, username, password): def check_username_and_password(self, username, password):
pass pass
def anonymous(self): def anonymous(self):
"""Provide a temporary user name for anonymous users.""" """Provide a temporary user name for anonymous users."""
pass pass
def on_login(self, username): def on_login(self, username):
pass pass
def on_logout(self, username): def on_logout(self, username):
pass pass
def on_check(self, username): def on_check(self, username):
pass pass
def login_screen(self, from_page='..', username='', error_msg='', **kwargs): def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
return ntob("""<html><body> return ntob("""<html><body>
Message: %(error_msg)s Message: %(error_msg)s
@@ -309,7 +309,7 @@ Message: %(error_msg)s
</form> </form>
</body></html>""" % {'from_page': from_page, 'username': username, </body></html>""" % {'from_page': from_page, 'username': username,
'error_msg': error_msg}, "utf-8") 'error_msg': error_msg}, "utf-8")
def do_login(self, username, password, from_page='..', **kwargs): def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled.""" """Login. May raise redirect, or return True if request handled."""
response = cherrypy.serving.response response = cherrypy.serving.response
@@ -326,7 +326,7 @@ Message: %(error_msg)s
cherrypy.session[self.session_key] = username cherrypy.session[self.session_key] = username
self.on_login(username) self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or "/") raise cherrypy.HTTPRedirect(from_page or "/")
def do_logout(self, from_page='..', **kwargs): def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled.""" """Logout. May raise redirect, or return True if request handled."""
sess = cherrypy.session sess = cherrypy.session
@@ -336,13 +336,13 @@ Message: %(error_msg)s
cherrypy.serving.request.login = None cherrypy.serving.request.login = None
self.on_logout(username) self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page) raise cherrypy.HTTPRedirect(from_page)
def do_check(self): def do_check(self):
"""Assert username. May raise redirect, or return True if request handled.""" """Assert username. May raise redirect, or return True if request handled."""
sess = cherrypy.session sess = cherrypy.session
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
username = sess.get(self.session_key) username = sess.get(self.session_key)
if not username: if not username:
sess[self.session_key] = username = self.anonymous() sess[self.session_key] = username = self.anonymous()
@@ -362,11 +362,11 @@ Message: %(error_msg)s
cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH') cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH')
request.login = username request.login = username
self.on_check(username) self.on_check(username)
def run(self): def run(self):
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
path = request.path_info path = request.path_info
if path.endswith('login_screen'): if path.endswith('login_screen'):
if self.debug: if self.debug:
@@ -420,7 +420,7 @@ def log_request_headers(debug=False):
def log_hooks(debug=False): def log_hooks(debug=False):
"""Write request.hooks to the cherrypy error log.""" """Write request.hooks to the cherrypy error log."""
request = cherrypy.serving.request request = cherrypy.serving.request
msg = [] msg = []
# Sort by the standard points if possible. # Sort by the standard points if possible.
from cherrypy import _cprequest from cherrypy import _cprequest
@@ -428,7 +428,7 @@ def log_hooks(debug=False):
for k in request.hooks.keys(): for k in request.hooks.keys():
if k not in points: if k not in points:
points.append(k) points.append(k)
for k in points: for k in points:
msg.append(" %s:" % k) msg.append(" %s:" % k)
v = request.hooks.get(k, []) v = request.hooks.get(k, [])
@@ -453,7 +453,7 @@ def trailing_slash(missing=True, extra=False, status=None, debug=False):
"""Redirect if path_info has (missing|extra) trailing slash.""" """Redirect if path_info has (missing|extra) trailing slash."""
request = cherrypy.serving.request request = cherrypy.serving.request
pi = request.path_info pi = request.path_info
if debug: if debug:
cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
(request.is_index, missing, extra, pi), (request.is_index, missing, extra, pi),
@@ -472,7 +472,7 @@ def trailing_slash(missing=True, extra=False, status=None, debug=False):
def flatten(debug=False): def flatten(debug=False):
"""Wrap response.body in a generator that recursively iterates over body. """Wrap response.body in a generator that recursively iterates over body.
This allows cherrypy.response.body to consist of 'nested generators'; This allows cherrypy.response.body to consist of 'nested generators';
that is, a set of generators that yield generators. that is, a set of generators that yield generators.
""" """
@@ -495,9 +495,9 @@ def flatten(debug=False):
def accept(media=None, debug=False): def accept(media=None, debug=False):
"""Return the client's preferred media-type (from the given Content-Types). """Return the client's preferred media-type (from the given Content-Types).
If 'media' is None (the default), no test will be performed. If 'media' is None (the default), no test will be performed.
If 'media' is provided, it should be the Content-Type value (as a string) If 'media' is provided, it should be the Content-Type value (as a string)
or values (as a list or tuple of strings) which the current resource or values (as a list or tuple of strings) which the current resource
can emit. The client's acceptable media ranges (as declared in the can emit. The client's acceptable media ranges (as declared in the
@@ -505,16 +505,16 @@ def accept(media=None, debug=False):
values; the first such string is returned. That is, the return value values; the first such string is returned. That is, the return value
will always be one of the strings provided in the 'media' arg (or None will always be one of the strings provided in the 'media' arg (or None
if 'media' is None). if 'media' is None).
If no match is found, then HTTPError 406 (Not Acceptable) is raised. If no match is found, then HTTPError 406 (Not Acceptable) is raised.
Note that most web browsers send */* as a (low-quality) acceptable Note that most web browsers send */* as a (low-quality) acceptable
media range, which should match any Content-Type. In addition, "...if media range, which should match any Content-Type. In addition, "...if
no Accept header field is present, then it is assumed that the client no Accept header field is present, then it is assumed that the client
accepts all media types." accepts all media types."
Matching types are checked in order of client preference first, Matching types are checked in order of client preference first,
and then in the order of the given 'media' values. and then in the order of the given 'media' values.
Note that this function does not honor accept-params (other than "q"). Note that this function does not honor accept-params (other than "q").
""" """
if not media: if not media:
@@ -522,7 +522,7 @@ def accept(media=None, debug=False):
if isinstance(media, basestring): if isinstance(media, basestring):
media = [media] media = [media]
request = cherrypy.serving.request request = cherrypy.serving.request
# Parse the Accept request header, and try to match one # Parse the Accept request header, and try to match one
# of the requested media-ranges (in order of preference). # of the requested media-ranges (in order of preference).
ranges = request.headers.elements('Accept') ranges = request.headers.elements('Accept')
@@ -556,7 +556,7 @@ def accept(media=None, debug=False):
cherrypy.log('Match due to %s' % element.value, cherrypy.log('Match due to %s' % element.value,
'TOOLS.ACCEPT') 'TOOLS.ACCEPT')
return element.value return element.value
# No suitable media-range found. # No suitable media-range found.
ah = request.headers.get('Accept') ah = request.headers.get('Accept')
if ah is None: if ah is None:
@@ -569,22 +569,22 @@ def accept(media=None, debug=False):
class MonitoredHeaderMap(_httputil.HeaderMap): class MonitoredHeaderMap(_httputil.HeaderMap):
def __init__(self): def __init__(self):
self.accessed_headers = set() self.accessed_headers = set()
def __getitem__(self, key): def __getitem__(self, key):
self.accessed_headers.add(key) self.accessed_headers.add(key)
return _httputil.HeaderMap.__getitem__(self, key) return _httputil.HeaderMap.__getitem__(self, key)
def __contains__(self, key): def __contains__(self, key):
self.accessed_headers.add(key) self.accessed_headers.add(key)
return _httputil.HeaderMap.__contains__(self, key) return _httputil.HeaderMap.__contains__(self, key)
def get(self, key, default=None): def get(self, key, default=None):
self.accessed_headers.add(key) self.accessed_headers.add(key)
return _httputil.HeaderMap.get(self, key, default=default) return _httputil.HeaderMap.get(self, key, default=default)
if hasattr({}, 'has_key'): if hasattr({}, 'has_key'):
# Python 2 # Python 2
def has_key(self, key): def has_key(self, key):
@@ -595,13 +595,13 @@ class MonitoredHeaderMap(_httputil.HeaderMap):
def autovary(ignore=None, debug=False): def autovary(ignore=None, debug=False):
"""Auto-populate the Vary response header based on request.header access.""" """Auto-populate the Vary response header based on request.header access."""
request = cherrypy.serving.request request = cherrypy.serving.request
req_h = request.headers req_h = request.headers
request.headers = MonitoredHeaderMap() request.headers = MonitoredHeaderMap()
request.headers.update(req_h) request.headers.update(req_h)
if ignore is None: if ignore is None:
ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
def set_response_header(): def set_response_header():
resp_h = cherrypy.serving.response.headers resp_h = cherrypy.serving.response.headers
v = set([e.value for e in resp_h.elements('Vary')]) v = set([e.value for e in resp_h.elements('Vary')])
+39 -39
View File
@@ -9,19 +9,19 @@ from cherrypy.lib import set_vary_header
def decode(encoding=None, default_encoding='utf-8'): def decode(encoding=None, default_encoding='utf-8'):
"""Replace or extend the list of charsets used to decode a request entity. """Replace or extend the list of charsets used to decode a request entity.
Either argument may be a single string or a list of strings. Either argument may be a single string or a list of strings.
encoding encoding
If not None, restricts the set of charsets attempted while decoding If not None, restricts the set of charsets attempted while decoding
a request entity to the given set (even if a different charset is given in a request entity to the given set (even if a different charset is given in
the Content-Type request header). the Content-Type request header).
default_encoding default_encoding
Only in effect if the 'encoding' argument is not given. Only in effect if the 'encoding' argument is not given.
If given, the set of charsets attempted while decoding a request entity is If given, the set of charsets attempted while decoding a request entity is
*extended* with the given value(s). *extended* with the given value(s).
""" """
body = cherrypy.request.body body = cherrypy.request.body
if encoding is not None: if encoding is not None:
@@ -35,7 +35,7 @@ def decode(encoding=None, default_encoding='utf-8'):
class ResponseEncoder: class ResponseEncoder:
default_encoding = 'utf-8' default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r." failmsg = "Response body could not be encoded with %r."
encoding = None encoding = None
@@ -43,11 +43,11 @@ class ResponseEncoder:
text_only = True text_only = True
add_charset = True add_charset = True
debug = False debug = False
def __init__(self, **kwargs): def __init__(self, **kwargs):
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(self, k, v) setattr(self, k, v)
self.attempted_charsets = set() self.attempted_charsets = set()
request = cherrypy.serving.request request = cherrypy.serving.request
if request.handler is not None: if request.handler is not None:
@@ -56,17 +56,17 @@ class ResponseEncoder:
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
self.oldhandler = request.handler self.oldhandler = request.handler
request.handler = self request.handler = self
def encode_stream(self, encoding): def encode_stream(self, encoding):
"""Encode a streaming response body. """Encode a streaming response body.
Use a generator wrapper, and just pray it works as the stream is Use a generator wrapper, and just pray it works as the stream is
being written out. being written out.
""" """
if encoding in self.attempted_charsets: if encoding in self.attempted_charsets:
return False return False
self.attempted_charsets.add(encoding) self.attempted_charsets.add(encoding)
def encoder(body): def encoder(body):
for chunk in body: for chunk in body:
if isinstance(chunk, unicodestr): if isinstance(chunk, unicodestr):
@@ -74,13 +74,13 @@ class ResponseEncoder:
yield chunk yield chunk
self.body = encoder(self.body) self.body = encoder(self.body)
return True return True
def encode_string(self, encoding): def encode_string(self, encoding):
"""Encode a buffered response body.""" """Encode a buffered response body."""
if encoding in self.attempted_charsets: if encoding in self.attempted_charsets:
return False return False
self.attempted_charsets.add(encoding) self.attempted_charsets.add(encoding)
try: try:
body = [] body = []
for chunk in self.body: for chunk in self.body:
@@ -92,11 +92,11 @@ class ResponseEncoder:
return False return False
else: else:
return True return True
def find_acceptable_charset(self): def find_acceptable_charset(self):
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
if self.debug: if self.debug:
cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
if response.stream: if response.stream:
@@ -115,14 +115,14 @@ class ResponseEncoder:
# >>> len(t.encode("utf7")) # >>> len(t.encode("utf7"))
# 8 # 8
del response.headers["Content-Length"] del response.headers["Content-Length"]
# Parse the Accept-Charset request header, and try to provide one # Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference). # of the requested charsets (in order of user preference).
encs = request.headers.elements('Accept-Charset') encs = request.headers.elements('Accept-Charset')
charsets = [enc.value.lower() for enc in encs] charsets = [enc.value.lower() for enc in encs]
if self.debug: if self.debug:
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
if self.encoding is not None: if self.encoding is not None:
# If specified, force this encoding to be used, or fail. # If specified, force this encoding to be used, or fail.
encoding = self.encoding.lower() encoding = self.encoding.lower()
@@ -160,7 +160,7 @@ class ResponseEncoder:
'0)' % element, 'TOOLS.ENCODE') '0)' % element, 'TOOLS.ENCODE')
if encoder(encoding): if encoder(encoding):
return encoding return encoding
if "*" not in charsets: if "*" not in charsets:
# If no "*" is present in an Accept-Charset field, then all # If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality # character sets not explicitly mentioned get a quality
@@ -173,7 +173,7 @@ class ResponseEncoder:
'TOOLS.ENCODE') 'TOOLS.ENCODE')
if encoder(iso): if encoder(iso):
return iso return iso
# No suitable encoding found. # No suitable encoding found.
ac = request.headers.get('Accept-Charset') ac = request.headers.get('Accept-Charset')
if ac is None: if ac is None:
@@ -182,11 +182,11 @@ class ResponseEncoder:
msg = "Your client sent this Accept-Charset header: %s." % ac msg = "Your client sent this Accept-Charset header: %s." % ac
msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets) msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
raise cherrypy.HTTPError(406, msg) raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
response = cherrypy.serving.response response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs) self.body = self.oldhandler(*args, **kwargs)
if isinstance(self.body, basestring): if isinstance(self.body, basestring):
# strings get wrapped in a list because iterating over a single # strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character # item list is much faster than iterating over every character
@@ -200,7 +200,7 @@ class ResponseEncoder:
self.body = file_generator(self.body) self.body = file_generator(self.body)
elif self.body is None: elif self.body is None:
self.body = [] self.body = []
ct = response.headers.elements("Content-Type") ct = response.headers.elements("Content-Type")
if self.debug: if self.debug:
cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE') cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
@@ -222,7 +222,7 @@ class ResponseEncoder:
if self.debug: if self.debug:
cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE') cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
do_find = True do_find = True
if do_find: if do_find:
# Set "charset=..." param on response Content-Type header # Set "charset=..." param on response Content-Type header
ct.params['charset'] = self.find_acceptable_charset() ct.params['charset'] = self.find_acceptable_charset()
@@ -231,7 +231,7 @@ class ResponseEncoder:
cherrypy.log('Setting Content-Type %s' % ct, cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE') 'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct) response.headers["Content-Type"] = str(ct)
return self.body return self.body
# GZIP # GZIP
@@ -239,7 +239,7 @@ class ResponseEncoder:
def compress(body, compress_level): def compress(body, compress_level):
"""Compress 'body' at the given compress_level.""" """Compress 'body' at the given compress_level."""
import zlib import zlib
# See http://www.gzip.org/zlib/rfc-gzip.html # See http://www.gzip.org/zlib/rfc-gzip.html
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
yield ntob('\x08') # CM: compression method yield ntob('\x08') # CM: compression method
@@ -248,7 +248,7 @@ def compress(body, compress_level):
yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16)) yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
yield ntob('\x02') # XFL: max compression, slowest algo yield ntob('\x02') # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown yield ntob('\xff') # OS: unknown
crc = zlib.crc32(ntob("")) crc = zlib.crc32(ntob(""))
size = 0 size = 0
zobj = zlib.compressobj(compress_level, zobj = zlib.compressobj(compress_level,
@@ -259,7 +259,7 @@ def compress(body, compress_level):
crc = zlib.crc32(line, crc) crc = zlib.crc32(line, crc)
yield zobj.compress(line) yield zobj.compress(line)
yield zobj.flush() yield zobj.flush()
# CRC32: 4 bytes # CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16)) yield struct.pack("<L", crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes # ISIZE: 4 bytes
@@ -267,7 +267,7 @@ def compress(body, compress_level):
def decompress(body): def decompress(body):
import gzip import gzip
zbuf = BytesIO() zbuf = BytesIO()
zbuf.write(body) zbuf.write(body)
zbuf.seek(0) zbuf.seek(0)
@@ -279,7 +279,7 @@ def decompress(body):
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False): def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
"""Try to gzip the response body if Content-Type in mime_types. """Try to gzip the response body if Content-Type in mime_types.
cherrypy.response.headers['Content-Type'] must be set to one of the cherrypy.response.headers['Content-Type'] must be set to one of the
values in the mime_types arg before calling this function. values in the mime_types arg before calling this function.
@@ -287,32 +287,32 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
* type/subtype * type/subtype
* type/* * type/*
* type/*+subtype * type/*+subtype
No compression is performed if any of the following hold: No compression is performed if any of the following hold:
* The client sends no Accept-Encoding request header * The client sends no Accept-Encoding request header
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
* The 'identity' value is given with a qvalue > 0. * The 'identity' value is given with a qvalue > 0.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding") set_vary_header(response, "Accept-Encoding")
if not response.body: if not response.body:
# Response body is empty (might be a 304 for instance) # Response body is empty (might be a 304 for instance)
if debug: if debug:
cherrypy.log('No response body', context='TOOLS.GZIP') cherrypy.log('No response body', context='TOOLS.GZIP')
return return
# If returning cached content (which should already have been gzipped), # If returning cached content (which should already have been gzipped),
# don't re-zip. # don't re-zip.
if getattr(request, "cached", False): if getattr(request, "cached", False):
if debug: if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return return
acceptable = request.headers.elements('Accept-Encoding') acceptable = request.headers.elements('Accept-Encoding')
if not acceptable: if not acceptable:
# If no Accept-Encoding field is present in a request, # If no Accept-Encoding field is present in a request,
@@ -325,7 +325,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
if debug: if debug:
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
return return
ct = response.headers.get('Content-Type', '').split(';')[0] ct = response.headers.get('Content-Type', '').split(';')[0]
for coding in acceptable: for coding in acceptable:
if coding.value == 'identity' and coding.qvalue != 0: if coding.value == 'identity' and coding.qvalue != 0:
@@ -339,7 +339,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Zero gzip qvalue: %s' % coding, cherrypy.log('Zero gzip qvalue: %s' % coding,
context='TOOLS.GZIP') context='TOOLS.GZIP')
return return
if ct not in mime_types: if ct not in mime_types:
# If the list of provided mime-types contains tokens # If the list of provided mime-types contains tokens
# such as 'text/*' or 'application/*+xml', # such as 'text/*' or 'application/*+xml',
@@ -370,7 +370,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Content-Type %s not in mime_types %r' % cherrypy.log('Content-Type %s not in mime_types %r' %
(ct, mime_types), context='TOOLS.GZIP') (ct, mime_types), context='TOOLS.GZIP')
return return
if debug: if debug:
cherrypy.log('Gzipping', context='TOOLS.GZIP') cherrypy.log('Gzipping', context='TOOLS.GZIP')
# Return a generator that compresses the page # Return a generator that compresses the page
@@ -379,9 +379,9 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
if "Content-Length" in response.headers: if "Content-Length" in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers["Content-Length"]
return return
if debug: if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP') cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response() cherrypy.HTTPError(406, "identity, gzip").set_response()
+7 -7
View File
@@ -3,7 +3,7 @@ import inspect
import os import os
import sys import sys
import time import time
try: try:
import objgraph import objgraph
except ImportError: except ImportError:
@@ -103,13 +103,13 @@ def get_instances(cls):
class RequestCounter(SimplePlugin): class RequestCounter(SimplePlugin):
def start(self): def start(self):
self.count = 0 self.count = 0
def before_request(self): def before_request(self):
self.count += 1 self.count += 1
def after_request(self): def after_request(self):
self.count -=1 self.count -=1
request_counter = RequestCounter(cherrypy.engine) request_counter = RequestCounter(cherrypy.engine)
@@ -145,14 +145,14 @@ class GCRoot(object):
def stats(self): def stats(self):
output = ["Statistics:"] output = ["Statistics:"]
for trial in range(10): for trial in range(10):
if request_counter.count > 0: if request_counter.count > 0:
break break
time.sleep(0.5) time.sleep(0.5)
else: else:
output.append("\nNot all requests closed properly.") output.append("\nNot all requests closed properly.")
# gc_collect isn't perfectly synchronous, because it may # gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully # break reference cycles that then take time to fully
# finalize. Call it thrice and hope for the best. # finalize. Call it thrice and hope for the best.
@@ -208,7 +208,7 @@ class GCRoot(object):
t = ReferrerTree(ignore=[objs], maxdepth=3) t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj) tree = t.ascend(obj)
output.extend(t.format(tree)) output.extend(t.format(tree))
return "\n".join(output) return "\n".join(output)
stats.exposed = True stats.exposed = True
+24 -24
View File
@@ -7,7 +7,7 @@ Usage:
First use 'doAuth' to request the client authentication for a First use 'doAuth' to request the client authentication for a
certain resource. You should send an httplib.UNAUTHORIZED response to the certain resource. You should send an httplib.UNAUTHORIZED response to the
client so he knows he has to authenticate itself. client so he knows he has to authenticate itself.
Then use 'parseAuthorization' to retrieve the 'auth_map' used in Then use 'parseAuthorization' to retrieve the 'auth_map' used in
'checkResponse'. 'checkResponse'.
@@ -29,27 +29,27 @@ __license__ = """
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net> Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, * Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, * Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
* Neither the name of Sylvain Hellegouarch nor the names of his contributors * Neither the name of Sylvain Hellegouarch nor the names of his contributors
may be used to endorse or promote products derived from this software may be used to endorse or promote products derived from this software
without specific prior written permission. without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" """
@@ -117,7 +117,7 @@ def doAuth (realm):
"""'doAuth' function returns the challenge string b giving priority over """'doAuth' function returns the challenge string b giving priority over
Digest and fallback to Basic authentication when the browser doesn't Digest and fallback to Basic authentication when the browser doesn't
support the first one. support the first one.
This should be set in the HTTP header under the key 'WWW-Authenticate'.""" This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth (realm) + " " + basicAuth (realm) return digestAuth (realm) + " " + basicAuth (realm)
@@ -187,7 +187,7 @@ def parseAuthorization (credentials):
# #
def md5SessionKey (params, password): def md5SessionKey (params, password):
""" """
If the "algorithm" directive's value is "MD5-sess", then A1 If the "algorithm" directive's value is "MD5-sess", then A1
[the session key] is calculated only once - on the first request by the [the session key] is calculated only once - on the first request by the
client following receipt of a WWW-Authenticate challenge from the server. client following receipt of a WWW-Authenticate challenge from the server.
@@ -332,23 +332,23 @@ AUTH_RESPONSES = {
def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs): def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
"""'checkResponse' compares the auth_map with the password and optionally """'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need. other arguments that each implementation might need.
If the response is of type 'Basic' then the function has the following If the response is of type 'Basic' then the function has the following
signature:: signature::
checkBasicResponse (auth_map, password) -> bool checkBasicResponse (auth_map, password) -> bool
If the response is of type 'Digest' then the function has the following If the response is of type 'Digest' then the function has the following
signature:: signature::
checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
The 'A1' argument is only used in MD5_SESS algorithm based responses. The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info. Check md5SessionKey() for more info.
""" """
checker = AUTH_RESPONSES[auth_map["auth_scheme"]] checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs) return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
+69 -69
View File
@@ -28,7 +28,7 @@ import urllib
def urljoin(*atoms): def urljoin(*atoms):
"""Return the given path \*atoms, joined into a single URL. """Return the given path \*atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
@@ -40,7 +40,7 @@ def urljoin(*atoms):
def urljoin_bytes(*atoms): def urljoin_bytes(*atoms):
"""Return the given path *atoms, joined into a single URL. """Return the given path *atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
@@ -56,18 +56,18 @@ def protocol_from_http(protocol_str):
def get_ranges(headervalue, content_length): def get_ranges(headervalue, content_length):
"""Return a list of (start, stop) indices from a Range header, or None. """Return a list of (start, stop) indices from a Range header, or None.
Each (start, stop) tuple will be composed of two ints, which are suitable Each (start, stop) tuple will be composed of two ints, which are suitable
for use in a slicing operation. That is, the header "Range: bytes=3-6", for use in a slicing operation. That is, the header "Range: bytes=3-6",
if applied against a Python string, is requesting resource[3:7]. This if applied against a Python string, is requesting resource[3:7]. This
function will return the list [(3, 7)]. function will return the list [(3, 7)].
If this function returns an empty list, you should return HTTP 416. If this function returns an empty list, you should return HTTP 416.
""" """
if not headervalue: if not headervalue:
return None return None
result = [] result = []
bytesunit, byteranges = headervalue.split("=", 1) bytesunit, byteranges = headervalue.split("=", 1)
for brange in byteranges.split(","): for brange in byteranges.split(","):
@@ -101,35 +101,35 @@ def get_ranges(headervalue, content_length):
return None return None
# Negative subscript (last N bytes) # Negative subscript (last N bytes)
result.append((content_length - int(stop), content_length)) result.append((content_length - int(stop), content_length))
return result return result
class HeaderElement(object): class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list.""" """An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None): def __init__(self, value, params=None):
self.value = value self.value = value
if params is None: if params is None:
params = {} params = {}
self.params = params self.params = params
def __cmp__(self, other): def __cmp__(self, other):
return cmp(self.value, other.value) return cmp(self.value, other.value)
def __lt__(self, other): def __lt__(self, other):
return self.value < other.value return self.value < other.value
def __str__(self): def __str__(self):
p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)] p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
return "%s%s" % (self.value, "".join(p)) return "%s%s" % (self.value, "".join(p))
def __bytes__(self): def __bytes__(self):
return ntob(self.__str__()) return ntob(self.__str__())
def __unicode__(self): def __unicode__(self):
return ntou(self.__str__()) return ntou(self.__str__())
def parse(elementstr): def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'}).""" """Transform 'token;key=val' to ('token', {'key': 'val'})."""
# Split the element into a value and parameters. The 'value' may # Split the element into a value and parameters. The 'value' may
@@ -150,7 +150,7 @@ class HeaderElement(object):
params[key] = val params[key] = val
return initial_value, params return initial_value, params
parse = staticmethod(parse) parse = staticmethod(parse)
def from_str(cls, elementstr): def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'.""" """Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr) ival, params = cls.parse(elementstr)
@@ -162,14 +162,14 @@ q_separator = re.compile(r'; *q *=')
class AcceptElement(HeaderElement): class AcceptElement(HeaderElement):
"""An element (with parameters) from an Accept* header's element list. """An element (with parameters) from an Accept* header's element list.
AcceptElement objects are comparable; the more-preferred object will be AcceptElement objects are comparable; the more-preferred object will be
"less than" the less-preferred object. They are also therefore sortable; "less than" the less-preferred object. They are also therefore sortable;
if you sort a list of AcceptElement objects, they will be listed in if you sort a list of AcceptElement objects, they will be listed in
priority order; the most preferred value will be first. Yes, it should priority order; the most preferred value will be first. Yes, it should
have been the other way around, but it's too late to fix now. have been the other way around, but it's too late to fix now.
""" """
def from_str(cls, elementstr): def from_str(cls, elementstr):
qvalue = None qvalue = None
# The first "q" parameter (if any) separates the initial # The first "q" parameter (if any) separates the initial
@@ -180,26 +180,26 @@ class AcceptElement(HeaderElement):
# The qvalue for an Accept header can have extensions. The other # The qvalue for an Accept header can have extensions. The other
# headers cannot, but it's easier to parse them as if they did. # headers cannot, but it's easier to parse them as if they did.
qvalue = HeaderElement.from_str(atoms[0].strip()) qvalue = HeaderElement.from_str(atoms[0].strip())
media_type, params = cls.parse(media_range) media_type, params = cls.parse(media_range)
if qvalue is not None: if qvalue is not None:
params["q"] = qvalue params["q"] = qvalue
return cls(media_type, params) return cls(media_type, params)
from_str = classmethod(from_str) from_str = classmethod(from_str)
def qvalue(self): def qvalue(self):
val = self.params.get("q", "1") val = self.params.get("q", "1")
if isinstance(val, HeaderElement): if isinstance(val, HeaderElement):
val = val.value val = val.value
return float(val) return float(val)
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.") qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
def __cmp__(self, other): def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue) diff = cmp(self.qvalue, other.qvalue)
if diff == 0: if diff == 0:
diff = cmp(str(self), str(other)) diff = cmp(str(self), str(other))
return diff return diff
def __lt__(self, other): def __lt__(self, other):
if self.qvalue == other.qvalue: if self.qvalue == other.qvalue:
return str(self) < str(other) return str(self) < str(other)
@@ -211,7 +211,7 @@ def header_elements(fieldname, fieldvalue):
"""Return a sorted HeaderElement list from a comma-separated header string.""" """Return a sorted HeaderElement list from a comma-separated header string."""
if not fieldvalue: if not fieldvalue:
return [] return []
result = [] result = []
for element in fieldvalue.split(","): for element in fieldvalue.split(","):
if fieldname.startswith("Accept") or fieldname == 'TE': if fieldname.startswith("Accept") or fieldname == 'TE':
@@ -219,7 +219,7 @@ def header_elements(fieldname, fieldvalue):
else: else:
hv = HeaderElement.from_str(element) hv = HeaderElement.from_str(element)
result.append(hv) result.append(hv)
return list(reversed(sorted(result))) return list(reversed(sorted(result)))
def decode_TEXT(value): def decode_TEXT(value):
@@ -239,16 +239,16 @@ def decode_TEXT(value):
def valid_status(status): def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message. """Return legal HTTP status Code, Reason-phrase and Message.
The status arg must be an int, or a str that begins with an int. The status arg must be an int, or a str that begins with an int.
If status is an int, or a str and no reason-phrase is supplied, If status is an int, or a str and no reason-phrase is supplied,
a default reason-phrase will be provided. a default reason-phrase will be provided.
""" """
if not status: if not status:
status = 200 status = 200
status = str(status) status = str(status)
parts = status.split(" ", 1) parts = status.split(" ", 1)
if len(parts) == 1: if len(parts) == 1:
@@ -258,26 +258,26 @@ def valid_status(status):
else: else:
code, reason = parts code, reason = parts
reason = reason.strip() reason = reason.strip()
try: try:
code = int(code) code = int(code)
except ValueError: except ValueError:
raise ValueError("Illegal response status from server " raise ValueError("Illegal response status from server "
"(%s is non-numeric)." % repr(code)) "(%s is non-numeric)." % repr(code))
if code < 100 or code > 599: if code < 100 or code > 599:
raise ValueError("Illegal response status from server " raise ValueError("Illegal response status from server "
"(%s is out of range)." % repr(code)) "(%s is out of range)." % repr(code))
if code not in response_codes: if code not in response_codes:
# code is unknown but not illegal # code is unknown but not illegal
default_reason, message = "", "" default_reason, message = "", ""
else: else:
default_reason, message = response_codes[code] default_reason, message = response_codes[code]
if reason is None: if reason is None:
reason = default_reason reason = default_reason
return code, reason, message return code, reason, message
@@ -287,21 +287,21 @@ def valid_status(status):
def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
"""Parse a query given as a string argument. """Parse a query given as a string argument.
Arguments: Arguments:
qs: URL-encoded query string to be parsed qs: URL-encoded query string to be parsed
keep_blank_values: flag indicating whether blank values in keep_blank_values: flag indicating whether blank values in
URL encoded queries should be treated as blank strings. A URL encoded queries should be treated as blank strings. A
true value indicates that blanks should be retained as blank true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included. are to be ignored and treated as if they were not included.
strict_parsing: flag indicating what to do with parsing errors. If strict_parsing: flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true, false (the default), errors are silently ignored. If true,
errors raise a ValueError exception. errors raise a ValueError exception.
Returns a dict, as G-d intended. Returns a dict, as G-d intended.
""" """
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
@@ -334,7 +334,7 @@ image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
"""Build a params dictionary from a query_string. """Build a params dictionary from a query_string.
Duplicate key/value pairs in the provided query_string will be Duplicate key/value pairs in the provided query_string will be
returned as {'key': [val1, val2, ...]}. Single key/values will returned as {'key': [val1, val2, ...]}. Single key/values will
be returned as strings: {'key': 'value'}. be returned as strings: {'key': 'value'}.
@@ -351,40 +351,40 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
class CaseInsensitiveDict(dict): class CaseInsensitiveDict(dict):
"""A case-insensitive dict subclass. """A case-insensitive dict subclass.
Each key is changed on entry to str(key).title(). Each key is changed on entry to str(key).title().
""" """
def __getitem__(self, key): def __getitem__(self, key):
return dict.__getitem__(self, str(key).title()) return dict.__getitem__(self, str(key).title())
def __setitem__(self, key, value): def __setitem__(self, key, value):
dict.__setitem__(self, str(key).title(), value) dict.__setitem__(self, str(key).title(), value)
def __delitem__(self, key): def __delitem__(self, key):
dict.__delitem__(self, str(key).title()) dict.__delitem__(self, str(key).title())
def __contains__(self, key): def __contains__(self, key):
return dict.__contains__(self, str(key).title()) return dict.__contains__(self, str(key).title())
def get(self, key, default=None): def get(self, key, default=None):
return dict.get(self, str(key).title(), default) return dict.get(self, str(key).title(), default)
if hasattr({}, 'has_key'): if hasattr({}, 'has_key'):
def has_key(self, key): def has_key(self, key):
return dict.has_key(self, str(key).title()) return dict.has_key(self, str(key).title())
def update(self, E): def update(self, E):
for k in E.keys(): for k in E.keys():
self[str(k).title()] = E[k] self[str(k).title()] = E[k]
def fromkeys(cls, seq, value=None): def fromkeys(cls, seq, value=None):
newdict = cls() newdict = cls()
for k in seq: for k in seq:
newdict[str(k).title()] = value newdict[str(k).title()] = value
return newdict return newdict
fromkeys = classmethod(fromkeys) fromkeys = classmethod(fromkeys)
def setdefault(self, key, x=None): def setdefault(self, key, x=None):
key = str(key).title() key = str(key).title()
try: try:
@@ -392,7 +392,7 @@ class CaseInsensitiveDict(dict):
except KeyError: except KeyError:
self[key] = x self[key] = x
return x return x
def pop(self, key, default): def pop(self, key, default):
return dict.pop(self, str(key).title(), default) return dict.pop(self, str(key).title(), default)
@@ -412,54 +412,54 @@ else:
class HeaderMap(CaseInsensitiveDict): class HeaderMap(CaseInsensitiveDict):
"""A dict subclass for HTTP request and response headers. """A dict subclass for HTTP request and response headers.
Each key is changed on entry to str(key).title(). This allows headers Each key is changed on entry to str(key).title(). This allows headers
to be case-insensitive and avoid duplicates. to be case-insensitive and avoid duplicates.
Values are header values (decoded according to :rfc:`2047` if necessary). Values are header values (decoded according to :rfc:`2047` if necessary).
""" """
protocol=(1, 1) protocol=(1, 1)
encodings = ["ISO-8859-1"] encodings = ["ISO-8859-1"]
# Someday, when http-bis is done, this will probably get dropped # Someday, when http-bis is done, this will probably get dropped
# since few servers, clients, or intermediaries do it. But until then, # since few servers, clients, or intermediaries do it. But until then,
# we're going to obey the spec as is. # we're going to obey the spec as is.
# "Words of *TEXT MAY contain characters from character sets other than # "Words of *TEXT MAY contain characters from character sets other than
# ISO-8859-1 only when encoded according to the rules of RFC 2047." # ISO-8859-1 only when encoded according to the rules of RFC 2047."
use_rfc_2047 = True use_rfc_2047 = True
def elements(self, key): def elements(self, key):
"""Return a sorted list of HeaderElements for the given header.""" """Return a sorted list of HeaderElements for the given header."""
key = str(key).title() key = str(key).title()
value = self.get(key) value = self.get(key)
return header_elements(key, value) return header_elements(key, value)
def values(self, key): def values(self, key):
"""Return a sorted list of HeaderElement.value for the given header.""" """Return a sorted list of HeaderElement.value for the given header."""
return [e.value for e in self.elements(key)] return [e.value for e in self.elements(key)]
def output(self): def output(self):
"""Transform self into a list of (name, value) tuples.""" """Transform self into a list of (name, value) tuples."""
header_list = [] header_list = []
for k, v in self.items(): for k, v in self.items():
if isinstance(k, unicodestr): if isinstance(k, unicodestr):
k = self.encode(k) k = self.encode(k)
if not isinstance(v, basestring): if not isinstance(v, basestring):
v = str(v) v = str(v)
if isinstance(v, unicodestr): if isinstance(v, unicodestr):
v = self.encode(v) v = self.encode(v)
# See header_translate_* constants above. # See header_translate_* constants above.
# Replace only if you really know what you're doing. # Replace only if you really know what you're doing.
k = k.translate(header_translate_table, header_translate_deletechars) k = k.translate(header_translate_table, header_translate_deletechars)
v = v.translate(header_translate_table, header_translate_deletechars) v = v.translate(header_translate_table, header_translate_deletechars)
header_list.append((k, v)) header_list.append((k, v))
return header_list return header_list
def encode(self, v): def encode(self, v):
"""Return the given header name or value, encoded for HTTP output.""" """Return the given header name or value, encoded for HTTP output."""
for enc in self.encodings: for enc in self.encodings:
@@ -467,16 +467,16 @@ class HeaderMap(CaseInsensitiveDict):
return v.encode(enc) return v.encode(enc)
except UnicodeEncodeError: except UnicodeEncodeError:
continue continue
if self.protocol == (1, 1) and self.use_rfc_2047: if self.protocol == (1, 1) and self.use_rfc_2047:
# Encode RFC-2047 TEXT # Encode RFC-2047 TEXT
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
# We do our own here instead of using the email module # We do our own here instead of using the email module
# because we never want to fold lines--folding has # because we never want to fold lines--folding has
# been deprecated by the HTTP working group. # been deprecated by the HTTP working group.
v = b2a_base64(v.encode('utf-8')) v = b2a_base64(v.encode('utf-8'))
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?=')) return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
raise ValueError("Could not encode header part %r using " raise ValueError("Could not encode header part %r using "
"any of the encodings %r." % "any of the encodings %r." %
(v, self.encodings)) (v, self.encodings))
@@ -484,23 +484,23 @@ class HeaderMap(CaseInsensitiveDict):
class Host(object): class Host(object):
"""An internet address. """An internet address.
name name
Should be the client's host name. If not available (because no DNS Should be the client's host name. If not available (because no DNS
lookup is performed), the IP address should be used instead. lookup is performed), the IP address should be used instead.
""" """
ip = "0.0.0.0" ip = "0.0.0.0"
port = 80 port = 80
name = "unknown.tld" name = "unknown.tld"
def __init__(self, ip, port, name=None): def __init__(self, ip, port, name=None):
self.ip = ip self.ip = ip
self.port = port self.port = port
if name is None: if name is None:
name = ip name = ip
self.name = name self.name = name
def __repr__(self): def __repr__(self):
return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name) return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
+7 -7
View File
@@ -6,7 +6,7 @@ def json_processor(entity):
"""Read application/json data into request.json.""" """Read application/json data into request.json."""
if not entity.headers.get(ntou("Content-Length"), ntou("")): if not entity.headers.get(ntou("Content-Length"), ntou("")):
raise cherrypy.HTTPError(411) raise cherrypy.HTTPError(411)
body = entity.fp.read() body = entity.fp.read()
try: try:
cherrypy.serving.request.json = json_decode(body.decode('utf-8')) cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
@@ -22,11 +22,11 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
be deserialized from JSON to the Python equivalent, and the result be deserialized from JSON to the Python equivalent, and the result
stored at cherrypy.request.json. The 'content_type' argument may stored at cherrypy.request.json. The 'content_type' argument may
be a Content-Type string or a list of allowable Content-Type strings. be a Content-Type string or a list of allowable Content-Type strings.
If the 'force' argument is True (the default), then entities of other If the 'force' argument is True (the default), then entities of other
content types will not be allowed; "415 Unsupported Media Type" is content types will not be allowed; "415 Unsupported Media Type" is
raised instead. raised instead.
Supply your own processor to use a custom decoder, or to handle the parsed Supply your own processor to use a custom decoder, or to handle the parsed
data differently. The processor can be configured via data differently. The processor can be configured via
tools.json_in.processor or via the decorator method. tools.json_in.processor or via the decorator method.
@@ -35,14 +35,14 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request header, or it will raise "411 Length Required". If for any request header, or it will raise "411 Length Required". If for any
other reason the request entity cannot be deserialized from JSON, other reason the request entity cannot be deserialized from JSON,
it will raise "400 Bad Request: Invalid JSON document". it will raise "400 Bad Request: Invalid JSON document".
You must be using Python 2.6 or greater, or have the 'simplejson' You must be using Python 2.6 or greater, or have the 'simplejson'
package importable; otherwise, ValueError is raised during processing. package importable; otherwise, ValueError is raised during processing.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
if isinstance(content_type, basestring): if isinstance(content_type, basestring):
content_type = [content_type] content_type = [content_type]
if force: if force:
if debug: if debug:
cherrypy.log('Removing body processors %s' % cherrypy.log('Removing body processors %s' %
@@ -51,7 +51,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request.body.default_proc = cherrypy.HTTPError( request.body.default_proc = cherrypy.HTTPError(
415, 'Expected an entity of content type %s' % 415, 'Expected an entity of content type %s' %
', '.join(content_type)) ', '.join(content_type))
for ct in content_type: for ct in content_type:
if debug: if debug:
cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
@@ -63,7 +63,7 @@ def json_handler(*args, **kwargs):
def json_out(content_type='application/json', debug=False, handler=json_handler): def json_out(content_type='application/json', debug=False, handler=json_handler):
"""Wrap request.handler to serialize its output to JSON. Sets Content-Type. """Wrap request.handler to serialize its output to JSON. Sets Content-Type.
If the given content_type is None, the Content-Type response header If the given content_type is None, the Content-Type response header
is not set. is not set.
+20 -20
View File
@@ -6,17 +6,17 @@ CherryPy users
You can profile any of your pages as follows:: You can profile any of your pages as follows::
from cherrypy.lib import profiler from cherrypy.lib import profiler
class Root: class Root:
p = profile.Profiler("/path/to/profile/dir") p = profile.Profiler("/path/to/profile/dir")
def index(self): def index(self):
self.p.run(self._index) self.p.run(self._index)
index.exposed = True index.exposed = True
def _index(self): def _index(self):
return "Hello, world!" return "Hello, world!"
cherrypy.tree.mount(Root()) cherrypy.tree.mount(Root())
You can also turn on profiling for all requests You can also turn on profiling for all requests
@@ -58,14 +58,14 @@ from cherrypy._cpcompat import BytesIO
_count = 0 _count = 0
class Profiler(object): class Profiler(object):
def __init__(self, path=None): def __init__(self, path=None):
if not path: if not path:
path = os.path.join(os.path.dirname(__file__), "profile") path = os.path.join(os.path.dirname(__file__), "profile")
self.path = path self.path = path
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
def run(self, func, *args, **params): def run(self, func, *args, **params):
"""Dump profile data into self.path.""" """Dump profile data into self.path."""
global _count global _count
@@ -75,13 +75,13 @@ class Profiler(object):
result = prof.runcall(func, *args, **params) result = prof.runcall(func, *args, **params)
prof.dump_stats(path) prof.dump_stats(path)
return result return result
def statfiles(self): def statfiles(self):
""":rtype: list of available profiles. """:rtype: list of available profiles.
""" """
return [f for f in os.listdir(self.path) return [f for f in os.listdir(self.path)
if f.startswith("cp_") and f.endswith(".prof")] if f.startswith("cp_") and f.endswith(".prof")]
def stats(self, filename, sortby='cumulative'): def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile. """:rtype stats(index): output of print_stats() for the given profile.
""" """
@@ -106,7 +106,7 @@ class Profiler(object):
response = sio.getvalue() response = sio.getvalue()
sio.close() sio.close()
return response return response
def index(self): def index(self):
return """<html> return """<html>
<head><title>CherryPy profile data</title></head> <head><title>CherryPy profile data</title></head>
@@ -117,7 +117,7 @@ class Profiler(object):
</html> </html>
""" """
index.exposed = True index.exposed = True
def menu(self): def menu(self):
yield "<h2>Profiling runs</h2>" yield "<h2>Profiling runs</h2>"
yield "<p>Click on one of the runs below to see profiling data.</p>" yield "<p>Click on one of the runs below to see profiling data.</p>"
@@ -126,7 +126,7 @@ class Profiler(object):
for i in runs: for i in runs:
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i) yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
menu.exposed = True menu.exposed = True
def report(self, filename): def report(self, filename):
import cherrypy import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain' cherrypy.response.headers['Content-Type'] = 'text/plain'
@@ -135,13 +135,13 @@ class Profiler(object):
class ProfileAggregator(Profiler): class ProfileAggregator(Profiler):
def __init__(self, path=None): def __init__(self, path=None):
Profiler.__init__(self, path) Profiler.__init__(self, path)
global _count global _count
self.count = _count = _count + 1 self.count = _count = _count + 1
self.profiler = profile.Profile() self.profiler = profile.Profile()
def run(self, func, *args): def run(self, func, *args):
path = os.path.join(self.path, "cp_%04d.prof" % self.count) path = os.path.join(self.path, "cp_%04d.prof" % self.count)
result = self.profiler.runcall(func, *args) result = self.profiler.runcall(func, *args)
@@ -152,33 +152,33 @@ class ProfileAggregator(Profiler):
class make_app: class make_app:
def __init__(self, nextapp, path=None, aggregate=False): def __init__(self, nextapp, path=None, aggregate=False):
"""Make a WSGI middleware app which wraps 'nextapp' with profiling. """Make a WSGI middleware app which wraps 'nextapp' with profiling.
nextapp nextapp
the WSGI application to wrap, usually an instance of the WSGI application to wrap, usually an instance of
cherrypy.Application. cherrypy.Application.
path path
where to dump the profiling output. where to dump the profiling output.
aggregate aggregate
if True, profile data for all HTTP requests will go in if True, profile data for all HTTP requests will go in
a single file. If False (the default), each HTTP request will a single file. If False (the default), each HTTP request will
dump its profile data into a separate file. dump its profile data into a separate file.
""" """
if profile is None or pstats is None: if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile module. " msg = ("Your installation of Python does not have a profile module. "
"If you're on Debian, try `sudo apt-get install python-profiler`. " "If you're on Debian, try `sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
warnings.warn(msg) warnings.warn(msg)
self.nextapp = nextapp self.nextapp = nextapp
self.aggregate = aggregate self.aggregate = aggregate
if aggregate: if aggregate:
self.profiler = ProfileAggregator(path) self.profiler = ProfileAggregator(path)
else: else:
self.profiler = Profiler(path) self.profiler = Profiler(path)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
def gather(): def gather():
result = [] result = []
@@ -194,7 +194,7 @@ def serve(path=None, port=8080):
"If you're on Debian, try `sudo apt-get install python-profiler`. " "If you're on Debian, try `sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
warnings.warn(msg) warnings.warn(msg)
import cherrypy import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
+65 -65
View File
@@ -55,25 +55,25 @@ def as_dict(config):
class NamespaceSet(dict): class NamespaceSet(dict):
"""A dict of config namespace names and handlers. """A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding Each config entry should begin with a namespace name; the corresponding
namespace handler will be called once for each config entry in that namespace handler will be called once for each config entry in that
namespace, and will be passed two arguments: the config key (with the namespace, and will be passed two arguments: the config key (with the
namespace removed) and the config value. namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be Namespace handlers may be any Python callable; they may also be
Python 2.5-style 'context managers', in which case their __enter__ Python 2.5-style 'context managers', in which case their __enter__
method should return a callable to be used as the handler. method should return a callable to be used as the handler.
See cherrypy.tools (the Toolbox class) for an example. See cherrypy.tools (the Toolbox class) for an example.
""" """
def __call__(self, config): def __call__(self, config):
"""Iterate through config and pass it to each namespace handler. """Iterate through config and pass it to each namespace handler.
config config
A flat dict, where keys use dots to separate A flat dict, where keys use dots to separate
namespaces, and values are arbitrary. namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding The first name in each config key is used to look up the corresponding
namespace handler. For example, a config entry of {'tools.gzip.on': v} namespace handler. For example, a config entry of {'tools.gzip.on': v}
will call the 'tools' namespace handler with the args: ('gzip.on', v) will call the 'tools' namespace handler with the args: ('gzip.on', v)
@@ -85,7 +85,7 @@ class NamespaceSet(dict):
ns, name = k.split(".", 1) ns, name = k.split(".", 1)
bucket = ns_confs.setdefault(ns, {}) bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k] bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be # I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement: # rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems(): # for ns, handler in self.iteritems():
@@ -116,11 +116,11 @@ class NamespaceSet(dict):
else: else:
for k, v in ns_confs.get(ns, {}).items(): for k, v in ns_confs.get(ns, {}).items():
handler(k, v) handler(k, v)
def __repr__(self): def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
dict.__repr__(self)) dict.__repr__(self))
def __copy__(self): def __copy__(self):
newobj = self.__class__() newobj = self.__class__()
newobj.update(self) newobj.update(self)
@@ -130,26 +130,26 @@ class NamespaceSet(dict):
class Config(dict): class Config(dict):
"""A dict-like set of configuration data, with defaults and namespaces. """A dict-like set of configuration data, with defaults and namespaces.
May take a file, filename, or dict. May take a file, filename, or dict.
""" """
defaults = {} defaults = {}
environments = {} environments = {}
namespaces = NamespaceSet() namespaces = NamespaceSet()
def __init__(self, file=None, **kwargs): def __init__(self, file=None, **kwargs):
self.reset() self.reset()
if file is not None: if file is not None:
self.update(file) self.update(file)
if kwargs: if kwargs:
self.update(kwargs) self.update(kwargs)
def reset(self): def reset(self):
"""Reset self to default values.""" """Reset self to default values."""
self.clear() self.clear()
dict.update(self, self.defaults) dict.update(self, self.defaults)
def update(self, config): def update(self, config):
"""Update self from a dict, file or filename.""" """Update self from a dict, file or filename."""
if isinstance(config, basestring): if isinstance(config, basestring):
@@ -161,7 +161,7 @@ class Config(dict):
else: else:
config = config.copy() config = config.copy()
self._apply(config) self._apply(config)
def _apply(self, config): def _apply(self, config):
"""Update self from a dict.""" """Update self from a dict."""
which_env = config.get('environment') which_env = config.get('environment')
@@ -170,23 +170,23 @@ class Config(dict):
for k in env: for k in env:
if k not in config: if k not in config:
config[k] = env[k] config[k] = env[k]
dict.update(self, config) dict.update(self, config)
self.namespaces(config) self.namespaces(config)
def __setitem__(self, k, v): def __setitem__(self, k, v):
dict.__setitem__(self, k, v) dict.__setitem__(self, k, v)
self.namespaces({k: v}) self.namespaces({k: v})
class Parser(ConfigParser): class Parser(ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that """Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read. raises an exception if the file cannot be read.
""" """
def optionxform(self, optionstr): def optionxform(self, optionstr):
return optionstr return optionstr
def read(self, filenames): def read(self, filenames):
if isinstance(filenames, basestring): if isinstance(filenames, basestring):
filenames = [filenames] filenames = [filenames]
@@ -200,7 +200,7 @@ class Parser(ConfigParser):
self._read(fp, filename) self._read(fp, filename)
finally: finally:
fp.close() fp.close()
def as_dict(self, raw=False, vars=None): def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary""" """Convert an INI file to a dictionary"""
# Load INI file into a dict # Load INI file into a dict
@@ -220,7 +220,7 @@ class Parser(ConfigParser):
raise ValueError(msg, x.__class__.__name__, x.args) raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value result[section][option] = value
return result return result
def dict_from_file(self, file): def dict_from_file(self, file):
if hasattr(file, 'read'): if hasattr(file, 'read'):
self.readfp(file) self.readfp(file)
@@ -233,14 +233,14 @@ class Parser(ConfigParser):
class _Builder2: class _Builder2:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError("unrepr does not recognize %s" %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
def astnode(self, s): def astnode(self, s):
"""Return a Python2 ast Node compiled from a string.""" """Return a Python2 ast Node compiled from a string."""
try: try:
@@ -249,16 +249,16 @@ class _Builder2:
# Fallback to eval when compiler package is not available, # Fallback to eval when compiler package is not available,
# e.g. IronPython 1.0. # e.g. IronPython 1.0.
return eval(s) return eval(s)
p = compiler.parse("__tempvalue__ = " + s) p = compiler.parse("__tempvalue__ = " + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1] return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o): def build_Subscript(self, o):
expr, flags, subs = o.getChildren() expr, flags, subs = o.getChildren()
expr = self.build(expr) expr = self.build(expr)
subs = self.build(subs) subs = self.build(subs)
return expr[subs] return expr[subs]
def build_CallFunc(self, o): def build_CallFunc(self, o):
children = map(self.build, o.getChildren()) children = map(self.build, o.getChildren())
callee = children.pop(0) callee = children.pop(0)
@@ -266,23 +266,23 @@ class _Builder2:
starargs = children.pop() or () starargs = children.pop() or ()
args = tuple(children) + tuple(starargs) args = tuple(children) + tuple(starargs)
return callee(*args, **kwargs) return callee(*args, **kwargs)
def build_List(self, o): def build_List(self, o):
return map(self.build, o.getChildren()) return map(self.build, o.getChildren())
def build_Const(self, o): def build_Const(self, o):
return o.value return o.value
def build_Dict(self, o): def build_Dict(self, o):
d = {} d = {}
i = iter(map(self.build, o.getChildren())) i = iter(map(self.build, o.getChildren()))
for el in i: for el in i:
d[el] = i.next() d[el] = i.next()
return d return d
def build_Tuple(self, o): def build_Tuple(self, o):
return tuple(self.build_List(o)) return tuple(self.build_List(o))
def build_Name(self, o): def build_Name(self, o):
name = o.name name = o.name
if name == 'None': if name == 'None':
@@ -291,21 +291,21 @@ class _Builder2:
return True return True
if name == 'False': if name == 'False':
return False return False
# See if the Name is a package or module. If it is, import it. # See if the Name is a package or module. If it is, import it.
try: try:
return modules(name) return modules(name)
except ImportError: except ImportError:
pass pass
# See if the Name is in builtins. # See if the Name is in builtins.
try: try:
return getattr(builtins, name) return getattr(builtins, name)
except AttributeError: except AttributeError:
pass pass
raise TypeError("unrepr could not resolve the name %s" % repr(name)) raise TypeError("unrepr could not resolve the name %s" % repr(name))
def build_Add(self, o): def build_Add(self, o):
left, right = map(self.build, o.getChildren()) left, right = map(self.build, o.getChildren())
return left + right return left + right
@@ -313,30 +313,30 @@ class _Builder2:
def build_Mul(self, o): def build_Mul(self, o):
left, right = map(self.build, o.getChildren()) left, right = map(self.build, o.getChildren())
return left * right return left * right
def build_Getattr(self, o): def build_Getattr(self, o):
parent = self.build(o.expr) parent = self.build(o.expr)
return getattr(parent, o.attrname) return getattr(parent, o.attrname)
def build_NoneType(self, o): def build_NoneType(self, o):
return None return None
def build_UnarySub(self, o): def build_UnarySub(self, o):
return -self.build(o.getChildren()[0]) return -self.build(o.getChildren()[0])
def build_UnaryAdd(self, o): def build_UnaryAdd(self, o):
return self.build(o.getChildren()[0]) return self.build(o.getChildren()[0])
class _Builder3: class _Builder3:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError("unrepr does not recognize %s" %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
def astnode(self, s): def astnode(self, s):
"""Return a Python3 ast Node compiled from a string.""" """Return a Python3 ast Node compiled from a string."""
try: try:
@@ -351,46 +351,46 @@ class _Builder3:
def build_Subscript(self, o): def build_Subscript(self, o):
return self.build(o.value)[self.build(o.slice)] return self.build(o.value)[self.build(o.slice)]
def build_Index(self, o): def build_Index(self, o):
return self.build(o.value) return self.build(o.value)
def build_Call(self, o): def build_Call(self, o):
callee = self.build(o.func) callee = self.build(o.func)
if o.args is None: if o.args is None:
args = () args = ()
else: else:
args = tuple([self.build(a) for a in o.args]) args = tuple([self.build(a) for a in o.args])
if o.starargs is None: if o.starargs is None:
starargs = () starargs = ()
else: else:
starargs = self.build(o.starargs) starargs = self.build(o.starargs)
if o.kwargs is None: if o.kwargs is None:
kwargs = {} kwargs = {}
else: else:
kwargs = self.build(o.kwargs) kwargs = self.build(o.kwargs)
return callee(*(args + starargs), **kwargs) return callee(*(args + starargs), **kwargs)
def build_List(self, o): def build_List(self, o):
return list(map(self.build, o.elts)) return list(map(self.build, o.elts))
def build_Str(self, o): def build_Str(self, o):
return o.s return o.s
def build_Num(self, o): def build_Num(self, o):
return o.n return o.n
def build_Dict(self, o): def build_Dict(self, o):
return dict([(self.build(k), self.build(v)) return dict([(self.build(k), self.build(v))
for k, v in zip(o.keys, o.values)]) for k, v in zip(o.keys, o.values)])
def build_Tuple(self, o): def build_Tuple(self, o):
return tuple(self.build_List(o)) return tuple(self.build_List(o))
def build_Name(self, o): def build_Name(self, o):
name = o.id name = o.id
if name == 'None': if name == 'None':
@@ -399,28 +399,28 @@ class _Builder3:
return True return True
if name == 'False': if name == 'False':
return False return False
# See if the Name is a package or module. If it is, import it. # See if the Name is a package or module. If it is, import it.
try: try:
return modules(name) return modules(name)
except ImportError: except ImportError:
pass pass
# See if the Name is in builtins. # See if the Name is in builtins.
try: try:
import builtins import builtins
return getattr(builtins, name) return getattr(builtins, name)
except AttributeError: except AttributeError:
pass pass
raise TypeError("unrepr could not resolve the name %s" % repr(name)) raise TypeError("unrepr could not resolve the name %s" % repr(name))
def build_UnaryOp(self, o): def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand]) op, operand = map(self.build, [o.op, o.operand])
return op(operand) return op(operand)
def build_BinOp(self, o): def build_BinOp(self, o):
left, op, right = map(self.build, [o.left, o.op, o.right]) left, op, right = map(self.build, [o.left, o.op, o.right])
return op(left, right) return op(left, right)
def build_Add(self, o): def build_Add(self, o):
@@ -428,7 +428,7 @@ class _Builder3:
def build_Mult(self, o): def build_Mult(self, o):
return _operator.mul return _operator.mul
def build_USub(self, o): def build_USub(self, o):
return _operator.neg return _operator.neg
@@ -465,12 +465,12 @@ def modules(modulePath):
def attributes(full_attribute_name): def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module.""" """Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute # Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind(".") last_dot = full_attribute_name.rfind(".")
attr_name = full_attribute_name[last_dot + 1:] attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot] mod_path = full_attribute_name[:last_dot]
mod = modules(mod_path) mod = modules(mod_path)
# Let an AttributeError propagate outward. # Let an AttributeError propagate outward.
try: try:
@@ -478,7 +478,7 @@ def attributes(full_attribute_name):
except AttributeError: except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'" raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name)) % (mod_path, attr_name))
# Return a reference to the attribute. # Return a reference to the attribute.
return attr return attr
+119 -119
View File
@@ -101,12 +101,12 @@ missing = object()
class Session(object): class Session(object):
"""A CherryPy dict-like Session object (one per request).""" """A CherryPy dict-like Session object (one per request)."""
_id = None _id = None
id_observers = None id_observers = None
"A list of callbacks to which to pass new id's." "A list of callbacks to which to pass new id's."
def _get_id(self): def _get_id(self):
return self._id return self._id
def _set_id(self, value): def _set_id(self, value):
@@ -114,46 +114,46 @@ class Session(object):
for o in self.id_observers: for o in self.id_observers:
o(value) o(value)
id = property(_get_id, _set_id, doc="The current session ID.") id = property(_get_id, _set_id, doc="The current session ID.")
timeout = 60 timeout = 60
"Number of minutes after which to delete session data." "Number of minutes after which to delete session data."
locked = False locked = False
""" """
If True, this session instance has exclusive read/write access If True, this session instance has exclusive read/write access
to session data.""" to session data."""
loaded = False loaded = False
""" """
If True, data has been retrieved from storage. This should happen If True, data has been retrieved from storage. This should happen
automatically on the first attempt to access session data.""" automatically on the first attempt to access session data."""
clean_thread = None clean_thread = None
"Class-level Monitor which calls self.clean_up." "Class-level Monitor which calls self.clean_up."
clean_freq = 5 clean_freq = 5
"The poll rate for expired session cleanup in minutes." "The poll rate for expired session cleanup in minutes."
originalid = None originalid = None
"The session id passed by the client. May be missing or unsafe." "The session id passed by the client. May be missing or unsafe."
missing = False missing = False
"True if the session requested by the client did not exist." "True if the session requested by the client did not exist."
regenerated = False regenerated = False
""" """
True if the application called session.regenerate(). This is not set by True if the application called session.regenerate(). This is not set by
internal calls to regenerate the session id.""" internal calls to regenerate the session id."""
debug=False debug=False
def __init__(self, id=None, **kwargs): def __init__(self, id=None, **kwargs):
self.id_observers = [] self.id_observers = []
self._data = {} self._data = {}
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(self, k, v) setattr(self, k, v)
self.originalid = id self.originalid = id
self.missing = False self.missing = False
if id is None: if id is None:
@@ -184,33 +184,33 @@ class Session(object):
"""Replace the current session (with a new id).""" """Replace the current session (with a new id)."""
self.regenerated = True self.regenerated = True
self._regenerate() self._regenerate()
def _regenerate(self): def _regenerate(self):
if self.id is not None: if self.id is not None:
self.delete() self.delete()
old_session_was_locked = self.locked old_session_was_locked = self.locked
if old_session_was_locked: if old_session_was_locked:
self.release_lock() self.release_lock()
self.id = None self.id = None
while self.id is None: while self.id is None:
self.id = self.generate_id() self.id = self.generate_id()
# Assert that the generated id is not already stored. # Assert that the generated id is not already stored.
if self._exists(): if self._exists():
self.id = None self.id = None
if old_session_was_locked: if old_session_was_locked:
self.acquire_lock() self.acquire_lock()
def clean_up(self): def clean_up(self):
"""Clean up expired sessions.""" """Clean up expired sessions."""
pass pass
def generate_id(self): def generate_id(self):
"""Return a new session id.""" """Return a new session id."""
return random20() return random20()
def save(self): def save(self):
"""Save session data.""" """Save session data."""
try: try:
@@ -223,12 +223,12 @@ class Session(object):
cherrypy.log('Saving with expiry %s' % expiration_time, cherrypy.log('Saving with expiry %s' % expiration_time,
'TOOLS.SESSIONS') 'TOOLS.SESSIONS')
self._save(expiration_time) self._save(expiration_time)
finally: finally:
if self.locked: if self.locked:
# Always release the lock if the user didn't release it # Always release the lock if the user didn't release it
self.release_lock() self.release_lock()
def load(self): def load(self):
"""Copy stored session data into this session instance.""" """Copy stored session data into this session instance."""
data = self._load() data = self._load()
@@ -240,7 +240,7 @@ class Session(object):
else: else:
self._data = data[0] self._data = data[0]
self.loaded = True self.loaded = True
# Stick the clean_thread in the class, not the instance. # Stick the clean_thread in the class, not the instance.
# The instances are created and destroyed per-request. # The instances are created and destroyed per-request.
cls = self.__class__ cls = self.__class__
@@ -253,23 +253,23 @@ class Session(object):
t.subscribe() t.subscribe()
cls.clean_thread = t cls.clean_thread = t
t.start() t.start()
def delete(self): def delete(self):
"""Delete stored session data.""" """Delete stored session data."""
self._delete() self._delete()
def __getitem__(self, key): def __getitem__(self, key):
if not self.loaded: self.load() if not self.loaded: self.load()
return self._data[key] return self._data[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
if not self.loaded: self.load() if not self.loaded: self.load()
self._data[key] = value self._data[key] = value
def __delitem__(self, key): def __delitem__(self, key):
if not self.loaded: self.load() if not self.loaded: self.load()
del self._data[key] del self._data[key]
def pop(self, key, default=missing): def pop(self, key, default=missing):
"""Remove the specified key and return the corresponding value. """Remove the specified key and return the corresponding value.
If key is not found, default is returned if given, If key is not found, default is returned if given,
@@ -280,47 +280,47 @@ class Session(object):
return self._data.pop(key) return self._data.pop(key)
else: else:
return self._data.pop(key, default) return self._data.pop(key, default)
def __contains__(self, key): def __contains__(self, key):
if not self.loaded: self.load() if not self.loaded: self.load()
return key in self._data return key in self._data
if hasattr({}, 'has_key'): if hasattr({}, 'has_key'):
def has_key(self, key): def has_key(self, key):
"""D.has_key(k) -> True if D has a key k, else False.""" """D.has_key(k) -> True if D has a key k, else False."""
if not self.loaded: self.load() if not self.loaded: self.load()
return key in self._data return key in self._data
def get(self, key, default=None): def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
if not self.loaded: self.load() if not self.loaded: self.load()
return self._data.get(key, default) return self._data.get(key, default)
def update(self, d): def update(self, d):
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
if not self.loaded: self.load() if not self.loaded: self.load()
self._data.update(d) self._data.update(d)
def setdefault(self, key, default=None): def setdefault(self, key, default=None):
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
if not self.loaded: self.load() if not self.loaded: self.load()
return self._data.setdefault(key, default) return self._data.setdefault(key, default)
def clear(self): def clear(self):
"""D.clear() -> None. Remove all items from D.""" """D.clear() -> None. Remove all items from D."""
if not self.loaded: self.load() if not self.loaded: self.load()
self._data.clear() self._data.clear()
def keys(self): def keys(self):
"""D.keys() -> list of D's keys.""" """D.keys() -> list of D's keys."""
if not self.loaded: self.load() if not self.loaded: self.load()
return self._data.keys() return self._data.keys()
def items(self): def items(self):
"""D.items() -> list of D's (key, value) pairs, as 2-tuples.""" """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
if not self.loaded: self.load() if not self.loaded: self.load()
return self._data.items() return self._data.items()
def values(self): def values(self):
"""D.values() -> list of D's values.""" """D.values() -> list of D's values."""
if not self.loaded: self.load() if not self.loaded: self.load()
@@ -328,11 +328,11 @@ class Session(object):
class RamSession(Session): class RamSession(Session):
# Class-level objects. Don't rebind these! # Class-level objects. Don't rebind these!
cache = {} cache = {}
locks = {} locks = {}
def clean_up(self): def clean_up(self):
"""Clean up expired sessions.""" """Clean up expired sessions."""
now = self.now() now = self.now()
@@ -346,34 +346,34 @@ class RamSession(Session):
del self.locks[id] del self.locks[id]
except KeyError: except KeyError:
pass pass
# added to remove obsolete lock objects # added to remove obsolete lock objects
for id in list(self.locks): for id in list(self.locks):
if id not in self.cache: if id not in self.cache:
self.locks.pop(id, None) self.locks.pop(id, None)
def _exists(self): def _exists(self):
return self.id in self.cache return self.id in self.cache
def _load(self): def _load(self):
return self.cache.get(self.id) return self.cache.get(self.id)
def _save(self, expiration_time): def _save(self, expiration_time):
self.cache[self.id] = (self._data, expiration_time) self.cache[self.id] = (self._data, expiration_time)
def _delete(self): def _delete(self):
self.cache.pop(self.id, None) self.cache.pop(self.id, None)
def acquire_lock(self): def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data.""" """Acquire an exclusive lock on the currently-loaded session data."""
self.locked = True self.locked = True
self.locks.setdefault(self.id, threading.RLock()).acquire() self.locks.setdefault(self.id, threading.RLock()).acquire()
def release_lock(self): def release_lock(self):
"""Release the lock on the currently-loaded session data.""" """Release the lock on the currently-loaded session data."""
self.locks[self.id].release() self.locks[self.id].release()
self.locked = False self.locked = False
def __len__(self): def __len__(self):
"""Return the number of active sessions.""" """Return the number of active sessions."""
return len(self.cache) return len(self.cache)
@@ -381,35 +381,35 @@ class RamSession(Session):
class FileSession(Session): class FileSession(Session):
"""Implementation of the File backend for sessions """Implementation of the File backend for sessions
storage_path storage_path
The folder where session data will be saved. Each session The folder where session data will be saved. Each session
will be saved as pickle.dump(data, expiration_time) in its own file; will be saved as pickle.dump(data, expiration_time) in its own file;
the filename will be self.SESSION_PREFIX + self.id. the filename will be self.SESSION_PREFIX + self.id.
""" """
SESSION_PREFIX = 'session-' SESSION_PREFIX = 'session-'
LOCK_SUFFIX = '.lock' LOCK_SUFFIX = '.lock'
pickle_protocol = pickle.HIGHEST_PROTOCOL pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs): def __init__(self, id=None, **kwargs):
# The 'storage_path' arg is required for file-based sessions. # The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
Session.__init__(self, id=id, **kwargs) Session.__init__(self, id=id, **kwargs)
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for file-based sessions. """Set up the storage system for file-based sessions.
This should only be called once per process; this will be done This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does). automatically when using sessions.init (as the built-in Tool does).
""" """
# The 'storage_path' arg is required for file-based sessions. # The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)
# Warn if any lock files exist at startup. # Warn if any lock files exist at startup.
lockfiles = [fname for fname in os.listdir(cls.storage_path) lockfiles = [fname for fname in os.listdir(cls.storage_path)
if (fname.startswith(cls.SESSION_PREFIX) if (fname.startswith(cls.SESSION_PREFIX)
@@ -421,17 +421,17 @@ class FileSession(Session):
"manually delete the lockfiles found at %r." "manually delete the lockfiles found at %r."
% (len(lockfiles), plural, cls.storage_path)) % (len(lockfiles), plural, cls.storage_path))
setup = classmethod(setup) setup = classmethod(setup)
def _get_file_path(self): def _get_file_path(self):
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
if not os.path.abspath(f).startswith(self.storage_path): if not os.path.abspath(f).startswith(self.storage_path):
raise cherrypy.HTTPError(400, "Invalid session id in cookie.") raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
return f return f
def _exists(self): def _exists(self):
path = self._get_file_path() path = self._get_file_path()
return os.path.exists(path) return os.path.exists(path)
def _load(self, path=None): def _load(self, path=None):
if path is None: if path is None:
path = self._get_file_path() path = self._get_file_path()
@@ -443,20 +443,20 @@ class FileSession(Session):
f.close() f.close()
except (IOError, EOFError): except (IOError, EOFError):
return None return None
def _save(self, expiration_time): def _save(self, expiration_time):
f = open(self._get_file_path(), "wb") f = open(self._get_file_path(), "wb")
try: try:
pickle.dump((self._data, expiration_time), f, self.pickle_protocol) pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
finally: finally:
f.close() f.close()
def _delete(self): def _delete(self):
try: try:
os.unlink(self._get_file_path()) os.unlink(self._get_file_path())
except OSError: except OSError:
pass pass
def acquire_lock(self, path=None): def acquire_lock(self, path=None):
"""Acquire an exclusive lock on the currently-loaded session data.""" """Acquire an exclusive lock on the currently-loaded session data."""
if path is None: if path is None:
@@ -468,17 +468,17 @@ class FileSession(Session):
except OSError: except OSError:
time.sleep(0.1) time.sleep(0.1)
else: else:
os.close(lockfd) os.close(lockfd)
break break
self.locked = True self.locked = True
def release_lock(self, path=None): def release_lock(self, path=None):
"""Release the lock on the currently-loaded session data.""" """Release the lock on the currently-loaded session data."""
if path is None: if path is None:
path = self._get_file_path() path = self._get_file_path()
os.unlink(path + self.LOCK_SUFFIX) os.unlink(path + self.LOCK_SUFFIX)
self.locked = False self.locked = False
def clean_up(self): def clean_up(self):
"""Clean up expired sessions.""" """Clean up expired sessions."""
now = self.now() now = self.now()
@@ -500,7 +500,7 @@ class FileSession(Session):
os.unlink(path) os.unlink(path)
finally: finally:
self.release_lock(path) self.release_lock(path)
def __len__(self): def __len__(self):
"""Return the number of active sessions.""" """Return the number of active sessions."""
return len([fname for fname in os.listdir(self.storage_path) return len([fname for fname in os.listdir(self.storage_path)
@@ -517,40 +517,40 @@ class PostgresqlSession(Session):
data text, data text,
expiration_time timestamp expiration_time timestamp
) )
You must provide your own get_db function. You must provide your own get_db function.
""" """
pickle_protocol = pickle.HIGHEST_PROTOCOL pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs): def __init__(self, id=None, **kwargs):
Session.__init__(self, id, **kwargs) Session.__init__(self, id, **kwargs)
self.cursor = self.db.cursor() self.cursor = self.db.cursor()
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for Postgres-based sessions. """Set up the storage system for Postgres-based sessions.
This should only be called once per process; this will be done This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does). automatically when using sessions.init (as the built-in Tool does).
""" """
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)
self.db = self.get_db() self.db = self.get_db()
setup = classmethod(setup) setup = classmethod(setup)
def __del__(self): def __del__(self):
if self.cursor: if self.cursor:
self.cursor.close() self.cursor.close()
self.db.commit() self.db.commit()
def _exists(self): def _exists(self):
# Select session data from table # Select session data from table
self.cursor.execute('select data, expiration_time from session ' self.cursor.execute('select data, expiration_time from session '
'where id=%s', (self.id,)) 'where id=%s', (self.id,))
rows = self.cursor.fetchall() rows = self.cursor.fetchall()
return bool(rows) return bool(rows)
def _load(self): def _load(self):
# Select session data from table # Select session data from table
self.cursor.execute('select data, expiration_time from session ' self.cursor.execute('select data, expiration_time from session '
@@ -558,34 +558,34 @@ class PostgresqlSession(Session):
rows = self.cursor.fetchall() rows = self.cursor.fetchall()
if not rows: if not rows:
return None return None
pickled_data, expiration_time = rows[0] pickled_data, expiration_time = rows[0]
data = pickle.loads(pickled_data) data = pickle.loads(pickled_data)
return data, expiration_time return data, expiration_time
def _save(self, expiration_time): def _save(self, expiration_time):
pickled_data = pickle.dumps(self._data, self.pickle_protocol) pickled_data = pickle.dumps(self._data, self.pickle_protocol)
self.cursor.execute('update session set data = %s, ' self.cursor.execute('update session set data = %s, '
'expiration_time = %s where id = %s', 'expiration_time = %s where id = %s',
(pickled_data, expiration_time, self.id)) (pickled_data, expiration_time, self.id))
def _delete(self): def _delete(self):
self.cursor.execute('delete from session where id=%s', (self.id,)) self.cursor.execute('delete from session where id=%s', (self.id,))
def acquire_lock(self): def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data.""" """Acquire an exclusive lock on the currently-loaded session data."""
# We use the "for update" clause to lock the row # We use the "for update" clause to lock the row
self.locked = True self.locked = True
self.cursor.execute('select id from session where id=%s for update', self.cursor.execute('select id from session where id=%s for update',
(self.id,)) (self.id,))
def release_lock(self): def release_lock(self):
"""Release the lock on the currently-loaded session data.""" """Release the lock on the currently-loaded session data."""
# We just close the cursor and that will remove the lock # We just close the cursor and that will remove the lock
# introduced by the "for update" clause # introduced by the "for update" clause
self.cursor.close() self.cursor.close()
self.locked = False self.locked = False
def clean_up(self): def clean_up(self):
"""Clean up expired sessions.""" """Clean up expired sessions."""
self.cursor.execute('delete from session where expiration_time < %s', self.cursor.execute('delete from session where expiration_time < %s',
@@ -593,29 +593,29 @@ class PostgresqlSession(Session):
class MemcachedSession(Session): class MemcachedSession(Session):
# The most popular memcached client for Python isn't thread-safe. # The most popular memcached client for Python isn't thread-safe.
# Wrap all .get and .set operations in a single lock. # Wrap all .get and .set operations in a single lock.
mc_lock = threading.RLock() mc_lock = threading.RLock()
# This is a seperate set of locks per session id. # This is a seperate set of locks per session id.
locks = {} locks = {}
servers = ['127.0.0.1:11211'] servers = ['127.0.0.1:11211']
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for memcached-based sessions. """Set up the storage system for memcached-based sessions.
This should only be called once per process; this will be done This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does). automatically when using sessions.init (as the built-in Tool does).
""" """
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)
import memcache import memcache
cls.cache = memcache.Client(cls.servers) cls.cache = memcache.Client(cls.servers)
setup = classmethod(setup) setup = classmethod(setup)
def _get_id(self): def _get_id(self):
return self._id return self._id
def _set_id(self, value): def _set_id(self, value):
@@ -628,21 +628,21 @@ class MemcachedSession(Session):
for o in self.id_observers: for o in self.id_observers:
o(value) o(value)
id = property(_get_id, _set_id, doc="The current session ID.") id = property(_get_id, _set_id, doc="The current session ID.")
def _exists(self): def _exists(self):
self.mc_lock.acquire() self.mc_lock.acquire()
try: try:
return bool(self.cache.get(self.id)) return bool(self.cache.get(self.id))
finally: finally:
self.mc_lock.release() self.mc_lock.release()
def _load(self): def _load(self):
self.mc_lock.acquire() self.mc_lock.acquire()
try: try:
return self.cache.get(self.id) return self.cache.get(self.id)
finally: finally:
self.mc_lock.release() self.mc_lock.release()
def _save(self, expiration_time): def _save(self, expiration_time):
# Send the expiration time as "Unix time" (seconds since 1/1/1970) # Send the expiration time as "Unix time" (seconds since 1/1/1970)
td = int(time.mktime(expiration_time.timetuple())) td = int(time.mktime(expiration_time.timetuple()))
@@ -652,20 +652,20 @@ class MemcachedSession(Session):
raise AssertionError("Session data for id %r not set." % self.id) raise AssertionError("Session data for id %r not set." % self.id)
finally: finally:
self.mc_lock.release() self.mc_lock.release()
def _delete(self): def _delete(self):
self.cache.delete(self.id) self.cache.delete(self.id)
def acquire_lock(self): def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data.""" """Acquire an exclusive lock on the currently-loaded session data."""
self.locked = True self.locked = True
self.locks.setdefault(self.id, threading.RLock()).acquire() self.locks.setdefault(self.id, threading.RLock()).acquire()
def release_lock(self): def release_lock(self):
"""Release the lock on the currently-loaded session data.""" """Release the lock on the currently-loaded session data."""
self.locks[self.id].release() self.locks[self.id].release()
self.locked = False self.locked = False
def __len__(self): def __len__(self):
"""Return the number of active sessions.""" """Return the number of active sessions."""
raise NotImplementedError raise NotImplementedError
@@ -675,17 +675,17 @@ class MemcachedSession(Session):
def save(): def save():
"""Save any changed session data.""" """Save any changed session data."""
if not hasattr(cherrypy.serving, "session"): if not hasattr(cherrypy.serving, "session"):
return return
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against running twice # Guard against running twice
if hasattr(request, "_sessionsaved"): if hasattr(request, "_sessionsaved"):
return return
request._sessionsaved = True request._sessionsaved = True
if response.stream: if response.stream:
# If the body is being streamed, we have to save the data # If the body is being streamed, we have to save the data
# *after* the response has been written out # *after* the response has been written out
@@ -712,59 +712,59 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
timeout=60, domain=None, secure=False, clean_freq=5, timeout=60, domain=None, secure=False, clean_freq=5,
persistent=True, httponly=False, debug=False, **kwargs): persistent=True, httponly=False, debug=False, **kwargs):
"""Initialize session object (using cookies). """Initialize session object (using cookies).
storage_type storage_type
One of 'ram', 'file', 'postgresql', 'memcached'. This will be One of 'ram', 'file', 'postgresql', 'memcached'. This will be
used to look up the corresponding class in cherrypy.lib.sessions used to look up the corresponding class in cherrypy.lib.sessions
globals. For example, 'file' will use the FileSession class. globals. For example, 'file' will use the FileSession class.
path path
The 'path' value to stick in the response cookie metadata. The 'path' value to stick in the response cookie metadata.
path_header path_header
If 'path' is None (the default), then the response If 'path' is None (the default), then the response
cookie 'path' will be pulled from request.headers[path_header]. cookie 'path' will be pulled from request.headers[path_header].
name name
The name of the cookie. The name of the cookie.
timeout timeout
The expiration timeout (in minutes) for the stored session data. The expiration timeout (in minutes) for the stored session data.
If 'persistent' is True (the default), this is also the timeout If 'persistent' is True (the default), this is also the timeout
for the cookie. for the cookie.
domain domain
The cookie domain. The cookie domain.
secure secure
If False (the default) the cookie 'secure' value will not If False (the default) the cookie 'secure' value will not
be set. If True, the cookie 'secure' value will be set (to 1). be set. If True, the cookie 'secure' value will be set (to 1).
clean_freq (minutes) clean_freq (minutes)
The poll rate for expired session cleanup. The poll rate for expired session cleanup.
persistent persistent
If True (the default), the 'timeout' argument will be used If True (the default), the 'timeout' argument will be used
to expire the cookie. If False, the cookie will not have an expiry, to expire the cookie. If False, the cookie will not have an expiry,
and the cookie will be a "session cookie" which expires when the and the cookie will be a "session cookie" which expires when the
browser is closed. browser is closed.
httponly httponly
If False (the default) the cookie 'httponly' value will not be set. If False (the default) the cookie 'httponly' value will not be set.
If True, the cookie 'httponly' value will be set (to 1). If True, the cookie 'httponly' value will be set (to 1).
Any additional kwargs will be bound to the new Session instance, Any additional kwargs will be bound to the new Session instance,
and may be specific to the storage type. See the subclass of Session and may be specific to the storage type. See the subclass of Session
you're using for more information. you're using for more information.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
# Guard against running twice # Guard against running twice
if hasattr(request, "_session_init_flag"): if hasattr(request, "_session_init_flag"):
return return
request._session_init_flag = True request._session_init_flag = True
# Check if request came with a session ID # Check if request came with a session ID
id = None id = None
if name in request.cookie: if name in request.cookie:
@@ -772,14 +772,14 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
if debug: if debug:
cherrypy.log('ID obtained from request.cookie: %r' % id, cherrypy.log('ID obtained from request.cookie: %r' % id,
'TOOLS.SESSIONS') 'TOOLS.SESSIONS')
# Find the storage class and call setup (first time only). # Find the storage class and call setup (first time only).
storage_class = storage_type.title() + 'Session' storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class] storage_class = globals()[storage_class]
if not hasattr(cherrypy, "session"): if not hasattr(cherrypy, "session"):
if hasattr(storage_class, "setup"): if hasattr(storage_class, "setup"):
storage_class.setup(**kwargs) storage_class.setup(**kwargs)
# Create and attach a new Session instance to cherrypy.serving. # Create and attach a new Session instance to cherrypy.serving.
# It will possess a reference to (and lock, and lazily load) # It will possess a reference to (and lock, and lazily load)
# the requested session data. # the requested session data.
@@ -791,11 +791,11 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
"""Update the cookie every time the session id changes.""" """Update the cookie every time the session id changes."""
cherrypy.serving.response.cookie[name] = id cherrypy.serving.response.cookie[name] = id
sess.id_observers.append(update_cookie) sess.id_observers.append(update_cookie)
# Create cherrypy.session which will proxy to cherrypy.serving.session # Create cherrypy.session which will proxy to cherrypy.serving.session
if not hasattr(cherrypy, "session"): if not hasattr(cherrypy, "session"):
cherrypy.session = cherrypy._ThreadLocalProxy('session') cherrypy.session = cherrypy._ThreadLocalProxy('session')
if persistent: if persistent:
cookie_timeout = timeout cookie_timeout = timeout
else: else:
@@ -810,7 +810,7 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
def set_response_cookie(path=None, path_header=None, name='session_id', def set_response_cookie(path=None, path_header=None, name='session_id',
timeout=60, domain=None, secure=False, httponly=False): timeout=60, domain=None, secure=False, httponly=False):
"""Set a response cookie for the client. """Set a response cookie for the client.
path path
the 'path' value to stick in the response cookie metadata. the 'path' value to stick in the response cookie metadata.
@@ -843,7 +843,7 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
cookie[name] = cherrypy.serving.session.id cookie[name] = cherrypy.serving.session.id
cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header) cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
or '/') or '/')
# We'd like to use the "max-age" param as indicated in # We'd like to use the "max-age" param as indicated in
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
# save it to disk and the session is lost if people close # save it to disk and the session is lost if people close
+38 -38
View File
@@ -22,19 +22,19 @@ from cherrypy.lib import cptools, httputil, file_generator_limited
def serve_file(path, content_type=None, disposition=None, name=None, debug=False): def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
"""Set status, headers, and body in order to serve the given path. """Set status, headers, and body in order to serve the given path.
The Content-Type header will be set to the content_type arg, if provided. The Content-Type header will be set to the content_type arg, if provided.
If not provided, the Content-Type will be guessed by the file extension If not provided, the Content-Type will be guessed by the file extension
of the 'path' argument. of the 'path' argument.
If disposition is not None, the Content-Disposition header will be set If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, it will be set to "<disposition>; filename=<name>". If name is None, it will be set
to the basename of path. If disposition is None, no Content-Disposition to the basename of path. If disposition is None, no Content-Disposition
header will be written. header will be written.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
# If path is relative, users should fix it by making path absolute. # If path is relative, users should fix it by making path absolute.
# That is, CherryPy should not guess where the application root is. # That is, CherryPy should not guess where the application root is.
# It certainly should *not* use cwd (since CP may be invoked from a # It certainly should *not* use cwd (since CP may be invoked from a
@@ -45,26 +45,26 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
if debug: if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE') cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg) raise ValueError(msg)
try: try:
st = os.stat(path) st = os.stat(path)
except OSError: except OSError:
if debug: if debug:
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound() raise cherrypy.NotFound()
# Check if path is a directory. # Check if path is a directory.
if stat.S_ISDIR(st.st_mode): if stat.S_ISDIR(st.st_mode):
# Let the caller deal with it as they like. # Let the caller deal with it as they like.
if debug: if debug:
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound() raise cherrypy.NotFound()
# Set the Last-Modified response header, so that # Set the Last-Modified response header, so that
# modified-since validation code can work. # modified-since validation code can work.
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since() cptools.validate_since()
if content_type is None: if content_type is None:
# Set content-type based on filename extension # Set content-type based on filename extension
ext = "" ext = ""
@@ -76,7 +76,7 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
response.headers['Content-Type'] = content_type response.headers['Content-Type'] = content_type
if debug: if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None cd = None
if disposition is not None: if disposition is not None:
if name is None: if name is None:
@@ -85,7 +85,7 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
response.headers["Content-Disposition"] = cd response.headers["Content-Disposition"] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object) # Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory # this way CP won't load the whole file in memory
content_length = st.st_size content_length = st.st_size
@@ -95,9 +95,9 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
debug=False): debug=False):
"""Set status, headers, and body in order to serve the given file object. """Set status, headers, and body in order to serve the given file object.
The Content-Type header will be set to the content_type arg, if provided. The Content-Type header will be set to the content_type arg, if provided.
If disposition is not None, the Content-Disposition header will be set If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, 'filename' will to "<disposition>; filename=<name>". If name is None, 'filename' will
not be set. If disposition is None, no Content-Disposition header will not be set. If disposition is None, no Content-Disposition header will
@@ -110,9 +110,9 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
serve_fileobj(), expecting that the data would be served starting from that serve_fileobj(), expecting that the data would be served starting from that
position. position.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
try: try:
st = os.fstat(fileobj.fileno()) st = os.fstat(fileobj.fileno())
except AttributeError: except AttributeError:
@@ -127,12 +127,12 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since() cptools.validate_since()
content_length = st.st_size content_length = st.st_size
if content_type is not None: if content_type is not None:
response.headers['Content-Type'] = content_type response.headers['Content-Type'] = content_type
if debug: if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None cd = None
if disposition is not None: if disposition is not None:
if name is None: if name is None:
@@ -142,13 +142,13 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
response.headers["Content-Disposition"] = cd response.headers["Content-Disposition"] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
return _serve_fileobj(fileobj, content_type, content_length, debug=debug) return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
def _serve_fileobj(fileobj, content_type, content_length, debug=False): def _serve_fileobj(fileobj, content_type, content_length, debug=False):
"""Internal. Set response.body to the given file object, perhaps ranged.""" """Internal. Set response.body to the given file object, perhaps ranged."""
response = cherrypy.serving.response response = cherrypy.serving.response
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
request = cherrypy.serving.request request = cherrypy.serving.request
if request.protocol >= (1, 1): if request.protocol >= (1, 1):
@@ -160,7 +160,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
if debug: if debug:
cherrypy.log(message, 'TOOLS.STATIC') cherrypy.log(message, 'TOOLS.STATIC')
raise cherrypy.HTTPError(416, message) raise cherrypy.HTTPError(416, message)
if r: if r:
if len(r) == 1: if len(r) == 1:
# Return a single-part response. # Return a single-part response.
@@ -192,11 +192,11 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
if "Content-Length" in response.headers: if "Content-Length" in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers["Content-Length"]
def file_ranges(): def file_ranges():
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield ntob("\r\n")
for start, stop in r: for start, stop in r:
if debug: if debug:
cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop), cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
@@ -211,7 +211,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
yield ntob("\r\n") yield ntob("\r\n")
# Final boundary # Final boundary
yield ntob("--" + boundary + "--", 'ascii') yield ntob("--" + boundary + "--", 'ascii')
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield ntob("\r\n")
response.body = file_ranges() response.body = file_ranges()
@@ -219,7 +219,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
else: else:
if debug: if debug:
cherrypy.log('No byteranges requested', 'TOOLS.STATIC') cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object) # Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory # this way CP won't load the whole file in memory
response.headers['Content-Length'] = content_length response.headers['Content-Length'] = content_length
@@ -255,17 +255,17 @@ def _attempt(filename, content_types, debug=False):
def staticdir(section, dir, root="", match="", content_types=None, index="", def staticdir(section, dir, root="", match="", content_types=None, index="",
debug=False): debug=False):
"""Serve a static resource from the given (root +) dir. """Serve a static resource from the given (root +) dir.
match match
If given, request.path_info will be searched for the given If given, request.path_info will be searched for the given
regular expression before attempting to serve static content. regular expression before attempting to serve static content.
content_types content_types
If given, it should be a Python dictionary of If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is {file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif"). out in the Content-Type response header (e.g. "image/gif").
index index
If provided, it should be the (relative) name of a file to If provided, it should be the (relative) name of a file to
serve for directory requests. For example, if the dir argument is serve for directory requests. For example, if the dir argument is
@@ -277,13 +277,13 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
if debug: if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
return False return False
if match and not re.search(match, request.path_info): if match and not re.search(match, request.path_info):
if debug: if debug:
cherrypy.log('request.path_info %r does not match pattern %r' % cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICDIR') (request.path_info, match), 'TOOLS.STATICDIR')
return False return False
# Allow the use of '~' to refer to a user's home directory. # Allow the use of '~' to refer to a user's home directory.
dir = os.path.expanduser(dir) dir = os.path.expanduser(dir)
@@ -295,7 +295,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
cherrypy.log(msg, 'TOOLS.STATICDIR') cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg) raise ValueError(msg)
dir = os.path.join(root, dir) dir = os.path.join(root, dir)
# Determine where we are in the object tree relative to 'section' # Determine where we are in the object tree relative to 'section'
# (where the static tool was defined). # (where the static tool was defined).
if section == 'global': if section == 'global':
@@ -303,19 +303,19 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
section = section.rstrip(r"\/") section = section.rstrip(r"\/")
branch = request.path_info[len(section) + 1:] branch = request.path_info[len(section) + 1:]
branch = unquote(branch.lstrip(r"\/")) branch = unquote(branch.lstrip(r"\/"))
# If branch is "", filename will end in a slash # If branch is "", filename will end in a slash
filename = os.path.join(dir, branch) filename = os.path.join(dir, branch)
if debug: if debug:
cherrypy.log('Checking file %r to fulfill %r' % cherrypy.log('Checking file %r to fulfill %r' %
(filename, request.path_info), 'TOOLS.STATICDIR') (filename, request.path_info), 'TOOLS.STATICDIR')
# There's a chance that the branch pulled from the URL might # There's a chance that the branch pulled from the URL might
# have ".." or similar uplevel attacks in it. Check that the final # have ".." or similar uplevel attacks in it. Check that the final
# filename is a child of dir. # filename is a child of dir.
if not os.path.normpath(filename).startswith(os.path.normpath(dir)): if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
raise cherrypy.HTTPError(403) # Forbidden raise cherrypy.HTTPError(403) # Forbidden
handled = _attempt(filename, content_types) handled = _attempt(filename, content_types)
if not handled: if not handled:
# Check for an index file if a folder was requested. # Check for an index file if a folder was requested.
@@ -327,30 +327,30 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
def staticfile(filename, root=None, match="", content_types=None, debug=False): def staticfile(filename, root=None, match="", content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename. """Serve a static resource from the given (root +) filename.
match match
If given, request.path_info will be searched for the given If given, request.path_info will be searched for the given
regular expression before attempting to serve static content. regular expression before attempting to serve static content.
content_types content_types
If given, it should be a Python dictionary of If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is {file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif"). out in the Content-Type response header (e.g. "image/gif").
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
if request.method not in ('GET', 'HEAD'): if request.method not in ('GET', 'HEAD'):
if debug: if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
return False return False
if match and not re.search(match, request.path_info): if match and not re.search(match, request.path_info):
if debug: if debug:
cherrypy.log('request.path_info %r does not match pattern %r' % cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICFILE') (request.path_info, match), 'TOOLS.STATICFILE')
return False return False
# If filename is relative, make absolute using "root". # If filename is relative, make absolute using "root".
if not os.path.isabs(filename): if not os.path.isabs(filename):
if not root: if not root:
@@ -359,5 +359,5 @@ def staticfile(filename, root=None, match="", content_types=None, debug=False):
cherrypy.log(msg, 'TOOLS.STATICFILE') cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg) raise ValueError(msg)
filename = os.path.join(root, filename) filename = os.path.join(root, filename)
return _attempt(filename, content_types, debug=debug) return _attempt(filename, content_types, debug=debug)
+89 -89
View File
@@ -30,13 +30,13 @@ _module__file__base = os.getcwd()
class SimplePlugin(object): class SimplePlugin(object):
"""Plugin base class which auto-subscribes methods for known channels.""" """Plugin base class which auto-subscribes methods for known channels."""
bus = None bus = None
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.""" """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
def __init__(self, bus): def __init__(self, bus):
self.bus = bus self.bus = bus
def subscribe(self): def subscribe(self):
"""Register this object as a (multi-channel) listener on the bus.""" """Register this object as a (multi-channel) listener on the bus."""
for channel in self.bus.listeners: for channel in self.bus.listeners:
@@ -44,7 +44,7 @@ class SimplePlugin(object):
method = getattr(self, channel, None) method = getattr(self, channel, None)
if method is not None: if method is not None:
self.bus.subscribe(channel, method) self.bus.subscribe(channel, method)
def unsubscribe(self): def unsubscribe(self):
"""Unregister this object as a listener on the bus.""" """Unregister this object as a listener on the bus."""
for channel in self.bus.listeners: for channel in self.bus.listeners:
@@ -57,39 +57,39 @@ class SimplePlugin(object):
class SignalHandler(object): class SignalHandler(object):
"""Register bus channels (and listeners) for system signals. """Register bus channels (and listeners) for system signals.
You can modify what signals your application listens for, and what it does You can modify what signals your application listens for, and what it does
when it receives signals, by modifying :attr:`SignalHandler.handlers`, when it receives signals, by modifying :attr:`SignalHandler.handlers`,
a dict of {signal name: callback} pairs. The default set is:: a dict of {signal name: callback} pairs. The default set is::
handlers = {'SIGTERM': self.bus.exit, handlers = {'SIGTERM': self.bus.exit,
'SIGHUP': self.handle_SIGHUP, 'SIGHUP': self.handle_SIGHUP,
'SIGUSR1': self.bus.graceful, 'SIGUSR1': self.bus.graceful,
} }
The :func:`SignalHandler.handle_SIGHUP`` method calls The :func:`SignalHandler.handle_SIGHUP`` method calls
:func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>` :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
if the process is daemonized, but if the process is daemonized, but
:func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>` :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
if the process is attached to a TTY. This is because Unix window if the process is attached to a TTY. This is because Unix window
managers tend to send SIGHUP to terminal windows when the user closes them. managers tend to send SIGHUP to terminal windows when the user closes them.
Feel free to add signals which are not available on every platform. The Feel free to add signals which are not available on every platform. The
:class:`SignalHandler` will ignore errors raised from attempting to register :class:`SignalHandler` will ignore errors raised from attempting to register
handlers for unknown signals. handlers for unknown signals.
""" """
handlers = {} handlers = {}
"""A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
signals = {} signals = {}
"""A map from signal numbers to names.""" """A map from signal numbers to names."""
for k, v in vars(_signal).items(): for k, v in vars(_signal).items():
if k.startswith('SIG') and not k.startswith('SIG_'): if k.startswith('SIG') and not k.startswith('SIG_'):
signals[v] = k signals[v] = k
del k, v del k, v
def __init__(self, bus): def __init__(self, bus):
self.bus = bus self.bus = bus
# Set default handlers # Set default handlers
@@ -106,12 +106,12 @@ class SignalHandler(object):
self.handlers['SIGINT'] = self._jython_SIGINT_handler self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {} self._previous_handlers = {}
def _jython_SIGINT_handler(self, signum=None, frame=None): def _jython_SIGINT_handler(self, signum=None, frame=None):
# See http://bugs.jython.org/issue1313 # See http://bugs.jython.org/issue1313
self.bus.log('Keyboard Interrupt: shutting down bus') self.bus.log('Keyboard Interrupt: shutting down bus')
self.bus.exit() self.bus.exit()
def subscribe(self): def subscribe(self):
"""Subscribe self.handlers to signals.""" """Subscribe self.handlers to signals."""
for sig, func in self.handlers.items(): for sig, func in self.handlers.items():
@@ -119,18 +119,18 @@ class SignalHandler(object):
self.set_handler(sig, func) self.set_handler(sig, func)
except ValueError: except ValueError:
pass pass
def unsubscribe(self): def unsubscribe(self):
"""Unsubscribe self.handlers from signals.""" """Unsubscribe self.handlers from signals."""
for signum, handler in self._previous_handlers.items(): for signum, handler in self._previous_handlers.items():
signame = self.signals[signum] signame = self.signals[signum]
if handler is None: if handler is None:
self.bus.log("Restoring %s handler to SIG_DFL." % signame) self.bus.log("Restoring %s handler to SIG_DFL." % signame)
handler = _signal.SIG_DFL handler = _signal.SIG_DFL
else: else:
self.bus.log("Restoring %s handler %r." % (signame, handler)) self.bus.log("Restoring %s handler %r." % (signame, handler))
try: try:
our_handler = _signal.signal(signum, handler) our_handler = _signal.signal(signum, handler)
if our_handler is None: if our_handler is None:
@@ -140,13 +140,13 @@ class SignalHandler(object):
except ValueError: except ValueError:
self.bus.log("Unable to restore %s handler %r." % self.bus.log("Unable to restore %s handler %r." %
(signame, handler), level=40, traceback=True) (signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None): def set_handler(self, signal, listener=None):
"""Subscribe a handler for the given signal (number or name). """Subscribe a handler for the given signal (number or name).
If the optional 'listener' argument is provided, it will be If the optional 'listener' argument is provided, it will be
subscribed as a listener for the given signal's channel. subscribed as a listener for the given signal's channel.
If the given signal name or number is not available on the current If the given signal name or number is not available on the current
platform, ValueError is raised. platform, ValueError is raised.
""" """
@@ -161,20 +161,20 @@ class SignalHandler(object):
except KeyError: except KeyError:
raise ValueError("No such signal: %r" % signal) raise ValueError("No such signal: %r" % signal)
signum = signal signum = signal
prev = _signal.signal(signum, self._handle_signal) prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev self._previous_handlers[signum] = prev
if listener is not None: if listener is not None:
self.bus.log("Listening for %s." % signame) self.bus.log("Listening for %s." % signame)
self.bus.subscribe(signame, listener) self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None): def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you).""" """Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum] signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame) self.bus.log("Caught signal %s." % signame)
self.bus.publish(signame) self.bus.publish(signame)
def handle_SIGHUP(self): def handle_SIGHUP(self):
"""Restart if daemonized, else exit.""" """Restart if daemonized, else exit."""
if os.isatty(sys.stdin.fileno()): if os.isatty(sys.stdin.fileno()):
@@ -194,17 +194,17 @@ except ImportError:
class DropPrivileges(SimplePlugin): class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows. """Drop privileges. uid/gid arguments not available on Windows.
Special thanks to Gavin Baker: http://antonym.org/node/100. Special thanks to Gavin Baker: http://antonym.org/node/100.
""" """
def __init__(self, bus, umask=None, uid=None, gid=None): def __init__(self, bus, umask=None, uid=None, gid=None):
SimplePlugin.__init__(self, bus) SimplePlugin.__init__(self, bus)
self.finalized = False self.finalized = False
self.uid = uid self.uid = uid
self.gid = gid self.gid = gid
self.umask = umask self.umask = umask
def _get_uid(self): def _get_uid(self):
return self._uid return self._uid
def _set_uid(self, val): def _set_uid(self, val):
@@ -218,7 +218,7 @@ class DropPrivileges(SimplePlugin):
self._uid = val self._uid = val
uid = property(_get_uid, _set_uid, uid = property(_get_uid, _set_uid,
doc="The uid under which to run. Availability: Unix.") doc="The uid under which to run. Availability: Unix.")
def _get_gid(self): def _get_gid(self):
return self._gid return self._gid
def _set_gid(self, val): def _set_gid(self, val):
@@ -232,7 +232,7 @@ class DropPrivileges(SimplePlugin):
self._gid = val self._gid = val
gid = property(_get_gid, _set_gid, gid = property(_get_gid, _set_gid,
doc="The gid under which to run. Availability: Unix.") doc="The gid under which to run. Availability: Unix.")
def _get_umask(self): def _get_umask(self):
return self._umask return self._umask
def _set_umask(self, val): def _set_umask(self, val):
@@ -246,11 +246,11 @@ class DropPrivileges(SimplePlugin):
self._umask = val self._umask = val
umask = property(_get_umask, _set_umask, umask = property(_get_umask, _set_umask,
doc="""The default permission mode for newly created files and directories. doc="""The default permission mode for newly created files and directories.
Usually expressed in octal format, for example, ``0644``. Usually expressed in octal format, for example, ``0644``.
Availability: Unix, Windows. Availability: Unix, Windows.
""") """)
def start(self): def start(self):
# uid/gid # uid/gid
def current_ids(): def current_ids():
@@ -261,7 +261,7 @@ class DropPrivileges(SimplePlugin):
if grp: if grp:
group = grp.getgrgid(os.getgid())[0] group = grp.getgrgid(os.getgid())[0]
return name, group return name, group
if self.finalized: if self.finalized:
if not (self.uid is None and self.gid is None): if not (self.uid is None and self.gid is None):
self.bus.log('Already running as uid: %r gid: %r' % self.bus.log('Already running as uid: %r gid: %r' %
@@ -278,7 +278,7 @@ class DropPrivileges(SimplePlugin):
if self.uid is not None: if self.uid is not None:
os.setuid(self.uid) os.setuid(self.uid)
self.bus.log('Running as uid: %r gid: %r' % current_ids()) self.bus.log('Running as uid: %r gid: %r' % current_ids())
# umask # umask
if self.finalized: if self.finalized:
if self.umask is not None: if self.umask is not None:
@@ -290,7 +290,7 @@ class DropPrivileges(SimplePlugin):
old_umask = os.umask(self.umask) old_umask = os.umask(self.umask)
self.bus.log('umask old: %03o, new: %03o' % self.bus.log('umask old: %03o, new: %03o' %
(old_umask, self.umask)) (old_umask, self.umask))
self.finalized = True self.finalized = True
# This is slightly higher than the priority for server.start # This is slightly higher than the priority for server.start
# in order to facilitate the most common use: starting on a low # in order to facilitate the most common use: starting on a low
@@ -300,11 +300,11 @@ class DropPrivileges(SimplePlugin):
class Daemonizer(SimplePlugin): class Daemonizer(SimplePlugin):
"""Daemonize the running script. """Daemonize the running script.
Use this with a Web Site Process Bus via:: Use this with a Web Site Process Bus via::
Daemonizer(bus).subscribe() Daemonizer(bus).subscribe()
When this component finishes, the process is completely decoupled from When this component finishes, the process is completely decoupled from
the parent environment. Please note that when this component is used, the parent environment. Please note that when this component is used,
the return code from the parent process will still be 0 if a startup the return code from the parent process will still be 0 if a startup
@@ -314,7 +314,7 @@ class Daemonizer(SimplePlugin):
of whether the process fully started. In fact, that return code only of whether the process fully started. In fact, that return code only
indicates if the process succesfully finished the first fork. indicates if the process succesfully finished the first fork.
""" """
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'): stderr='/dev/null'):
SimplePlugin.__init__(self, bus) SimplePlugin.__init__(self, bus)
@@ -322,11 +322,11 @@ class Daemonizer(SimplePlugin):
self.stdout = stdout self.stdout = stdout
self.stderr = stderr self.stderr = stderr
self.finalized = False self.finalized = False
def start(self): def start(self):
if self.finalized: if self.finalized:
self.bus.log('Already deamonized.') self.bus.log('Already deamonized.')
# forking has issues with threads: # forking has issues with threads:
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
# "The general problem with making fork() work in a multi-threaded # "The general problem with making fork() work in a multi-threaded
@@ -336,15 +336,15 @@ class Daemonizer(SimplePlugin):
self.bus.log('There are %r active threads. ' self.bus.log('There are %r active threads. '
'Daemonizing now may cause strange failures.' % 'Daemonizing now may cause strange failures.' %
threading.enumerate(), level=30) threading.enumerate(), level=30)
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
# Finish up with the current stdout/stderr # Finish up with the current stdout/stderr
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
# Do first fork. # Do first fork.
try: try:
pid = os.fork() pid = os.fork()
@@ -360,9 +360,9 @@ class Daemonizer(SimplePlugin):
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
sys.exit("%s: fork #1 failed: (%d) %s\n" sys.exit("%s: fork #1 failed: (%d) %s\n"
% (sys.argv[0], exc.errno, exc.strerror)) % (sys.argv[0], exc.errno, exc.strerror))
os.setsid() os.setsid()
# Do second fork # Do second fork
try: try:
pid = os.fork() pid = os.fork()
@@ -373,10 +373,10 @@ class Daemonizer(SimplePlugin):
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
sys.exit("%s: fork #2 failed: (%d) %s\n" sys.exit("%s: fork #2 failed: (%d) %s\n"
% (sys.argv[0], exc.errno, exc.strerror)) % (sys.argv[0], exc.errno, exc.strerror))
os.chdir("/") os.chdir("/")
os.umask(0) os.umask(0)
si = open(self.stdin, "r") si = open(self.stdin, "r")
so = open(self.stdout, "a+") so = open(self.stdout, "a+")
se = open(self.stderr, "a+") se = open(self.stderr, "a+")
@@ -387,7 +387,7 @@ class Daemonizer(SimplePlugin):
os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno()) os.dup2(se.fileno(), sys.stderr.fileno())
self.bus.log('Daemonized to PID: %s' % os.getpid()) self.bus.log('Daemonized to PID: %s' % os.getpid())
self.finalized = True self.finalized = True
start.priority = 65 start.priority = 65
@@ -395,12 +395,12 @@ class Daemonizer(SimplePlugin):
class PIDFile(SimplePlugin): class PIDFile(SimplePlugin):
"""Maintain a PID file via a WSPBus.""" """Maintain a PID file via a WSPBus."""
def __init__(self, bus, pidfile): def __init__(self, bus, pidfile):
SimplePlugin.__init__(self, bus) SimplePlugin.__init__(self, bus)
self.pidfile = pidfile self.pidfile = pidfile
self.finalized = False self.finalized = False
def start(self): def start(self):
pid = os.getpid() pid = os.getpid()
if self.finalized: if self.finalized:
@@ -410,7 +410,7 @@ class PIDFile(SimplePlugin):
self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
self.finalized = True self.finalized = True
start.priority = 70 start.priority = 70
def exit(self): def exit(self):
try: try:
os.remove(self.pidfile) os.remove(self.pidfile)
@@ -423,12 +423,12 @@ class PIDFile(SimplePlugin):
class PerpetualTimer(threading._Timer): class PerpetualTimer(threading._Timer):
"""A responsive subclass of threading._Timer whose run() method repeats. """A responsive subclass of threading._Timer whose run() method repeats.
Use this timer only when you really need a very interruptible timer; Use this timer only when you really need a very interruptible timer;
this checks its 'finished' condition up to 20 times a second, which can this checks its 'finished' condition up to 20 times a second, which can
results in pretty high CPU usage results in pretty high CPU usage
""" """
def run(self): def run(self):
while True: while True:
self.finished.wait(self.interval) self.finished.wait(self.interval)
@@ -445,14 +445,14 @@ class PerpetualTimer(threading._Timer):
class BackgroundTask(threading.Thread): class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats. """A subclass of threading.Thread whose run() method repeats.
Use this class for most repeating tasks. It uses time.sleep() to wait Use this class for most repeating tasks. It uses time.sleep() to wait
for each interval, which isn't very responsive; that is, even if you call for each interval, which isn't very responsive; that is, even if you call
self.cancel(), you'll have to wait until the sleep() call finishes before self.cancel(), you'll have to wait until the sleep() call finishes before
the thread stops. To compensate, it defaults to being daemonic, which means the thread stops. To compensate, it defaults to being daemonic, which means
it won't delay stopping the whole process. it won't delay stopping the whole process.
""" """
def __init__(self, interval, function, args=[], kwargs={}, bus=None): def __init__(self, interval, function, args=[], kwargs={}, bus=None):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.interval = interval self.interval = interval
@@ -461,10 +461,10 @@ class BackgroundTask(threading.Thread):
self.kwargs = kwargs self.kwargs = kwargs
self.running = False self.running = False
self.bus = bus self.bus = bus
def cancel(self): def cancel(self):
self.running = False self.running = False
def run(self): def run(self):
self.running = True self.running = True
while self.running: while self.running:
@@ -479,30 +479,30 @@ class BackgroundTask(threading.Thread):
% self.function, level=40, traceback=True) % self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs. # Quit on first error to avoid massive logs.
raise raise
def _set_daemon(self): def _set_daemon(self):
return True return True
class Monitor(SimplePlugin): class Monitor(SimplePlugin):
"""WSPBus listener to periodically run a callback in its own thread.""" """WSPBus listener to periodically run a callback in its own thread."""
callback = None callback = None
"""The function to call at intervals.""" """The function to call at intervals."""
frequency = 60 frequency = 60
"""The time in seconds between callback runs.""" """The time in seconds between callback runs."""
thread = None thread = None
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread.""" """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
def __init__(self, bus, callback, frequency=60, name=None): def __init__(self, bus, callback, frequency=60, name=None):
SimplePlugin.__init__(self, bus) SimplePlugin.__init__(self, bus)
self.callback = callback self.callback = callback
self.frequency = frequency self.frequency = frequency
self.thread = None self.thread = None
self.name = name self.name = name
def start(self): def start(self):
"""Start our callback in its own background thread.""" """Start our callback in its own background thread."""
if self.frequency > 0: if self.frequency > 0:
@@ -516,7 +516,7 @@ class Monitor(SimplePlugin):
else: else:
self.bus.log("Monitor thread %r already started." % threadname) self.bus.log("Monitor thread %r already started." % threadname)
start.priority = 70 start.priority = 70
def stop(self): def stop(self):
"""Stop our callback's background task thread.""" """Stop our callback's background task thread."""
if self.thread is None: if self.thread is None:
@@ -530,7 +530,7 @@ class Monitor(SimplePlugin):
self.thread.join() self.thread.join()
self.bus.log("Stopped thread %r." % name) self.bus.log("Stopped thread %r." % name)
self.thread = None self.thread = None
def graceful(self): def graceful(self):
"""Stop the callback's background task thread and restart it.""" """Stop the callback's background task thread and restart it."""
self.stop() self.stop()
@@ -539,47 +539,47 @@ class Monitor(SimplePlugin):
class Autoreloader(Monitor): class Autoreloader(Monitor):
"""Monitor which re-executes the process when files change. """Monitor which re-executes the process when files change.
This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`) This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
if any of the files it monitors change (or is deleted). By default, the if any of the files it monitors change (or is deleted). By default, the
autoreloader monitors all imported modules; you can add to the autoreloader monitors all imported modules; you can add to the
set by adding to ``autoreload.files``:: set by adding to ``autoreload.files``::
cherrypy.engine.autoreload.files.add(myFile) cherrypy.engine.autoreload.files.add(myFile)
If there are imported files you do *not* wish to monitor, you can adjust the If there are imported files you do *not* wish to monitor, you can adjust the
``match`` attribute, a regular expression. For example, to stop monitoring ``match`` attribute, a regular expression. For example, to stop monitoring
cherrypy itself:: cherrypy itself::
cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins, Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
the autoreload plugin takes a ``frequency`` argument. The default is the autoreload plugin takes a ``frequency`` argument. The default is
1 second; that is, the autoreloader will examine files once each second. 1 second; that is, the autoreloader will examine files once each second.
""" """
files = None files = None
"""The set of files to poll for modifications.""" """The set of files to poll for modifications."""
frequency = 1 frequency = 1
"""The interval in seconds at which to poll for modified files.""" """The interval in seconds at which to poll for modified files."""
match = '.*' match = '.*'
"""A regular expression by which to match filenames.""" """A regular expression by which to match filenames."""
def __init__(self, bus, frequency=1, match='.*'): def __init__(self, bus, frequency=1, match='.*'):
self.mtimes = {} self.mtimes = {}
self.files = set() self.files = set()
self.match = match self.match = match
Monitor.__init__(self, bus, self.run, frequency) Monitor.__init__(self, bus, self.run, frequency)
def start(self): def start(self):
"""Start our own background task thread for self.run.""" """Start our own background task thread for self.run."""
if self.thread is None: if self.thread is None:
self.mtimes = {} self.mtimes = {}
Monitor.start(self) Monitor.start(self)
start.priority = 70 start.priority = 70
def sysfiles(self): def sysfiles(self):
"""Return a Set of sys.modules filenames to monitor.""" """Return a Set of sys.modules filenames to monitor."""
files = set() files = set()
@@ -594,25 +594,25 @@ class Autoreloader(Monitor):
f = os.path.normpath(os.path.join(_module__file__base, f)) f = os.path.normpath(os.path.join(_module__file__base, f))
files.add(f) files.add(f)
return files return files
def run(self): def run(self):
"""Reload the process if registered files have been modified.""" """Reload the process if registered files have been modified."""
for filename in self.sysfiles() | self.files: for filename in self.sysfiles() | self.files:
if filename: if filename:
if filename.endswith('.pyc'): if filename.endswith('.pyc'):
filename = filename[:-1] filename = filename[:-1]
oldtime = self.mtimes.get(filename, 0) oldtime = self.mtimes.get(filename, 0)
if oldtime is None: if oldtime is None:
# Module with no .py file. Skip it. # Module with no .py file. Skip it.
continue continue
try: try:
mtime = os.stat(filename).st_mtime mtime = os.stat(filename).st_mtime
except OSError: except OSError:
# Either a module with no .py file, or it's been deleted. # Either a module with no .py file, or it's been deleted.
mtime = None mtime = None
if filename not in self.mtimes: if filename not in self.mtimes:
# If a module has no .py file, this will be None. # If a module has no .py file, this will be None.
self.mtimes[filename] = mtime self.mtimes[filename] = mtime
@@ -628,12 +628,12 @@ class Autoreloader(Monitor):
class ThreadManager(SimplePlugin): class ThreadManager(SimplePlugin):
"""Manager for HTTP request threads. """Manager for HTTP request threads.
If you have control over thread creation and destruction, publish to If you have control over thread creation and destruction, publish to
the 'acquire_thread' and 'release_thread' channels (for each thread). the 'acquire_thread' and 'release_thread' channels (for each thread).
This will register/unregister the current thread and publish to This will register/unregister the current thread and publish to
'start_thread' and 'stop_thread' listeners in the bus as needed. 'start_thread' and 'stop_thread' listeners in the bus as needed.
If threads are created and destroyed by code you do not control If threads are created and destroyed by code you do not control
(e.g., Apache), then, at the beginning of every HTTP request, (e.g., Apache), then, at the beginning of every HTTP request,
publish to 'acquire_thread' only. You should not publish to publish to 'acquire_thread' only. You should not publish to
@@ -641,10 +641,10 @@ class ThreadManager(SimplePlugin):
the thread will be re-used or not. The bus will call the thread will be re-used or not. The bus will call
'stop_thread' listeners for you when it stops. 'stop_thread' listeners for you when it stops.
""" """
threads = None threads = None
"""A map of {thread ident: index number} pairs.""" """A map of {thread ident: index number} pairs."""
def __init__(self, bus): def __init__(self, bus):
self.threads = {} self.threads = {}
SimplePlugin.__init__(self, bus) SimplePlugin.__init__(self, bus)
@@ -655,7 +655,7 @@ class ThreadManager(SimplePlugin):
def acquire_thread(self): def acquire_thread(self):
"""Run 'start_thread' listeners for the current thread. """Run 'start_thread' listeners for the current thread.
If the current thread has already been seen, any 'start_thread' If the current thread has already been seen, any 'start_thread'
listeners will not be run again. listeners will not be run again.
""" """
@@ -666,14 +666,14 @@ class ThreadManager(SimplePlugin):
i = len(self.threads) + 1 i = len(self.threads) + 1
self.threads[thread_ident] = i self.threads[thread_ident] = i
self.bus.publish('start_thread', i) self.bus.publish('start_thread', i)
def release_thread(self): def release_thread(self):
"""Release the current thread and run 'stop_thread' listeners.""" """Release the current thread and run 'stop_thread' listeners."""
thread_ident = get_thread_ident() thread_ident = get_thread_ident()
i = self.threads.pop(thread_ident, None) i = self.threads.pop(thread_ident, None)
if i is not None: if i is not None:
self.bus.publish('stop_thread', i) self.bus.publish('stop_thread', i)
def stop(self): def stop(self):
"""Release all threads and run all 'stop_thread' listeners.""" """Release all threads and run all 'stop_thread' listeners."""
for thread_ident, i in self.threads.items(): for thread_ident, i in self.threads.items():
+36 -36
View File
@@ -54,13 +54,13 @@ hello.py::
#!/usr/bin/python #!/usr/bin/python
import cherrypy import cherrypy
class HelloWorld: class HelloWorld:
\"""Sample request handler class.\""" \"""Sample request handler class.\"""
def index(self): def index(self):
return "Hello world!" return "Hello world!"
index.exposed = True index.exposed = True
cherrypy.tree.mount(HelloWorld()) cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work # CherryPy autoreload must be disabled for the flup server to work
cherrypy.config.update({'engine.autoreload_on':False}) cherrypy.config.update({'engine.autoreload_on':False})
@@ -107,7 +107,7 @@ directive, configure your fastcgi script like the following::
} # end of $HTTP["url"] =~ "^/" } # end of $HTTP["url"] =~ "^/"
Please see `Lighttpd FastCGI Docs Please see `Lighttpd FastCGI Docs
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation <http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation
of the possible configuration options. of the possible configuration options.
""" """
@@ -117,33 +117,33 @@ import time
class ServerAdapter(object): class ServerAdapter(object):
"""Adapter for an HTTP server. """Adapter for an HTTP server.
If you need to start more than one HTTP server (to serve on multiple If you need to start more than one HTTP server (to serve on multiple
ports, or protocols, etc.), you can manually register each one and then ports, or protocols, etc.), you can manually register each one and then
start them all with bus.start: start them all with bus.start:
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
s1.subscribe() s1.subscribe()
s2.subscribe() s2.subscribe()
bus.start() bus.start()
""" """
def __init__(self, bus, httpserver=None, bind_addr=None): def __init__(self, bus, httpserver=None, bind_addr=None):
self.bus = bus self.bus = bus
self.httpserver = httpserver self.httpserver = httpserver
self.bind_addr = bind_addr self.bind_addr = bind_addr
self.interrupt = None self.interrupt = None
self.running = False self.running = False
def subscribe(self): def subscribe(self):
self.bus.subscribe('start', self.start) self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop) self.bus.subscribe('stop', self.stop)
def unsubscribe(self): def unsubscribe(self):
self.bus.unsubscribe('start', self.start) self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop) self.bus.unsubscribe('stop', self.stop)
def start(self): def start(self):
"""Start the HTTP server.""" """Start the HTTP server."""
if self.bind_addr is None: if self.bind_addr is None:
@@ -153,29 +153,29 @@ class ServerAdapter(object):
on_what = "%s:%s" % (host, port) on_what = "%s:%s" % (host, port)
else: else:
on_what = "socket file: %s" % self.bind_addr on_what = "socket file: %s" % self.bind_addr
if self.running: if self.running:
self.bus.log("Already serving on %s" % on_what) self.bus.log("Already serving on %s" % on_what)
return return
self.interrupt = None self.interrupt = None
if not self.httpserver: if not self.httpserver:
raise ValueError("No HTTP server has been created.") raise ValueError("No HTTP server has been created.")
# Start the httpserver in a new thread. # Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple): if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr) wait_for_free_port(*self.bind_addr)
import threading import threading
t = threading.Thread(target=self._start_http_thread) t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName()) t.setName("HTTPServer " + t.getName())
t.start() t.start()
self.wait() self.wait()
self.running = True self.running = True
self.bus.log("Serving on %s" % on_what) self.bus.log("Serving on %s" % on_what)
start.priority = 75 start.priority = 75
def _start_http_thread(self): def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the """HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an main thread persists to receive KeyboardInterrupt's. If an
@@ -200,19 +200,19 @@ class ServerAdapter(object):
traceback=True, level=40) traceback=True, level=40)
self.bus.exit() self.bus.exit()
raise raise
def wait(self): def wait(self):
"""Wait until the HTTP server is ready to receive requests.""" """Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False): while not getattr(self.httpserver, "ready", False):
if self.interrupt: if self.interrupt:
raise self.interrupt raise self.interrupt
time.sleep(.1) time.sleep(.1)
# Wait for port to be occupied # Wait for port to be occupied
if isinstance(self.bind_addr, tuple): if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr host, port = self.bind_addr
wait_for_occupied_port(host, port) wait_for_occupied_port(host, port)
def stop(self): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
if self.running: if self.running:
@@ -226,7 +226,7 @@ class ServerAdapter(object):
else: else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver) self.bus.log("HTTP Server %s already shut down" % self.httpserver)
stop.priority = 25 stop.priority = 25
def restart(self): def restart(self):
"""Restart the HTTP server.""" """Restart the HTTP server."""
self.stop() self.stop()
@@ -235,22 +235,22 @@ class ServerAdapter(object):
class FlupCGIServer(object): class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer.""" """Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.ready = False self.ready = False
def start(self): def start(self):
"""Start the CGI server.""" """Start the CGI server."""
# We have to instantiate the server class here because its __init__ # We have to instantiate the server class here because its __init__
# starts a threadpool. If we do it too early, daemonize won't work. # starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.cgi import WSGIServer from flup.server.cgi import WSGIServer
self.cgiserver = WSGIServer(*self.args, **self.kwargs) self.cgiserver = WSGIServer(*self.args, **self.kwargs)
self.ready = True self.ready = True
self.cgiserver.run() self.cgiserver.run()
def stop(self): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
self.ready = False self.ready = False
@@ -258,7 +258,7 @@ class FlupCGIServer(object):
class FlupFCGIServer(object): class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer.""" """Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if kwargs.get('bindAddress', None) is None: if kwargs.get('bindAddress', None) is None:
import socket import socket
@@ -270,7 +270,7 @@ class FlupFCGIServer(object):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.ready = False self.ready = False
def start(self): def start(self):
"""Start the FCGI server.""" """Start the FCGI server."""
# We have to instantiate the server class here because its __init__ # We have to instantiate the server class here because its __init__
@@ -290,7 +290,7 @@ class FlupFCGIServer(object):
self.fcgiserver._oldSIGs = [] self.fcgiserver._oldSIGs = []
self.ready = True self.ready = True
self.fcgiserver.run() self.fcgiserver.run()
def stop(self): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
# Forcibly stop the fcgi server main event loop. # Forcibly stop the fcgi server main event loop.
@@ -302,12 +302,12 @@ class FlupFCGIServer(object):
class FlupSCGIServer(object): class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer.""" """Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.ready = False self.ready = False
def start(self): def start(self):
"""Start the SCGI server.""" """Start the SCGI server."""
# We have to instantiate the server class here because its __init__ # We have to instantiate the server class here because its __init__
@@ -327,7 +327,7 @@ class FlupSCGIServer(object):
self.scgiserver._oldSIGs = [] self.scgiserver._oldSIGs = []
self.ready = True self.ready = True
self.scgiserver.run() self.scgiserver.run()
def stop(self): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
self.ready = False self.ready = False
@@ -354,9 +354,9 @@ def check_port(host, port, timeout=1.0):
raise ValueError("Host values of '' or None are not allowed.") raise ValueError("Host values of '' or None are not allowed.")
host = client_host(host) host = client_host(host)
port = int(port) port = int(port)
import socket import socket
# AF_INET or AF_INET6 socket # AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6 addresses) # Get the correct address family for our host (allows IPv6 addresses)
try: try:
@@ -367,7 +367,7 @@ def check_port(host, port, timeout=1.0):
info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))] info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
else: else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))] info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
for res in info: for res in info:
af, socktype, proto, canonname, sa = res af, socktype, proto, canonname, sa = res
s = None s = None
@@ -396,7 +396,7 @@ def wait_for_free_port(host, port, timeout=None):
raise ValueError("Host values of '' or None are not allowed.") raise ValueError("Host values of '' or None are not allowed.")
if timeout is None: if timeout is None:
timeout = free_port_timeout timeout = free_port_timeout
for trial in range(50): for trial in range(50):
try: try:
# we are expecting a free port, so reduce the timeout # we are expecting a free port, so reduce the timeout
@@ -406,7 +406,7 @@ def wait_for_free_port(host, port, timeout=None):
time.sleep(timeout) time.sleep(timeout)
else: else:
return return
raise IOError("Port %r not free on %r" % (port, host)) raise IOError("Port %r not free on %r" % (port, host))
def wait_for_occupied_port(host, port, timeout=None): def wait_for_occupied_port(host, port, timeout=None):
@@ -415,7 +415,7 @@ def wait_for_occupied_port(host, port, timeout=None):
raise ValueError("Host values of '' or None are not allowed.") raise ValueError("Host values of '' or None are not allowed.")
if timeout is None: if timeout is None:
timeout = occupied_port_timeout timeout = occupied_port_timeout
for trial in range(50): for trial in range(50):
try: try:
check_port(host, port, timeout=timeout) check_port(host, port, timeout=timeout)
@@ -423,5 +423,5 @@ def wait_for_occupied_port(host, port, timeout=None):
return return
else: else:
time.sleep(timeout) time.sleep(timeout)
raise IOError("Port %r not bound on %r" % (port, host)) raise IOError("Port %r not bound on %r" % (port, host))
+23 -23
View File
@@ -12,16 +12,16 @@ from cherrypy.process import wspbus, plugins
class ConsoleCtrlHandler(plugins.SimplePlugin): class ConsoleCtrlHandler(plugins.SimplePlugin):
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
def __init__(self, bus): def __init__(self, bus):
self.is_set = False self.is_set = False
plugins.SimplePlugin.__init__(self, bus) plugins.SimplePlugin.__init__(self, bus)
def start(self): def start(self):
if self.is_set: if self.is_set:
self.bus.log('Handler for console events already set.', level=40) self.bus.log('Handler for console events already set.', level=40)
return return
result = win32api.SetConsoleCtrlHandler(self.handle, 1) result = win32api.SetConsoleCtrlHandler(self.handle, 1)
if result == 0: if result == 0:
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
@@ -29,38 +29,38 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
else: else:
self.bus.log('Set handler for console events.', level=40) self.bus.log('Set handler for console events.', level=40)
self.is_set = True self.is_set = True
def stop(self): def stop(self):
if not self.is_set: if not self.is_set:
self.bus.log('Handler for console events already off.', level=40) self.bus.log('Handler for console events already off.', level=40)
return return
try: try:
result = win32api.SetConsoleCtrlHandler(self.handle, 0) result = win32api.SetConsoleCtrlHandler(self.handle, 0)
except ValueError: except ValueError:
# "ValueError: The object has not been registered" # "ValueError: The object has not been registered"
result = 1 result = 1
if result == 0: if result == 0:
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
win32api.GetLastError(), level=40) win32api.GetLastError(), level=40)
else: else:
self.bus.log('Removed handler for console events.', level=40) self.bus.log('Removed handler for console events.', level=40)
self.is_set = False self.is_set = False
def handle(self, event): def handle(self, event):
"""Handle console control events (like Ctrl-C).""" """Handle console control events (like Ctrl-C)."""
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
win32con.CTRL_CLOSE_EVENT): win32con.CTRL_CLOSE_EVENT):
self.bus.log('Console event %s: shutting down bus' % event) self.bus.log('Console event %s: shutting down bus' % event)
# Remove self immediately so repeated Ctrl-C doesn't re-call it. # Remove self immediately so repeated Ctrl-C doesn't re-call it.
try: try:
self.stop() self.stop()
except ValueError: except ValueError:
pass pass
self.bus.exit() self.bus.exit()
# 'First to return True stops the calls' # 'First to return True stops the calls'
return 1 return 1
@@ -69,14 +69,14 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
class Win32Bus(wspbus.Bus): class Win32Bus(wspbus.Bus):
"""A Web Site Process Bus implementation for Win32. """A Web Site Process Bus implementation for Win32.
Instead of time.sleep, this bus blocks using native win32event objects. Instead of time.sleep, this bus blocks using native win32event objects.
""" """
def __init__(self): def __init__(self):
self.events = {} self.events = {}
wspbus.Bus.__init__(self) wspbus.Bus.__init__(self)
def _get_state_event(self, state): def _get_state_event(self, state):
"""Return a win32event for the given state (creating it if needed).""" """Return a win32event for the given state (creating it if needed)."""
try: try:
@@ -87,7 +87,7 @@ class Win32Bus(wspbus.Bus):
(state.name, os.getpid())) (state.name, os.getpid()))
self.events[state] = event self.events[state] = event
return event return event
def _get_state(self): def _get_state(self):
return self._state return self._state
def _set_state(self, value): def _set_state(self, value):
@@ -95,10 +95,10 @@ class Win32Bus(wspbus.Bus):
event = self._get_state_event(value) event = self._get_state_event(value)
win32event.PulseEvent(event) win32event.PulseEvent(event)
state = property(_get_state, _set_state) state = property(_get_state, _set_state)
def wait(self, state, interval=0.1, channel=None): def wait(self, state, interval=0.1, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit. """Wait for the given state(s), KeyboardInterrupt or SystemExit.
Since this class uses native win32event objects, the interval Since this class uses native win32event objects, the interval
argument is ignored. argument is ignored.
""" """
@@ -116,15 +116,15 @@ class Win32Bus(wspbus.Bus):
class _ControlCodes(dict): class _ControlCodes(dict):
"""Control codes used to "signal" a service via ControlService. """Control codes used to "signal" a service via ControlService.
User-defined control codes are in the range 128-255. We generally use User-defined control codes are in the range 128-255. We generally use
the standard Python value for the Linux signal and add 128. Example: the standard Python value for the Linux signal and add 128. Example:
>>> signal.SIGUSR1 >>> signal.SIGUSR1
10 10
control_codes['graceful'] = 128 + 10 control_codes['graceful'] = 128 + 10
""" """
def key_for(self, obj): def key_for(self, obj):
"""For the given value, return its corresponding key.""" """For the given value, return its corresponding key."""
for key, val in self.items(): for key, val in self.items():
@@ -146,26 +146,26 @@ def signal_child(service, command):
class PyWebService(win32serviceutil.ServiceFramework): class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service.""" """Python Web Service."""
_svc_name_ = "Python Web Service" _svc_name_ = "Python Web Service"
_svc_display_name_ = "Python Web Service" _svc_display_name_ = "Python Web Service"
_svc_deps_ = None # sequence of service names on which this depends _svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc" _exe_name_ = "pywebsvc"
_exe_args_ = None # Default to no arguments _exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT # Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service" _svc_description_ = "Python Web Service"
def SvcDoRun(self): def SvcDoRun(self):
from cherrypy import process from cherrypy import process
process.bus.start() process.bus.start()
process.bus.block() process.bus.block()
def SvcStop(self): def SvcStop(self):
from cherrypy import process from cherrypy import process
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
process.bus.exit() process.bus.exit()
def SvcOther(self, control): def SvcOther(self, control):
process.bus.publish(control_codes.key_for(control)) process.bus.publish(control_codes.key_for(control))
+43 -43
View File
@@ -81,21 +81,21 @@ _startup_cwd = os.getcwd()
class ChannelFailures(Exception): class ChannelFailures(Exception):
"""Exception raised when errors occur in a listener during Bus.publish().""" """Exception raised when errors occur in a listener during Bus.publish()."""
delimiter = '\n' delimiter = '\n'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4 # Don't use 'super' here; Exceptions are old-style in Py2.4
# See http://www.cherrypy.org/ticket/959 # See http://www.cherrypy.org/ticket/959
Exception.__init__(self, *args, **kwargs) Exception.__init__(self, *args, **kwargs)
self._exceptions = list() self._exceptions = list()
def handle_exception(self): def handle_exception(self):
"""Append the current exception to self.""" """Append the current exception to self."""
self._exceptions.append(sys.exc_info()[1]) self._exceptions.append(sys.exc_info()[1])
def get_instances(self): def get_instances(self):
"""Return a list of seen exception instances.""" """Return a list of seen exception instances."""
return self._exceptions[:] return self._exceptions[:]
def __str__(self): def __str__(self):
exception_strings = map(repr, self.get_instances()) exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings) return self.delimiter.join(exception_strings)
@@ -112,7 +112,7 @@ class _StateEnum(object):
name = None name = None
def __repr__(self): def __repr__(self):
return "states.%s" % self.name return "states.%s" % self.name
def __setattr__(self, key, value): def __setattr__(self, key, value):
if isinstance(value, self.State): if isinstance(value, self.State):
value.name = key value.name = key
@@ -138,19 +138,19 @@ else:
class Bus(object): class Bus(object):
"""Process state-machine and messenger for HTTP site deployment. """Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the processing from inside a listener is to raise SystemExit and stop the
whole server. whole server.
""" """
states = states states = states
state = states.STOPPED state = states.STOPPED
execv = False execv = False
max_cloexec_files = max_files max_cloexec_files = max_files
def __init__(self): def __init__(self):
self.execv = False self.execv = False
self.state = states.STOPPED self.state = states.STOPPED
@@ -158,32 +158,32 @@ class Bus(object):
[(channel, set()) for channel [(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
self._priorities = {} self._priorities = {}
def subscribe(self, channel, callback, priority=None): def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present).""" """Add the given callback at the given channel (if not present)."""
if channel not in self.listeners: if channel not in self.listeners:
self.listeners[channel] = set() self.listeners[channel] = set()
self.listeners[channel].add(callback) self.listeners[channel].add(callback)
if priority is None: if priority is None:
priority = getattr(callback, 'priority', 50) priority = getattr(callback, 'priority', 50)
self._priorities[(channel, callback)] = priority self._priorities[(channel, callback)] = priority
def unsubscribe(self, channel, callback): def unsubscribe(self, channel, callback):
"""Discard the given callback (if present).""" """Discard the given callback (if present)."""
listeners = self.listeners.get(channel) listeners = self.listeners.get(channel)
if listeners and callback in listeners: if listeners and callback in listeners:
listeners.discard(callback) listeners.discard(callback)
del self._priorities[(channel, callback)] del self._priorities[(channel, callback)]
def publish(self, channel, *args, **kwargs): def publish(self, channel, *args, **kwargs):
"""Return output of all subscribers for the given channel.""" """Return output of all subscribers for the given channel."""
if channel not in self.listeners: if channel not in self.listeners:
return [] return []
exc = ChannelFailures() exc = ChannelFailures()
output = [] output = []
items = [(self._priorities[(channel, listener)], listener) items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]] for listener in self.listeners[channel]]
try: try:
@@ -214,7 +214,7 @@ class Bus(object):
if exc: if exc:
raise exc raise exc
return output return output
def _clean_exit(self): def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running.""" """An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING: if self.state != states.EXITING:
@@ -224,11 +224,11 @@ class Bus(object):
"bus.block() after start(), or call bus.exit() before the " "bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning) "main thread exits." % self.state, RuntimeWarning)
self.exit() self.exit()
def start(self): def start(self):
"""Start all services.""" """Start all services."""
atexit.register(self._clean_exit) atexit.register(self._clean_exit)
self.state = states.STARTING self.state = states.STARTING
self.log('Bus STARTING') self.log('Bus STARTING')
try: try:
@@ -248,13 +248,13 @@ class Bus(object):
pass pass
# Re-raise the original error # Re-raise the original error
raise e_info raise e_info
def exit(self): def exit(self):
"""Stop all services and prepare to exit the process.""" """Stop all services and prepare to exit the process."""
exitstate = self.state exitstate = self.state
try: try:
self.stop() self.stop()
self.state = states.EXITING self.state = states.EXITING
self.log('Bus EXITING') self.log('Bus EXITING')
self.publish('exit') self.publish('exit')
@@ -267,31 +267,31 @@ class Bus(object):
# can't just let exceptions propagate out unhandled. # can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die. # Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE os._exit(70) # EX_SOFTWARE
if exitstate == states.STARTING: if exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to # exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case, # Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the # we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here. # process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE os._exit(70) # EX_SOFTWARE
def restart(self): def restart(self):
"""Restart the process (may close connections). """Restart the process (may close connections).
This method does not restart the process from the calling thread; This method does not restart the process from the calling thread;
instead, it stops the bus and asks the main thread to call execv. instead, it stops the bus and asks the main thread to call execv.
""" """
self.execv = True self.execv = True
self.exit() self.exit()
def graceful(self): def graceful(self):
"""Advise all services to reload.""" """Advise all services to reload."""
self.log('Bus graceful') self.log('Bus graceful')
self.publish('graceful') self.publish('graceful')
def block(self, interval=0.1): def block(self, interval=0.1):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit. """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread. This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads After waiting for the EXITING state, it also waits for all threads
to terminate, and then calls os.execv if self.execv is True. This to terminate, and then calls os.execv if self.execv is True. This
@@ -309,7 +309,7 @@ class Bus(object):
self.log('SystemExit raised: shutting down bus') self.log('SystemExit raised: shutting down bus')
self.exit() self.exit()
raise raise
# Waiting for ALL child threads to finish is necessary on OS X. # Waiting for ALL child threads to finish is necessary on OS X.
# See http://www.cherrypy.org/ticket/581. # See http://www.cherrypy.org/ticket/581.
# It's also good to let them all shut down before allowing # It's also good to let them all shut down before allowing
@@ -327,22 +327,22 @@ class Bus(object):
if not d: if not d:
self.log("Waiting for thread %s." % t.getName()) self.log("Waiting for thread %s." % t.getName())
t.join() t.join()
if self.execv: if self.execv:
self._do_execv() self._do_execv()
def wait(self, state, interval=0.1, channel=None): def wait(self, state, interval=0.1, channel=None):
"""Poll for the given state(s) at intervals; publish to channel.""" """Poll for the given state(s) at intervals; publish to channel."""
if isinstance(state, (tuple, list)): if isinstance(state, (tuple, list)):
states = state states = state
else: else:
states = [state] states = [state]
def _wait(): def _wait():
while self.state not in states: while self.state not in states:
time.sleep(interval) time.sleep(interval)
self.publish(channel) self.publish(channel)
# From http://psyco.sourceforge.net/psycoguide/bugs.html: # From http://psyco.sourceforge.net/psycoguide/bugs.html:
# "The compiled machine code does not include the regular polling # "The compiled machine code does not include the regular polling
# done by Python, meaning that a KeyboardInterrupt will not be # done by Python, meaning that a KeyboardInterrupt will not be
@@ -353,18 +353,18 @@ class Bus(object):
sys.modules['psyco'].cannotcompile(_wait) sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError): except (KeyError, AttributeError):
pass pass
_wait() _wait()
def _do_execv(self): def _do_execv(self):
"""Re-execute the current process. """Re-execute the current process.
This must be called from the main thread, because certain platforms This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well. (OS X) don't allow execv to be called in a child thread very well.
""" """
args = sys.argv[:] args = sys.argv[:]
self.log('Re-spawning %s' % ' '.join(args)) self.log('Re-spawning %s' % ' '.join(args))
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart from _systemrestart import SystemRestart
raise SystemRestart raise SystemRestart
@@ -377,16 +377,16 @@ class Bus(object):
if self.max_cloexec_files: if self.max_cloexec_files:
self._set_cloexec() self._set_cloexec()
os.execv(sys.executable, args) os.execv(sys.executable, args)
def _set_cloexec(self): def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err). """Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting platforms which support it, it represents the max open files setting
for the operating system. This function will be called just before for the operating system. This function will be called just before
the process is restarted via os.execv() to prevent open files the process is restarted via os.execv() to prevent open files
from persisting into the new process. from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior. Set self.max_cloexec_files to 0 to disable this behavior.
""" """
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
@@ -395,7 +395,7 @@ class Bus(object):
except IOError: except IOError:
continue continue
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def stop(self): def stop(self):
"""Stop all services.""" """Stop all services."""
self.state = states.STOPPING self.state = states.STOPPING
@@ -403,7 +403,7 @@ class Bus(object):
self.publish('stop') self.publish('stop')
self.state = states.STOPPED self.state = states.STOPPED
self.log('Bus STOPPED') self.log('Bus STOPPED')
def start_with_callback(self, func, args=None, kwargs=None): def start_with_callback(self, func, args=None, kwargs=None):
"""Start 'func' in a new thread T, then start self (and return T).""" """Start 'func' in a new thread T, then start self (and return T)."""
if args is None: if args is None:
@@ -411,18 +411,18 @@ class Bus(object):
if kwargs is None: if kwargs is None:
kwargs = {} kwargs = {}
args = (func,) + args args = (func,) + args
def _callback(func, *a, **kw): def _callback(func, *a, **kw):
self.wait(states.STARTED) self.wait(states.STARTED)
func(*a, **kw) func(*a, **kw)
t = threading.Thread(target=_callback, args=args, kwargs=kwargs) t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
t.setName('Bus Callback ' + t.getName()) t.setName('Bus Callback ' + t.getName())
t.start() t.start()
self.start() self.start()
return t return t
def log(self, msg="", level=20, traceback=False): def log(self, msg="", level=20, traceback=False):
"""Log the given message. Append the last traceback if requested.""" """Log the given message. Append the last traceback if requested."""
if traceback: if traceback:
+5 -5
View File
@@ -20,10 +20,10 @@ local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class Root: class Root:
_cp_config = {'tools.log_tracebacks.on': True, _cp_config = {'tools.log_tracebacks.on': True,
} }
def index(self): def index(self):
return """<html> return """<html>
<body>Try some <a href='%s?a=7'>other</a> path, <body>Try some <a href='%s?a=7'>other</a> path,
@@ -33,11 +33,11 @@ Or, just look at the pretty picture:<br />
</body></html>""" % (url("other"), url("else"), </body></html>""" % (url("other"), url("else"),
url("files/made_with_cherrypy_small.png")) url("files/made_with_cherrypy_small.png"))
index.exposed = True index.exposed = True
def default(self, *args, **kwargs): def default(self, *args, **kwargs):
return "args: %s kwargs: %s" % (args, kwargs) return "args: %s kwargs: %s" % (args, kwargs)
default.exposed = True default.exposed = True
def other(self, a=2, b='bananas', c=None): def other(self, a=2, b='bananas', c=None):
cherrypy.response.headers['Content-Type'] = 'text/plain' cherrypy.response.headers['Content-Type'] = 'text/plain'
if c is None: if c is None:
@@ -45,7 +45,7 @@ Or, just look at the pretty picture:<br />
else: else:
return "Have %d %s, %s." % (int(a), b, c) return "Have %d %s, %s." % (int(a), b, c)
other.exposed = True other.exposed = True
files = cherrypy.tools.staticdir.handler( files = cherrypy.tools.staticdir.handler(
section="/files", section="/files",
dir=os.path.join(local_dir, "static"), dir=os.path.join(local_dir, "static"),
+7 -7
View File
@@ -26,24 +26,24 @@ from cherrypy import wsgiserver
class BuiltinSSLAdapter(wsgiserver.SSLAdapter): class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating Python's builtin ssl module with CherryPy.""" """A wrapper for integrating Python's builtin ssl module with CherryPy."""
certificate = None certificate = None
"""The filename of the server SSL certificate.""" """The filename of the server SSL certificate."""
private_key = None private_key = None
"""The filename of the server's private key file.""" """The filename of the server's private key file."""
def __init__(self, certificate, private_key, certificate_chain=None): def __init__(self, certificate, private_key, certificate_chain=None):
if ssl is None: if ssl is None:
raise ImportError("You must install the ssl module to use HTTPS.") raise ImportError("You must install the ssl module to use HTTPS.")
self.certificate = certificate self.certificate = certificate
self.private_key = private_key self.private_key = private_key
self.certificate_chain = certificate_chain self.certificate_chain = certificate_chain
def bind(self, sock): def bind(self, sock):
"""Wrap and return the given socket.""" """Wrap and return the given socket."""
return sock return sock
def wrap(self, sock): def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries.""" """Wrap and return the given socket, plus WSGI environ entries."""
try: try:
@@ -67,7 +67,7 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
return None, {} return None, {}
raise raise
return s, self.get_environ(s) return s, self.get_environ(s)
# TODO: fill this out more with mod ssl env # TODO: fill this out more with mod ssl env
def get_environ(self, sock): def get_environ(self, sock):
"""Create WSGI environ entries to be merged into each request.""" """Create WSGI environ entries to be merged into each request."""
@@ -81,7 +81,7 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
## SSL_VERSION_LIBRARY string The OpenSSL program version ## SSL_VERSION_LIBRARY string The OpenSSL program version
} }
return ssl_environ return ssl_environ
if sys.version_info >= (3, 0): if sys.version_info >= (3, 0):
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
return wsgiserver.CP_makefile(sock, mode, bufsize) return wsgiserver.CP_makefile(sock, mode, bufsize)
+31 -31
View File
@@ -45,13 +45,13 @@ except ImportError:
class SSL_fileobject(wsgiserver.CP_fileobject): class SSL_fileobject(wsgiserver.CP_fileobject):
"""SSL file object attached to a socket object.""" """SSL file object attached to a socket object."""
ssl_timeout = 3 ssl_timeout = 3
ssl_retry = .01 ssl_retry = .01
def _safe_call(self, is_reader, call, *args, **kwargs): def _safe_call(self, is_reader, call, *args, **kwargs):
"""Wrap the given call with SSL error-trapping. """Wrap the given call with SSL error-trapping.
is_reader: if False EOF errors will be raised. If True, EOF errors is_reader: if False EOF errors will be raised. If True, EOF errors
will return "" (to emulate normal sockets). will return "" (to emulate normal sockets).
""" """
@@ -70,7 +70,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
except SSL.SysCallError, e: except SSL.SysCallError, e:
if is_reader and e.args == (-1, 'Unexpected EOF'): if is_reader and e.args == (-1, 'Unexpected EOF'):
return "" return ""
errnum = e.args[0] errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore: if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return "" return ""
@@ -78,24 +78,24 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
except SSL.Error, e: except SSL.Error, e:
if is_reader and e.args == (-1, 'Unexpected EOF'): if is_reader and e.args == (-1, 'Unexpected EOF'):
return "" return ""
thirdarg = None thirdarg = None
try: try:
thirdarg = e.args[0][0][2] thirdarg = e.args[0][0][2]
except IndexError: except IndexError:
pass pass
if thirdarg == 'http request': if thirdarg == 'http request':
# The client is talking HTTP to an HTTPS server. # The client is talking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError() raise wsgiserver.NoSSLError()
raise wsgiserver.FatalSSLAlert(*e.args) raise wsgiserver.FatalSSLAlert(*e.args)
except: except:
raise raise
if time.time() - start > self.ssl_timeout: if time.time() - start > self.ssl_timeout:
raise socket.timeout("timed out") raise socket.timeout("timed out")
def recv(self, *args, **kwargs): def recv(self, *args, **kwargs):
buf = [] buf = []
r = super(SSL_fileobject, self).recv r = super(SSL_fileobject, self).recv
@@ -105,7 +105,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
p = self._sock.pending() p = self._sock.pending()
if not p: if not p:
return "".join(buf) return "".join(buf)
def sendall(self, *args, **kwargs): def sendall(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).sendall, return self._safe_call(False, super(SSL_fileobject, self).sendall,
*args, **kwargs) *args, **kwargs)
@@ -117,14 +117,14 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
class SSLConnection: class SSLConnection:
"""A thread-safe wrapper for an SSL.Connection. """A thread-safe wrapper for an SSL.Connection.
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
""" """
def __init__(self, *args): def __init__(self, *args):
self._ssl_conn = SSL.Connection(*args) self._ssl_conn = SSL.Connection(*args)
self._lock = threading.RLock() self._lock = threading.RLock()
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
'renegotiate', 'bind', 'listen', 'connect', 'accept', 'renegotiate', 'bind', 'listen', 'connect', 'accept',
'setblocking', 'fileno', 'close', 'get_cipher_list', 'setblocking', 'fileno', 'close', 'get_cipher_list',
@@ -140,7 +140,7 @@ class SSLConnection:
finally: finally:
self._lock.release() self._lock.release()
""" % (f, f)) """ % (f, f))
def shutdown(self, *args): def shutdown(self, *args):
self._lock.acquire() self._lock.acquire()
try: try:
@@ -152,32 +152,32 @@ class SSLConnection:
class pyOpenSSLAdapter(wsgiserver.SSLAdapter): class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating pyOpenSSL with CherryPy.""" """A wrapper for integrating pyOpenSSL with CherryPy."""
context = None context = None
"""An instance of SSL.Context.""" """An instance of SSL.Context."""
certificate = None certificate = None
"""The filename of the server SSL certificate.""" """The filename of the server SSL certificate."""
private_key = None private_key = None
"""The filename of the server's private key file.""" """The filename of the server's private key file."""
certificate_chain = None certificate_chain = None
"""Optional. The filename of CA's intermediate certificate bundle. """Optional. The filename of CA's intermediate certificate bundle.
This is needed for cheaper "chained root" SSL certificates, and should be This is needed for cheaper "chained root" SSL certificates, and should be
left as None if not required.""" left as None if not required."""
def __init__(self, certificate, private_key, certificate_chain=None): def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is None: if SSL is None:
raise ImportError("You must install pyOpenSSL to use HTTPS.") raise ImportError("You must install pyOpenSSL to use HTTPS.")
self.context = None self.context = None
self.certificate = certificate self.certificate = certificate
self.private_key = private_key self.private_key = private_key
self.certificate_chain = certificate_chain self.certificate_chain = certificate_chain
self._environ = None self._environ = None
def bind(self, sock): def bind(self, sock):
"""Wrap and return the given socket.""" """Wrap and return the given socket."""
if self.context is None: if self.context is None:
@@ -185,11 +185,11 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
conn = SSLConnection(self.context, sock) conn = SSLConnection(self.context, sock)
self._environ = self.get_environ() self._environ = self.get_environ()
return conn return conn
def wrap(self, sock): def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries.""" """Wrap and return the given socket, plus WSGI environ entries."""
return sock, self._environ.copy() return sock, self._environ.copy()
def get_context(self): def get_context(self):
"""Return an SSL.Context from self attributes.""" """Return an SSL.Context from self attributes."""
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
@@ -199,7 +199,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
c.load_verify_locations(self.certificate_chain) c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate) c.use_certificate_file(self.certificate)
return c return c
def get_environ(self): def get_environ(self):
"""Return WSGI environ entries to be merged into each request.""" """Return WSGI environ entries to be merged into each request."""
ssl_environ = { ssl_environ = {
@@ -210,7 +210,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
## SSL_VERSION_INTERFACE string The mod_ssl program version ## SSL_VERSION_INTERFACE string The mod_ssl program version
## SSL_VERSION_LIBRARY string The OpenSSL program version ## SSL_VERSION_LIBRARY string The OpenSSL program version
} }
if self.certificate: if self.certificate:
# Server certificate attributes # Server certificate attributes
cert = open(self.certificate, 'rb').read() cert = open(self.certificate, 'rb').read()
@@ -221,17 +221,17 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
## 'SSL_SERVER_V_START': Validity of server's certificate (start time), ## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
## 'SSL_SERVER_V_END': Validity of server's certificate (end time), ## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
}) })
for prefix, dn in [("I", cert.get_issuer()), for prefix, dn in [("I", cert.get_issuer()),
("S", cert.get_subject())]: ("S", cert.get_subject())]:
# X509Name objects don't seem to have a way to get the # X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead, # complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>" # because str(dn) == "<X509Name object '/C=US/ST=...'>"
dnstr = str(dn)[18:-2] dnstr = str(dn)[18:-2]
wsgikey = 'SSL_SERVER_%s_DN' % prefix wsgikey = 'SSL_SERVER_%s_DN' % prefix
ssl_environ[wsgikey] = dnstr ssl_environ[wsgikey] = dnstr
# The DN should be of the form: /k1=v1/k2=v2, but we must allow # The DN should be of the form: /k1=v1/k2=v2, but we must allow
# for any value to contain slashes itself (in a URL). # for any value to contain slashes itself (in a URL).
while dnstr: while dnstr:
@@ -242,9 +242,9 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
if key and value: if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
ssl_environ[wsgikey] = value ssl_environ[wsgikey] = value
return ssl_environ return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1): def makefile(self, sock, mode='r', bufsize=-1):
if SSL and isinstance(sock, SSL.ConnectionType): if SSL and isinstance(sock, SSL.ConnectionType):
timeout = sock.gettimeout() timeout = sock.gettimeout()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+194 -194
View File
File diff suppressed because it is too large Load Diff
+105 -105
View File
@@ -89,7 +89,7 @@ try:
except (NameError, AttributeError): except (NameError, AttributeError):
import string import string
_maketrans = string.maketrans _maketrans = string.maketrans
# base64 support for Atom feeds that contain embedded binary data # base64 support for Atom feeds that contain embedded binary data
try: try:
import base64, binascii import base64, binascii
@@ -240,7 +240,7 @@ if sgmllib.endbracket.search(' <').start(0):
if match is not None: if match is not None:
# Returning a new object in the calling thread's context # Returning a new object in the calling thread's context
# resolves a thread-safety. # resolves a thread-safety.
return EndBracketMatch(match) return EndBracketMatch(match)
return None return None
class EndBracketMatch: class EndBracketMatch:
def __init__(self, match): def __init__(self, match):
@@ -334,7 +334,7 @@ class FeedParserDict(UserDict):
if not self.has_key(key): if not self.has_key(key):
self[key] = value self[key] = value
return self[key] return self[key]
def has_key(self, key): def has_key(self, key):
try: try:
return hasattr(self, key) or UserDict.__contains__(self, key) return hasattr(self, key) or UserDict.__contains__(self, key)
@@ -343,7 +343,7 @@ class FeedParserDict(UserDict):
# This alias prevents the 2to3 tool from changing the semantics of the # This alias prevents the 2to3 tool from changing the semantics of the
# __contains__ function below and exhausting the maximum recursion depth # __contains__ function below and exhausting the maximum recursion depth
__has_key = has_key __has_key = has_key
def __getattr__(self, key): def __getattr__(self, key):
try: try:
return self.__dict__[key] return self.__dict__[key]
@@ -398,7 +398,7 @@ def _ebcdic_to_ascii(s):
_ebcdic_to_ascii_map = _maketrans( \ _ebcdic_to_ascii_map = _maketrans( \
_l2bytes(range(256)), _l2bytes(emap)) _l2bytes(range(256)), _l2bytes(emap))
return s.translate(_ebcdic_to_ascii_map) return s.translate(_ebcdic_to_ascii_map)
_cp1252 = { _cp1252 = {
unichr(128): unichr(8364), # euro sign unichr(128): unichr(8364), # euro sign
unichr(130): unichr(8218), # single low-9 quotation mark unichr(130): unichr(8218), # single low-9 quotation mark
@@ -451,7 +451,7 @@ class _FeedParserMixin:
'http://purl.org/atom/ns#': '', 'http://purl.org/atom/ns#': '',
'http://www.w3.org/2005/Atom': '', 'http://www.w3.org/2005/Atom': '',
'http://purl.org/rss/1.0/modules/rss091#': '', 'http://purl.org/rss/1.0/modules/rss091#': '',
'http://webns.net/mvcb/': 'admin', 'http://webns.net/mvcb/': 'admin',
'http://purl.org/rss/1.0/modules/aggregation/': 'ag', 'http://purl.org/rss/1.0/modules/aggregation/': 'ag',
'http://purl.org/rss/1.0/modules/annotate/': 'annotate', 'http://purl.org/rss/1.0/modules/annotate/': 'annotate',
@@ -508,7 +508,7 @@ class _FeedParserMixin:
can_contain_relative_uris = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description'] can_contain_relative_uris = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
can_contain_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description'] can_contain_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
html_types = ['text/html', 'application/xhtml+xml'] html_types = ['text/html', 'application/xhtml+xml']
def __init__(self, baseuri=None, baselang=None, encoding='utf-8'): def __init__(self, baseuri=None, baselang=None, encoding='utf-8'):
if _debug: sys.stderr.write('initializing FeedParser\n') if _debug: sys.stderr.write('initializing FeedParser\n')
if not self._matchnamespaces: if not self._matchnamespaces:
@@ -554,7 +554,7 @@ class _FeedParserMixin:
# strict xml parsers do -- account for this difference # strict xml parsers do -- account for this difference
if isinstance(self, _LooseFeedParser): if isinstance(self, _LooseFeedParser):
attrs = [(k, v.replace('&amp;', '&')) for k, v in attrs] attrs = [(k, v.replace('&amp;', '&')) for k, v in attrs]
# track xml:base and xml:lang # track xml:base and xml:lang
attrsD = dict(attrs) attrsD = dict(attrs)
baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri
@@ -582,7 +582,7 @@ class _FeedParserMixin:
self.lang = lang self.lang = lang
self.basestack.append(self.baseuri) self.basestack.append(self.baseuri)
self.langstack.append(lang) self.langstack.append(lang)
# track namespaces # track namespaces
for prefix, uri in attrs: for prefix, uri in attrs:
if prefix.startswith('xmlns:'): if prefix.startswith('xmlns:'):
@@ -620,7 +620,7 @@ class _FeedParserMixin:
self.intextinput = 0 self.intextinput = 0
if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'): if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
self.inimage = 0 self.inimage = 0
# call special handler (if defined) or default handler # call special handler (if defined) or default handler
methodname = '_start_' + prefix + suffix methodname = '_start_' + prefix + suffix
try: try:
@@ -754,7 +754,7 @@ class _FeedParserMixin:
elif contentType == 'xhtml': elif contentType == 'xhtml':
contentType = 'application/xhtml+xml' contentType = 'application/xhtml+xml'
return contentType return contentType
def trackNamespace(self, prefix, uri): def trackNamespace(self, prefix, uri):
loweruri = uri.lower() loweruri = uri.lower()
if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version: if (prefix, loweruri) == (None, 'http://my.netscape.com/rdf/simple/0.9/') and not self.version:
@@ -775,7 +775,7 @@ class _FeedParserMixin:
def resolveURI(self, uri): def resolveURI(self, uri):
return _urljoin(self.baseuri or '', uri) return _urljoin(self.baseuri or '', uri)
def decodeEntities(self, element, data): def decodeEntities(self, element, data):
return data return data
@@ -788,7 +788,7 @@ class _FeedParserMixin:
def pop(self, element, stripWhitespace=1): def pop(self, element, stripWhitespace=1):
if not self.elementstack: return if not self.elementstack: return
if self.elementstack[-1][0] != element: return if self.elementstack[-1][0] != element: return
element, expectingText, pieces = self.elementstack.pop() element, expectingText, pieces = self.elementstack.pop()
if self.version == 'atom10' and self.contentparams.get('type','text') == 'application/xhtml+xml': if self.version == 'atom10' and self.contentparams.get('type','text') == 'application/xhtml+xml':
@@ -833,11 +833,11 @@ class _FeedParserMixin:
# In Python 3, base64 takes and outputs bytes, not str # In Python 3, base64 takes and outputs bytes, not str
# This may not be the most correct way to accomplish this # This may not be the most correct way to accomplish this
output = _base64decode(output.encode('utf-8')).decode('utf-8') output = _base64decode(output.encode('utf-8')).decode('utf-8')
# resolve relative URIs # resolve relative URIs
if (element in self.can_be_relative_uri) and output: if (element in self.can_be_relative_uri) and output:
output = self.resolveURI(output) output = self.resolveURI(output)
# decode entities within embedded markup # decode entities within embedded markup
if not self.contentparams.get('base64', 0): if not self.contentparams.get('base64', 0):
output = self.decodeEntities(element, output) output = self.decodeEntities(element, output)
@@ -860,7 +860,7 @@ class _FeedParserMixin:
if is_htmlish and RESOLVE_RELATIVE_URIS: if is_htmlish and RESOLVE_RELATIVE_URIS:
if element in self.can_contain_relative_uris: if element in self.can_contain_relative_uris:
output = _resolveRelativeURIs(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html')) output = _resolveRelativeURIs(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html'))
# parse microformats # parse microformats
# (must do this before sanitizing because some microformats # (must do this before sanitizing because some microformats
# rely on elements that we sanitize) # rely on elements that we sanitize)
@@ -876,7 +876,7 @@ class _FeedParserMixin:
vcard = mfresults.get('vcard') vcard = mfresults.get('vcard')
if vcard: if vcard:
self._getContext()['vcard'] = vcard self._getContext()['vcard'] = vcard
# sanitize embedded markup # sanitize embedded markup
if is_htmlish and SANITIZE_HTML: if is_htmlish and SANITIZE_HTML:
if element in self.can_contain_dangerous_markup: if element in self.can_contain_dangerous_markup:
@@ -906,7 +906,7 @@ class _FeedParserMixin:
if element == 'title' and self.hasTitle: if element == 'title' and self.hasTitle:
return output return output
# store output in appropriate place(s) # store output in appropriate place(s)
if self.inentry and not self.insource: if self.inentry and not self.insource:
if element == 'content': if element == 'content':
@@ -962,7 +962,7 @@ class _FeedParserMixin:
self.incontent -= 1 self.incontent -= 1
self.contentparams.clear() self.contentparams.clear()
return value return value
# a number of elements in a number of RSS variants are nominally plain # a number of elements in a number of RSS variants are nominally plain
# text, but this is routinely ignored. This is an attempt to detect # text, but this is routinely ignored. This is an attempt to detect
# the most common cases. As false positives often result in silent # the most common cases. As false positives often result in silent
@@ -993,7 +993,7 @@ class _FeedParserMixin:
prefix = self.namespacemap.get(prefix, prefix) prefix = self.namespacemap.get(prefix, prefix)
name = prefix + ':' + suffix name = prefix + ':' + suffix
return name return name
def _getAttribute(self, attrsD, name): def _getAttribute(self, attrsD, name):
return attrsD.get(self._mapToStandardPrefix(name)) return attrsD.get(self._mapToStandardPrefix(name))
@@ -1021,7 +1021,7 @@ class _FeedParserMixin:
pass pass
attrsD['href'] = href attrsD['href'] = href
return attrsD return attrsD
def _save(self, key, value, overwrite=False): def _save(self, key, value, overwrite=False):
context = self._getContext() context = self._getContext()
if overwrite: if overwrite:
@@ -1046,7 +1046,7 @@ class _FeedParserMixin:
self.version = 'rss20' self.version = 'rss20'
else: else:
self.version = 'rss' self.version = 'rss'
def _start_dlhottitles(self, attrsD): def _start_dlhottitles(self, attrsD):
self.version = 'hotrss' self.version = 'hotrss'
@@ -1064,7 +1064,7 @@ class _FeedParserMixin:
self._start_link({}) self._start_link({})
self.elementstack[-1][-1] = attrsD['href'] self.elementstack[-1][-1] = attrsD['href']
self._end_link() self._end_link()
def _start_feed(self, attrsD): def _start_feed(self, attrsD):
self.infeed = 1 self.infeed = 1
versionmap = {'0.1': 'atom01', versionmap = {'0.1': 'atom01',
@@ -1081,7 +1081,7 @@ class _FeedParserMixin:
def _end_channel(self): def _end_channel(self):
self.infeed = 0 self.infeed = 0
_end_feed = _end_channel _end_feed = _end_channel
def _start_image(self, attrsD): def _start_image(self, attrsD):
context = self._getContext() context = self._getContext()
if not self.inentry: if not self.inentry:
@@ -1089,7 +1089,7 @@ class _FeedParserMixin:
self.inimage = 1 self.inimage = 1
self.hasTitle = 0 self.hasTitle = 0
self.push('image', 0) self.push('image', 0)
def _end_image(self): def _end_image(self):
self.pop('image') self.pop('image')
self.inimage = 0 self.inimage = 0
@@ -1101,7 +1101,7 @@ class _FeedParserMixin:
self.hasTitle = 0 self.hasTitle = 0
self.push('textinput', 0) self.push('textinput', 0)
_start_textInput = _start_textinput _start_textInput = _start_textinput
def _end_textinput(self): def _end_textinput(self):
self.pop('textinput') self.pop('textinput')
self.intextinput = 0 self.intextinput = 0
@@ -1301,7 +1301,7 @@ class _FeedParserMixin:
self.popContent('subtitle') self.popContent('subtitle')
_end_tagline = _end_subtitle _end_tagline = _end_subtitle
_end_itunes_subtitle = _end_subtitle _end_itunes_subtitle = _end_subtitle
def _start_rights(self, attrsD): def _start_rights(self, attrsD):
self.pushContent('rights', attrsD, 'text/plain', 1) self.pushContent('rights', attrsD, 'text/plain', 1)
_start_dc_rights = _start_rights _start_dc_rights = _start_rights
@@ -1399,7 +1399,7 @@ class _FeedParserMixin:
attrsD['rel']='license' attrsD['rel']='license'
if value: attrsD['href']=value if value: attrsD['href']=value
context.setdefault('links', []).append(attrsD) context.setdefault('links', []).append(attrsD)
def _start_creativecommons_license(self, attrsD): def _start_creativecommons_license(self, attrsD):
self.push('license', 1) self.push('license', 1)
_start_creativeCommons_license = _start_creativecommons_license _start_creativeCommons_license = _start_creativecommons_license
@@ -1420,7 +1420,7 @@ class _FeedParserMixin:
value = FeedParserDict({'relationships': relationships, 'href': href, 'name': name}) value = FeedParserDict({'relationships': relationships, 'href': href, 'name': name})
if value not in xfn: if value not in xfn:
xfn.append(value) xfn.append(value)
def _addTag(self, term, scheme, label): def _addTag(self, term, scheme, label):
context = self._getContext() context = self._getContext()
tags = context.setdefault('tags', []) tags = context.setdefault('tags', [])
@@ -1438,7 +1438,7 @@ class _FeedParserMixin:
self.push('category', 1) self.push('category', 1)
_start_dc_subject = _start_category _start_dc_subject = _start_category
_start_keywords = _start_category _start_keywords = _start_category
def _start_media_category(self, attrsD): def _start_media_category(self, attrsD):
attrsD.setdefault('scheme', 'http://search.yahoo.com/mrss/category_schema') attrsD.setdefault('scheme', 'http://search.yahoo.com/mrss/category_schema')
self._start_category(attrsD) self._start_category(attrsD)
@@ -1446,11 +1446,11 @@ class _FeedParserMixin:
def _end_itunes_keywords(self): def _end_itunes_keywords(self):
for term in self.pop('itunes_keywords').split(): for term in self.pop('itunes_keywords').split():
self._addTag(term, 'http://www.itunes.com/', None) self._addTag(term, 'http://www.itunes.com/', None)
def _start_itunes_category(self, attrsD): def _start_itunes_category(self, attrsD):
self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None) self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
self.push('category', 1) self.push('category', 1)
def _end_category(self): def _end_category(self):
value = self.pop('category') value = self.pop('category')
if not value: return if not value: return
@@ -1467,7 +1467,7 @@ class _FeedParserMixin:
def _start_cloud(self, attrsD): def _start_cloud(self, attrsD):
self._getContext()['cloud'] = FeedParserDict(attrsD) self._getContext()['cloud'] = FeedParserDict(attrsD)
def _start_link(self, attrsD): def _start_link(self, attrsD):
attrsD.setdefault('rel', 'alternate') attrsD.setdefault('rel', 'alternate')
if attrsD['rel'] == 'self': if attrsD['rel'] == 'self':
@@ -1568,7 +1568,7 @@ class _FeedParserMixin:
context = self._getContext() context = self._getContext()
if context.has_key('generator_detail'): if context.has_key('generator_detail'):
context['generator_detail']['name'] = value context['generator_detail']['name'] = value
def _start_admin_generatoragent(self, attrsD): def _start_admin_generatoragent(self, attrsD):
self.push('generator', 1) self.push('generator', 1)
value = self._getAttribute(attrsD, 'rdf:resource') value = self._getAttribute(attrsD, 'rdf:resource')
@@ -1583,7 +1583,7 @@ class _FeedParserMixin:
if value: if value:
self.elementstack[-1][2].append(value) self.elementstack[-1][2].append(value)
self.pop('errorreportsto') self.pop('errorreportsto')
def _start_summary(self, attrsD): def _start_summary(self, attrsD):
context = self._getContext() context = self._getContext()
if context.has_key('summary'): if context.has_key('summary'):
@@ -1601,13 +1601,13 @@ class _FeedParserMixin:
self.popContent(self._summaryKey or 'summary') self.popContent(self._summaryKey or 'summary')
self._summaryKey = None self._summaryKey = None
_end_itunes_summary = _end_summary _end_itunes_summary = _end_summary
def _start_enclosure(self, attrsD): def _start_enclosure(self, attrsD):
attrsD = self._itsAnHrefDamnIt(attrsD) attrsD = self._itsAnHrefDamnIt(attrsD)
context = self._getContext() context = self._getContext()
attrsD['rel']='enclosure' attrsD['rel']='enclosure'
context.setdefault('links', []).append(FeedParserDict(attrsD)) context.setdefault('links', []).append(FeedParserDict(attrsD))
def _start_source(self, attrsD): def _start_source(self, attrsD):
if 'url' in attrsD: if 'url' in attrsD:
# This means that we're processing a source element from an RSS 2.0 feed # This means that we're processing a source element from an RSS 2.0 feed
@@ -1659,7 +1659,7 @@ class _FeedParserMixin:
if attrsD.get('href'): if attrsD.get('href'):
self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')}) self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
_start_itunes_link = _start_itunes_image _start_itunes_link = _start_itunes_image
def _end_itunes_block(self): def _end_itunes_block(self):
value = self.pop('itunes_block', 0) value = self.pop('itunes_block', 0)
self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0 self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0
@@ -1718,12 +1718,12 @@ if _XML_AVAILABLE:
self.bozo = 0 self.bozo = 0
self.exc = None self.exc = None
self.decls = {} self.decls = {}
def startPrefixMapping(self, prefix, uri): def startPrefixMapping(self, prefix, uri):
self.trackNamespace(prefix, uri) self.trackNamespace(prefix, uri)
if uri == 'http://www.w3.org/1999/xlink': if uri == 'http://www.w3.org/1999/xlink':
self.decls['xmlns:'+prefix] = uri self.decls['xmlns:'+prefix] = uri
def startElementNS(self, name, qname, attrs): def startElementNS(self, name, qname, attrs):
namespace, localname = name namespace, localname = name
lowernamespace = str(namespace or '').lower() lowernamespace = str(namespace or '').lower()
@@ -1805,7 +1805,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
special = re.compile('''[<>'"]''') special = re.compile('''[<>'"]''')
bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)")
elements_no_end_tag = [ elements_no_end_tag = [
'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame',
'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param',
'source', 'track', 'wbr' 'source', 'track', 'wbr'
] ]
@@ -1837,7 +1837,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
def feed(self, data): def feed(self, data):
data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'&lt;!\1', data) data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'&lt;!\1', data)
#data = re.sub(r'<(\S+?)\s*?/>', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace #data = re.sub(r'<(\S+?)\s*?/>', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data) data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
data = data.replace('&#39;', "'") data = data.replace('&#39;', "'")
data = data.replace('&#34;', '"') data = data.replace('&#34;', '"')
try: try:
@@ -1910,7 +1910,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:])
else: else:
self.pieces.append('&#%(ref)s;' % locals()) self.pieces.append('&#%(ref)s;' % locals())
def handle_entityref(self, ref): def handle_entityref(self, ref):
# called for each entity reference, e.g. for '&copy;', ref will be 'copy' # called for each entity reference, e.g. for '&copy;', ref will be 'copy'
# Reconstruct the original entity reference. # Reconstruct the original entity reference.
@@ -1925,12 +1925,12 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
# Store the original text verbatim. # Store the original text verbatim.
if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_data, text=%s\n' % text) if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_data, text=%s\n' % text)
self.pieces.append(text) self.pieces.append(text)
def handle_comment(self, text): def handle_comment(self, text):
# called for each HTML comment, e.g. <!-- insert Javascript code here --> # called for each HTML comment, e.g. <!-- insert Javascript code here -->
# Reconstruct the original comment. # Reconstruct the original comment.
self.pieces.append('<!--%(text)s-->' % locals()) self.pieces.append('<!--%(text)s-->' % locals())
def handle_pi(self, text): def handle_pi(self, text):
# called for each processing instruction, e.g. <?instruction> # called for each processing instruction, e.g. <?instruction>
# Reconstruct original processing instruction. # Reconstruct original processing instruction.
@@ -1942,7 +1942,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
# "http://www.w3.org/TR/html4/loose.dtd"> # "http://www.w3.org/TR/html4/loose.dtd">
# Reconstruct original DOCTYPE # Reconstruct original DOCTYPE
self.pieces.append('<!%(text)s>' % locals()) self.pieces.append('<!%(text)s>' % locals())
_new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
def _scan_name(self, i, declstartpos): def _scan_name(self, i, declstartpos):
rawdata = self.rawdata rawdata = self.rawdata
@@ -2006,7 +2006,7 @@ class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
data = data.replace('&quot;', '"') data = data.replace('&quot;', '"')
data = data.replace('&apos;', "'") data = data.replace('&apos;', "'")
return data return data
def strattrs(self, attrs): def strattrs(self, attrs):
return ''.join([' %s="%s"' % (n,v.replace('"','&quot;')) for n,v in attrs]) return ''.join([' %s="%s"' % (n,v.replace('"','&quot;')) for n,v in attrs])
@@ -2030,12 +2030,12 @@ class _MicroformatsParser:
self.enclosures = [] self.enclosures = []
self.xfn = [] self.xfn = []
self.vcard = None self.vcard = None
def vcardEscape(self, s): def vcardEscape(self, s):
if type(s) in (type(''), type(u'')): if type(s) in (type(''), type(u'')):
s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n') s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n')
return s return s
def vcardFold(self, s): def vcardFold(self, s):
s = re.sub(';+$', '', s) s = re.sub(';+$', '', s)
sFolded = '' sFolded = ''
@@ -2051,14 +2051,14 @@ class _MicroformatsParser:
def normalize(self, s): def normalize(self, s):
return re.sub(r'\s+', ' ', s).strip() return re.sub(r'\s+', ' ', s).strip()
def unique(self, aList): def unique(self, aList):
results = [] results = []
for element in aList: for element in aList:
if element not in results: if element not in results:
results.append(element) results.append(element)
return results return results
def toISO8601(self, dt): def toISO8601(self, dt):
return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt) return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt)
@@ -2148,21 +2148,21 @@ class _MicroformatsParser:
def findVCards(self, elmRoot, bAgentParsing=0): def findVCards(self, elmRoot, bAgentParsing=0):
sVCards = '' sVCards = ''
if not bAgentParsing: if not bAgentParsing:
arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1) arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1)
else: else:
arCards = [elmRoot] arCards = [elmRoot]
for elmCard in arCards: for elmCard in arCards:
arLines = [] arLines = []
def processSingleString(sProperty): def processSingleString(sProperty):
sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding) sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding)
if sValue: if sValue:
arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue)) arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue))
return sValue or u'' return sValue or u''
def processSingleURI(sProperty): def processSingleURI(sProperty):
sValue = self.getPropertyValue(elmCard, sProperty, self.URI) sValue = self.getPropertyValue(elmCard, sProperty, self.URI)
if sValue: if sValue:
@@ -2185,7 +2185,7 @@ class _MicroformatsParser:
if sContentType: if sContentType:
sContentType = ';TYPE=' + sContentType.upper() sContentType = ';TYPE=' + sContentType.upper()
arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue)) arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue))
def processTypeValue(sProperty, arDefaultType, arForceType=None): def processTypeValue(sProperty, arDefaultType, arForceType=None):
arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1) arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1)
for elmResult in arResults: for elmResult in arResults:
@@ -2197,7 +2197,7 @@ class _MicroformatsParser:
sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0) sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0)
if sValue: if sValue:
arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue)) arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue))
# AGENT # AGENT
# must do this before all other properties because it is destructive # must do this before all other properties because it is destructive
# (removes nested class="vcard" nodes so they don't interfere with # (removes nested class="vcard" nodes so they don't interfere with
@@ -2216,10 +2216,10 @@ class _MicroformatsParser:
sAgentValue = self.getPropertyValue(elmAgent, 'value', self.URI, bAutoEscape=1); sAgentValue = self.getPropertyValue(elmAgent, 'value', self.URI, bAutoEscape=1);
if sAgentValue: if sAgentValue:
arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue)) arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue))
# FN (full name) # FN (full name)
sFN = processSingleString('fn') sFN = processSingleString('fn')
# N (name) # N (name)
elmName = self.getPropertyValue(elmCard, 'n') elmName = self.getPropertyValue(elmCard, 'n')
if elmName: if elmName:
@@ -2228,7 +2228,7 @@ class _MicroformatsParser:
arAdditionalNames = self.getPropertyValue(elmName, 'additional-name', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'additional-names', self.STRING, 1, 1) arAdditionalNames = self.getPropertyValue(elmName, 'additional-name', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'additional-names', self.STRING, 1, 1)
arHonorificPrefixes = self.getPropertyValue(elmName, 'honorific-prefix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-prefixes', self.STRING, 1, 1) arHonorificPrefixes = self.getPropertyValue(elmName, 'honorific-prefix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-prefixes', self.STRING, 1, 1)
arHonorificSuffixes = self.getPropertyValue(elmName, 'honorific-suffix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-suffixes', self.STRING, 1, 1) arHonorificSuffixes = self.getPropertyValue(elmName, 'honorific-suffix', self.STRING, 1, 1) + self.getPropertyValue(elmName, 'honorific-suffixes', self.STRING, 1, 1)
arLines.append(self.vcardFold('N:' + sFamilyName + ';' + arLines.append(self.vcardFold('N:' + sFamilyName + ';' +
sGivenName + ';' + sGivenName + ';' +
','.join(arAdditionalNames) + ';' + ','.join(arAdditionalNames) + ';' +
','.join(arHonorificPrefixes) + ';' + ','.join(arHonorificPrefixes) + ';' +
@@ -2245,25 +2245,25 @@ class _MicroformatsParser:
arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1])) arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1]))
else: else:
arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0])) arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0]))
# SORT-STRING # SORT-STRING
sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1) sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1)
if sSortString: if sSortString:
arLines.append(self.vcardFold('SORT-STRING:' + sSortString)) arLines.append(self.vcardFold('SORT-STRING:' + sSortString))
# NICKNAME # NICKNAME
arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1) arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1)
if arNickname: if arNickname:
arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname))) arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname)))
# PHOTO # PHOTO
processSingleURI('photo') processSingleURI('photo')
# BDAY # BDAY
dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE) dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE)
if dtBday: if dtBday:
arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday))) arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday)))
# ADR (address) # ADR (address)
arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1) arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1)
for elmAdr in arAdr: for elmAdr in arAdr:
@@ -2285,38 +2285,38 @@ class _MicroformatsParser:
sRegion + ';' + sRegion + ';' +
sPostalCode + ';' + sPostalCode + ';' +
sCountryName)) sCountryName))
# LABEL # LABEL
processTypeValue('label', ['intl','postal','parcel','work']) processTypeValue('label', ['intl','postal','parcel','work'])
# TEL (phone number) # TEL (phone number)
processTypeValue('tel', ['voice']) processTypeValue('tel', ['voice'])
# EMAIL # EMAIL
processTypeValue('email', ['internet'], ['internet']) processTypeValue('email', ['internet'], ['internet'])
# MAILER # MAILER
processSingleString('mailer') processSingleString('mailer')
# TZ (timezone) # TZ (timezone)
processSingleString('tz') processSingleString('tz')
# GEO (geographical information) # GEO (geographical information)
elmGeo = self.getPropertyValue(elmCard, 'geo') elmGeo = self.getPropertyValue(elmCard, 'geo')
if elmGeo: if elmGeo:
sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1) sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1)
sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1) sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1)
arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude)) arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude))
# TITLE # TITLE
processSingleString('title') processSingleString('title')
# ROLE # ROLE
processSingleString('role') processSingleString('role')
# LOGO # LOGO
processSingleURI('logo') processSingleURI('logo')
# ORG (organization) # ORG (organization)
elmOrg = self.getPropertyValue(elmCard, 'org') elmOrg = self.getPropertyValue(elmCard, 'org')
if elmOrg: if elmOrg:
@@ -2330,39 +2330,39 @@ class _MicroformatsParser:
else: else:
arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1) arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1)
arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit))) arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit)))
# CATEGORY # CATEGORY
arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1) arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1)
if arCategory: if arCategory:
arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory))) arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory)))
# NOTE # NOTE
processSingleString('note') processSingleString('note')
# REV # REV
processSingleString('rev') processSingleString('rev')
# SOUND # SOUND
processSingleURI('sound') processSingleURI('sound')
# UID # UID
processSingleString('uid') processSingleString('uid')
# URL # URL
processSingleURI('url') processSingleURI('url')
# CLASS # CLASS
processSingleString('class') processSingleString('class')
# KEY # KEY
processSingleURI('key') processSingleURI('key')
if arLines: if arLines:
arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard'] arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard']
sVCards += u'\n'.join(arLines) + u'\n' sVCards += u'\n'.join(arLines) + u'\n'
return sVCards.strip() return sVCards.strip()
def isProbablyDownloadable(self, elm): def isProbablyDownloadable(self, elm):
attrsD = elm.attrMap attrsD = elm.attrMap
if not attrsD.has_key('href'): return 0 if not attrsD.has_key('href'): return 0
@@ -2461,7 +2461,7 @@ class _RelativeURIResolver(_BaseHTMLProcessor):
def resolveURI(self, uri): def resolveURI(self, uri):
return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip())) return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip()))
def unknown_starttag(self, tag, attrs): def unknown_starttag(self, tag, attrs):
if _debug: if _debug:
sys.stderr.write('tag: [%s] with attributes: [%s]\n' % (tag, str(attrs))) sys.stderr.write('tag: [%s] with attributes: [%s]\n' % (tag, str(attrs)))
@@ -2575,7 +2575,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
# svgtiny - foreignObject + linearGradient + radialGradient + stop # svgtiny - foreignObject + linearGradient + radialGradient + stop
svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', svg_elements = ['a', 'animate', 'animateColor', 'animateMotion',
'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject', 'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject',
'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern',
'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath', 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath',
'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop',
'svg', 'switch', 'text', 'title', 'tspan', 'use'] 'svg', 'switch', 'text', 'title', 'tspan', 'use']
@@ -2621,7 +2621,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
self.unacceptablestack = 0 self.unacceptablestack = 0
self.mathmlOK = 0 self.mathmlOK = 0
self.svgOK = 0 self.svgOK = 0
def unknown_starttag(self, tag, attrs): def unknown_starttag(self, tag, attrs):
acceptable_attributes = self.acceptable_attributes acceptable_attributes = self.acceptable_attributes
keymap = {} keymap = {}
@@ -2683,7 +2683,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
clean_value = self.sanitize_style(value) clean_value = self.sanitize_style(value)
if clean_value: clean_attrs.append((key,clean_value)) if clean_value: clean_attrs.append((key,clean_value))
_BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs) _BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs)
def unknown_endtag(self, tag): def unknown_endtag(self, tag):
if not tag in self.acceptable_elements: if not tag in self.acceptable_elements:
if tag in self.unacceptable_elements_with_end_tag: if tag in self.unacceptable_elements_with_end_tag:
@@ -2815,7 +2815,7 @@ class _FeedURLHandler(urllib2.HTTPDigestAuthHandler, urllib2.HTTPRedirectHandler
http_error_300 = http_error_302 http_error_300 = http_error_302
http_error_303 = http_error_302 http_error_303 = http_error_302
http_error_307 = http_error_302 http_error_307 = http_error_302
def http_error_401(self, req, fp, code, msg, headers): def http_error_401(self, req, fp, code, msg, headers):
# Check if # Check if
# - server requires digest auth, AND # - server requires digest auth, AND
@@ -2914,7 +2914,7 @@ def _open_resource(url_file_stream_or_string, etag, modified, agent, referrer, h
return opener.open(request, timeout=15) return opener.open(request, timeout=15)
finally: finally:
opener.close() # JohnD opener.close() # JohnD
# try to open with native open function (if url_file_stream_or_string is a filename) # try to open with native open function (if url_file_stream_or_string is a filename)
try: try:
return open(url_file_stream_or_string, 'rb') return open(url_file_stream_or_string, 'rb')
@@ -2966,7 +2966,7 @@ _date_handlers = []
def registerDateHandler(func): def registerDateHandler(func):
'''Register a date handler function (takes string, returns 9-tuple date in GMT)''' '''Register a date handler function (takes string, returns 9-tuple date in GMT)'''
_date_handlers.insert(0, func) _date_handlers.insert(0, func)
# ISO-8601 date parsing routines written by Fazal Majid. # ISO-8601 date parsing routines written by Fazal Majid.
# The ISO 8601 standard is very convoluted and irregular - a full ISO 8601 # The ISO 8601 standard is very convoluted and irregular - a full ISO 8601
# parser is beyond the scope of feedparser and would be a worthwhile addition # parser is beyond the scope of feedparser and would be a worthwhile addition
@@ -2977,7 +2977,7 @@ def registerDateHandler(func):
# Please note the order in templates is significant because we need a # Please note the order in templates is significant because we need a
# greedy match. # greedy match.
_iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-0MM?-?DD', 'YYYY-MM', 'YYYY-?OOO', _iso8601_tmpl = ['YYYY-?MM-?DD', 'YYYY-0MM?-?DD', 'YYYY-MM', 'YYYY-?OOO',
'YY-?MM-?DD', 'YY-?OOO', 'YYYY', 'YY-?MM-?DD', 'YY-?OOO', 'YYYY',
'-YY-?MM', '-OOO', '-YY', '-YY-?MM', '-OOO', '-YY',
'--MM-?DD', '--MM', '--MM-?DD', '--MM',
'---DD', '---DD',
@@ -3079,7 +3079,7 @@ def _parse_date_iso8601(dateString):
# Many implementations have bugs, but we'll pretend they don't. # Many implementations have bugs, but we'll pretend they don't.
return time.localtime(time.mktime(tuple(tm))) return time.localtime(time.mktime(tuple(tm)))
registerDateHandler(_parse_date_iso8601) registerDateHandler(_parse_date_iso8601)
# 8-bit date handling routines written by ytrewq1. # 8-bit date handling routines written by ytrewq1.
_korean_year = u'\ub144' # b3e2 in euc-kr _korean_year = u'\ub144' # b3e2 in euc-kr
_korean_month = u'\uc6d4' # bff9 in euc-kr _korean_month = u'\uc6d4' # bff9 in euc-kr
@@ -3170,7 +3170,7 @@ _greek_wdays = \
u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7 u'\u03a4\u03b5\u03c4': u'Wed', # d4e5f4 in iso-8859-7
u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7 u'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec in iso-8859-7
u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7 u'\u03a0\u03b1\u03c1': u'Fri', # d0e1f1 in iso-8859-7
u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7 u'\u03a3\u03b1\u03b2': u'Sat', # d3e1e2 in iso-8859-7
} }
_greek_date_format_re = \ _greek_date_format_re = \
@@ -3360,7 +3360,7 @@ def _parse_date_rfc822(dateString):
# 'ET' is equivalent to 'EST', etc. # 'ET' is equivalent to 'EST', etc.
_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800} _additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
rfc822._timezones.update(_additional_timezones) rfc822._timezones.update(_additional_timezones)
registerDateHandler(_parse_date_rfc822) registerDateHandler(_parse_date_rfc822)
def _parse_date_perforce(aDateString): def _parse_date_perforce(aDateString):
"""parse a date in yyyy/mm/dd hh:mm:ss TTT format""" """parse a date in yyyy/mm/dd hh:mm:ss TTT format"""
@@ -3398,7 +3398,7 @@ def _getCharacterEncoding(http_headers, xml_data):
http_headers is a dictionary http_headers is a dictionary
xml_data is a raw string (not Unicode) xml_data is a raw string (not Unicode)
This is so much trickier than it sounds, it's not even funny. This is so much trickier than it sounds, it's not even funny.
According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type According to RFC 3023 ('XML Media Types'), if the HTTP Content-Type
is application/xml, application/*+xml, is application/xml, application/*+xml,
@@ -3417,12 +3417,12 @@ def _getCharacterEncoding(http_headers, xml_data):
served with a Content-Type of text/* and no charset parameter served with a Content-Type of text/* and no charset parameter
must be treated as us-ascii. (We now do this.) And also that it must be treated as us-ascii. (We now do this.) And also that it
must always be flagged as non-well-formed. (We now do this too.) must always be flagged as non-well-formed. (We now do this too.)
If Content-Type is unspecified (input was local file or non-HTTP source) If Content-Type is unspecified (input was local file or non-HTTP source)
or unrecognized (server just got it totally wrong), then go by the or unrecognized (server just got it totally wrong), then go by the
encoding given in the XML prefix of the document and default to encoding given in the XML prefix of the document and default to
'iso-8859-1' as per the HTTP specification (RFC 2616). 'iso-8859-1' as per the HTTP specification (RFC 2616).
Then, assuming we didn't find a character encoding in the HTTP headers Then, assuming we didn't find a character encoding in the HTTP headers
(and the HTTP Content-type allowed us to look in the body), we need (and the HTTP Content-type allowed us to look in the body), we need
to sniff the first few bytes of the XML data and try to determine to sniff the first few bytes of the XML data and try to determine
@@ -3532,7 +3532,7 @@ def _getCharacterEncoding(http_headers, xml_data):
if true_encoding.lower() == 'gb2312': if true_encoding.lower() == 'gb2312':
true_encoding = 'gb18030' true_encoding = 'gb18030'
return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
def _toUTF8(data, encoding): def _toUTF8(data, encoding):
'''Changes an XML data stream on the fly to specify a new encoding '''Changes an XML data stream on the fly to specify a new encoding
@@ -3595,7 +3595,7 @@ def _stripDoctype(data):
start = re.search(_s2bytes('<\w'), data) start = re.search(_s2bytes('<\w'), data)
start = start and start.start() or -1 start = start and start.start() or -1
head,data = data[:start+1], data[start+1:] head,data = data[:start+1], data[start+1:]
entity_pattern = re.compile(_s2bytes(r'^\s*<!ENTITY([^>]*?)>'), re.MULTILINE) entity_pattern = re.compile(_s2bytes(r'^\s*<!ENTITY([^>]*?)>'), re.MULTILINE)
entity_results=entity_pattern.findall(head) entity_results=entity_pattern.findall(head)
head = entity_pattern.sub(_s2bytes(''), head) head = entity_pattern.sub(_s2bytes(''), head)
@@ -3617,10 +3617,10 @@ def _stripDoctype(data):
data = doctype_pattern.sub(replacement, head) + data data = doctype_pattern.sub(replacement, head) + data
return version, data, dict(replacement and [(k.decode('utf-8'), v.decode('utf-8')) for k, v in safe_pattern.findall(replacement)]) return version, data, dict(replacement and [(k.decode('utf-8'), v.decode('utf-8')) for k, v in safe_pattern.findall(replacement)])
def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[], request_headers={}, response_headers={}): def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, referrer=None, handlers=[], request_headers={}, response_headers={}):
'''Parse a feed from a URL, file, stream, or string. '''Parse a feed from a URL, file, stream, or string.
request_headers, if given, is a dict from http header name to value to add request_headers, if given, is a dict from http header name to value to add
to the request; this overrides internally generated values. to the request; this overrides internally generated values.
''' '''
@@ -3861,7 +3861,7 @@ class TextSerializer(Serializer):
stream.write('\n') stream.write('\n')
except: except:
pass pass
class PprintSerializer(Serializer): class PprintSerializer(Serializer):
def write(self, stream=sys.stdout): def write(self, stream=sys.stdout):
if self.results.has_key('href'): if self.results.has_key('href'):
@@ -3869,7 +3869,7 @@ class PprintSerializer(Serializer):
from pprint import pprint from pprint import pprint
pprint(self.results, stream) pprint(self.results, stream)
stream.write('\n') stream.write('\n')
if __name__ == '__main__': if __name__ == '__main__':
try: try:
from optparse import OptionParser from optparse import OptionParser
+1 -1
View File
@@ -70,7 +70,7 @@ class _GNTPBase(object):
'SHA1': hashlib.sha1, 'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256, 'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512, 'SHA512': hashlib.sha512,
} }
self.headers = {} self.headers = {}
self.resources = {} self.resources = {}
+77 -77
View File
@@ -3,7 +3,7 @@ from __future__ import generators
httplib2 httplib2
A caching http interface that supports ETags and gzip A caching http interface that supports ETags and gzip
to conserve bandwidth. to conserve bandwidth.
Requires Python 2.3 or later Requires Python 2.3 or later
@@ -24,8 +24,8 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
__license__ = "MIT" __license__ = "MIT"
__version__ = "$Rev$" __version__ = "$Rev$"
import re import re
import sys import sys
import email import email
import email.Utils import email.Utils
import email.Message import email.Message
@@ -85,7 +85,7 @@ def has_timeout(timeout): # python 2.6
return (timeout is not None) return (timeout is not None)
__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error', __all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error',
'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError',
'debuglevel'] 'debuglevel']
@@ -113,8 +113,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'):
# All exceptions raised here derive from HttpLib2Error # All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception): pass class HttpLib2Error(Exception): pass
# Some exceptions can be caught and optionally # Some exceptions can be caught and optionally
# be turned back into responses. # be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error): class HttpLib2ErrorWithResponse(HttpLib2Error):
def __init__(self, desc, response, content): def __init__(self, desc, response, content):
self.response = response self.response = response
@@ -176,7 +176,7 @@ def urlnorm(uri):
raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri)
authority = authority.lower() authority = authority.lower()
scheme = scheme.lower() scheme = scheme.lower()
if not path: if not path:
path = "/" path = "/"
# Could do syntax based normalization of the URI before # Could do syntax based normalization of the URI before
# computing the digest. See Section 6.2.2 of Std 66. # computing the digest. See Section 6.2.2 of Std 66.
@@ -228,7 +228,7 @@ def _parse_cache_control(headers):
parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")]
parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")]
retval = dict(parts_with_args + parts_wo_args) retval = dict(parts_with_args + parts_wo_args)
return retval return retval
# Whether to use a strict mode to parse WWW-Authenticate headers # Whether to use a strict mode to parse WWW-Authenticate headers
# Might lead to bad results in case of ill-formed header value, # Might lead to bad results in case of ill-formed header value,
@@ -254,10 +254,10 @@ def _parse_www_authenticate(headers, headername='www-authenticate'):
while authenticate: while authenticate:
# Break off the scheme at the beginning of the line # Break off the scheme at the beginning of the line
if headername == 'authentication-info': if headername == 'authentication-info':
(auth_scheme, the_rest) = ('digest', authenticate) (auth_scheme, the_rest) = ('digest', authenticate)
else: else:
(auth_scheme, the_rest) = authenticate.split(" ", 1) (auth_scheme, the_rest) = authenticate.split(" ", 1)
# Now loop over all the key value pairs that come after the scheme, # Now loop over all the key value pairs that come after the scheme,
# being careful not to roll into the next scheme # being careful not to roll into the next scheme
match = www_auth.search(the_rest) match = www_auth.search(the_rest)
auth_params = {} auth_params = {}
@@ -279,17 +279,17 @@ def _entry_disposition(response_headers, request_headers):
1. Cache-Control: max-stale 1. Cache-Control: max-stale
2. Age: headers are not used in the calculations. 2. Age: headers are not used in the calculations.
Not that this algorithm is simpler than you might think Not that this algorithm is simpler than you might think
because we are operating as a private (non-shared) cache. because we are operating as a private (non-shared) cache.
This lets us ignore 's-maxage'. We can also ignore This lets us ignore 's-maxage'. We can also ignore
'proxy-invalidate' since we aren't a proxy. 'proxy-invalidate' since we aren't a proxy.
We will never return a stale document as We will never return a stale document as
fresh as a design decision, and thus the non-implementation fresh as a design decision, and thus the non-implementation
of 'max-stale'. This also lets us safely ignore 'must-revalidate' of 'max-stale'. This also lets us safely ignore 'must-revalidate'
since we operate as if every server has sent 'must-revalidate'. since we operate as if every server has sent 'must-revalidate'.
Since we are private we get to ignore both 'public' and Since we are private we get to ignore both 'public' and
'private' parameters. We also ignore 'no-transform' since 'private' parameters. We also ignore 'no-transform' since
we don't do any transformations. we don't do any transformations.
The 'no-store' parameter is handled at a higher level. The 'no-store' parameter is handled at a higher level.
So the only Cache-Control parameters we look at are: So the only Cache-Control parameters we look at are:
@@ -298,7 +298,7 @@ def _entry_disposition(response_headers, request_headers):
max-age max-age
min-fresh min-fresh
""" """
retval = "STALE" retval = "STALE"
cc = _parse_cache_control(request_headers) cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers) cc_response = _parse_cache_control(response_headers)
@@ -340,10 +340,10 @@ def _entry_disposition(response_headers, request_headers):
min_fresh = int(cc['min-fresh']) min_fresh = int(cc['min-fresh'])
except ValueError: except ValueError:
min_fresh = 0 min_fresh = 0
current_age += min_fresh current_age += min_fresh
if freshness_lifetime > current_age: if freshness_lifetime > current_age:
retval = "FRESH" retval = "FRESH"
return retval return retval
def _decompressContent(response, new_content): def _decompressContent(response, new_content):
content = new_content content = new_content
@@ -408,10 +408,10 @@ def _wsse_username_token(cnonce, iso_now, password):
return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip()
# For credentials we need two things, first # For credentials we need two things, first
# a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.)
# Then we also need a list of URIs that have already demanded authentication # Then we also need a list of URIs that have already demanded authentication
# That list is tricky since sub-URIs can take the same auth, or the # That list is tricky since sub-URIs can take the same auth, or the
# auth scheme may change as you descend the tree. # auth scheme may change as you descend the tree.
# So we also need each Auth instance to be able to tell us # So we also need each Auth instance to be able to tell us
# how close to the 'top' it is. # how close to the 'top' it is.
@@ -443,7 +443,7 @@ class Authentication(object):
or such returned from the last authorized response. or such returned from the last authorized response.
Over-rise this in sub-classes if necessary. Over-rise this in sub-classes if necessary.
Return TRUE is the request is to be retried, for Return TRUE is the request is to be retried, for
example Digest may return stale=true. example Digest may return stale=true.
""" """
return False return False
@@ -461,7 +461,7 @@ class BasicAuthentication(Authentication):
class DigestAuthentication(Authentication): class DigestAuthentication(Authentication):
"""Only do qop='auth' and MD5, since that """Only do qop='auth' and MD5, since that
is all Apache currently implements""" is all Apache currently implements"""
def __init__(self, credentials, host, request_uri, headers, response, content, http): def __init__(self, credentials, host, request_uri, headers, response, content, http):
Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
@@ -474,7 +474,7 @@ class DigestAuthentication(Authentication):
self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper() self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper()
if self.challenge['algorithm'] != 'MD5': if self.challenge['algorithm'] != 'MD5':
raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm']))
self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
self.challenge['nc'] = 1 self.challenge['nc'] = 1
def request(self, method, request_uri, headers, content, cnonce = None): def request(self, method, request_uri, headers, content, cnonce = None):
@@ -482,17 +482,17 @@ class DigestAuthentication(Authentication):
H = lambda x: _md5(x).hexdigest() H = lambda x: _md5(x).hexdigest()
KD = lambda s, d: H("%s:%s" % (s, d)) KD = lambda s, d: H("%s:%s" % (s, d))
A2 = "".join([method, ":", request_uri]) A2 = "".join([method, ":", request_uri])
self.challenge['cnonce'] = cnonce or _cnonce() self.challenge['cnonce'] = cnonce or _cnonce()
request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'],
'%08x' % self.challenge['nc'], '%08x' % self.challenge['nc'],
self.challenge['cnonce'], self.challenge['cnonce'],
self.challenge['qop'], H(A2) self.challenge['qop'], H(A2)
)) ))
headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
self.credentials[0], self.credentials[0],
self.challenge['realm'], self.challenge['realm'],
self.challenge['nonce'], self.challenge['nonce'],
request_uri, request_uri,
self.challenge['algorithm'], self.challenge['algorithm'],
request_digest, request_digest,
self.challenge['qop'], self.challenge['qop'],
@@ -506,14 +506,14 @@ class DigestAuthentication(Authentication):
challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {})
if 'true' == challenge.get('stale'): if 'true' == challenge.get('stale'):
self.challenge['nonce'] = challenge['nonce'] self.challenge['nonce'] = challenge['nonce']
self.challenge['nc'] = 1 self.challenge['nc'] = 1
return True return True
else: else:
updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {})
if updated_challenge.has_key('nextnonce'): if updated_challenge.has_key('nextnonce'):
self.challenge['nonce'] = updated_challenge['nextnonce'] self.challenge['nonce'] = updated_challenge['nextnonce']
self.challenge['nc'] = 1 self.challenge['nc'] = 1
return False return False
@@ -562,11 +562,11 @@ class HmacDigestAuthentication(Authentication):
request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val)
request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower()
headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
self.credentials[0], self.credentials[0],
self.challenge['realm'], self.challenge['realm'],
self.challenge['snonce'], self.challenge['snonce'],
cnonce, cnonce,
request_uri, request_uri,
created, created,
request_digest, request_digest,
keylist, keylist,
@@ -583,7 +583,7 @@ class WsseAuthentication(Authentication):
"""This is thinly tested and should not be relied upon. """This is thinly tested and should not be relied upon.
At this time there isn't any third party server to test against. At this time there isn't any third party server to test against.
Blogger and TypePad implemented this algorithm at one point Blogger and TypePad implemented this algorithm at one point
but Blogger has since switched to Basic over HTTPS and but Blogger has since switched to Basic over HTTPS and
TypePad has implemented it wrong, by never issuing a 401 TypePad has implemented it wrong, by never issuing a 401
challenge but instead requiring your client to telepathically know that challenge but instead requiring your client to telepathically know that
their endpoint is expecting WSSE profile="UsernameToken".""" their endpoint is expecting WSSE profile="UsernameToken"."""
@@ -629,7 +629,7 @@ class GoogleLoginAuthentication(Authentication):
def request(self, method, request_uri, headers, content): def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate """Modify the request headers to add the appropriate
Authorization header.""" Authorization header."""
headers['authorization'] = 'GoogleLogin Auth=' + self.Auth headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
AUTH_SCHEME_CLASSES = { AUTH_SCHEME_CLASSES = {
@@ -644,13 +644,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
class FileCache(object): class FileCache(object):
"""Uses a local directory as a store for cached files. """Uses a local directory as a store for cached files.
Not really safe to use if multiple threads or processes are going to Not really safe to use if multiple threads or processes are going to
be running on the same cache. be running on the same cache.
""" """
def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
self.cache = cache self.cache = cache
self.safe = safe self.safe = safe
if not os.path.exists(cache): if not os.path.exists(cache):
os.makedirs(self.cache) os.makedirs(self.cache)
def get(self, key): def get(self, key):
@@ -688,7 +688,7 @@ class Credentials(object):
def iter(self, domain): def iter(self, domain):
for (cdomain, name, password) in self.credentials: for (cdomain, name, password) in self.credentials:
if cdomain == "" or domain == cdomain: if cdomain == "" or domain == cdomain:
yield (name, password) yield (name, password)
class KeyCerts(Credentials): class KeyCerts(Credentials):
"""Identical to Credentials except that """Identical to Credentials except that
@@ -772,7 +772,7 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
sock.setproxy(*self.proxy_info.astuple()) sock.setproxy(*self.proxy_info.astuple())
else: else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if has_timeout(self.timeout): if has_timeout(self.timeout):
sock.settimeout(self.timeout) sock.settimeout(self.timeout)
sock.connect((self.host, self.port)) sock.connect((self.host, self.port))
@@ -820,7 +820,7 @@ the same interface as FileCache."""
# If set to False then no redirects are followed, even safe ones. # If set to False then no redirects are followed, even safe ones.
self.follow_redirects = True self.follow_redirects = True
# Which HTTP methods do we apply optimistic concurrency to, i.e. # Which HTTP methods do we apply optimistic concurrency to, i.e.
# which methods get an "if-match:" etag header added to them. # which methods get an "if-match:" etag header added to them.
self.optimistic_concurrency_methods = ["PUT"] self.optimistic_concurrency_methods = ["PUT"]
@@ -831,7 +831,7 @@ the same interface as FileCache."""
self.ignore_etag = False self.ignore_etag = False
self.force_exception_to_status_code = False self.force_exception_to_status_code = False
self.timeout = timeout self.timeout = timeout
@@ -908,12 +908,12 @@ the same interface as FileCache."""
auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
auth = auths and sorted(auths)[0][1] or None auth = auths and sorted(auths)[0][1] or None
if auth: if auth:
auth.request(method, request_uri, headers, body) auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers) (response, content) = self._conn_request(conn, request_uri, method, body, headers)
if auth: if auth:
if auth.response(response, body): if auth.response(response, body):
auth.request(method, request_uri, headers, body) auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers ) (response, content) = self._conn_request(conn, request_uri, method, body, headers )
@@ -921,7 +921,7 @@ the same interface as FileCache."""
if response.status == 401: if response.status == 401:
for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
authorization.request(method, request_uri, headers, body) authorization.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers, ) (response, content) = self._conn_request(conn, request_uri, method, body, headers, )
if response.status != 401: if response.status != 401:
self.authorizations.append(authorization) self.authorizations.append(authorization)
@@ -944,7 +944,7 @@ the same interface as FileCache."""
if response.status == 301 and method in ["GET", "HEAD"]: if response.status == 301 and method in ["GET", "HEAD"]:
response['-x-permanent-redirect-url'] = response['location'] response['-x-permanent-redirect-url'] = response['location']
if not response.has_key('content-location'): if not response.has_key('content-location'):
response['content-location'] = absolute_uri response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey) _updateCache(headers, response, content, self.cache, cachekey)
if headers.has_key('if-none-match'): if headers.has_key('if-none-match'):
del headers['if-none-match'] del headers['if-none-match']
@@ -954,7 +954,7 @@ the same interface as FileCache."""
location = response['location'] location = response['location']
old_response = copy.deepcopy(response) old_response = copy.deepcopy(response)
if not old_response.has_key('content-location'): if not old_response.has_key('content-location'):
old_response['content-location'] = absolute_uri old_response['content-location'] = absolute_uri
redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method
(response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1)
response.previous = old_response response.previous = old_response
@@ -963,7 +963,7 @@ the same interface as FileCache."""
elif response.status in [200, 203] and method == "GET": elif response.status in [200, 203] and method == "GET":
# Don't cache 206's since we aren't going to handle byte range requests # Don't cache 206's since we aren't going to handle byte range requests
if not response.has_key('content-location'): if not response.has_key('content-location'):
response['content-location'] = absolute_uri response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey) _updateCache(headers, response, content, self.cache, cachekey)
return (response, content) return (response, content)
@@ -978,10 +978,10 @@ the same interface as FileCache."""
def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None): def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None):
""" Performs a single HTTP request. """ Performs a single HTTP request.
The 'uri' is the URI of the HTTP resource and can begin The 'uri' is the URI of the HTTP resource and can begin
with either 'http' or 'https'. The value of 'uri' must be an absolute URI. with either 'http' or 'https'. The value of 'uri' must be an absolute URI.
The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc.
There is no restriction on the methods allowed. There is no restriction on the methods allowed.
The 'body' is the entity body to be sent with the request. It is a string The 'body' is the entity body to be sent with the request. It is a string
@@ -990,11 +990,11 @@ object.
Any extra headers that are to be sent with the request should be provided in the Any extra headers that are to be sent with the request should be provided in the
'headers' dictionary. 'headers' dictionary.
The maximum number of redirect to follow before raising an The maximum number of redirect to follow before raising an
exception is 'redirections. The default is 5. exception is 'redirections. The default is 5.
The return value is a tuple of (response, content), the first The return value is a tuple of (response, content), the first
being and instance of the 'Response' class, the second being being and instance of the 'Response' class, the second being
a string that contains the response entity body. a string that contains the response entity body.
""" """
try: try:
@@ -1085,13 +1085,13 @@ a string that contains the response entity body.
# Determine our course of action: # Determine our course of action:
# Is the cached entry fresh or stale? # Is the cached entry fresh or stale?
# Has the client requested a non-cached response? # Has the client requested a non-cached response?
# #
# There seems to be three possible answers: # There seems to be three possible answers:
# 1. [FRESH] Return the cache entry w/o doing a GET # 1. [FRESH] Return the cache entry w/o doing a GET
# 2. [STALE] Do the GET (but add in cache validators if available) # 2. [STALE] Do the GET (but add in cache validators if available)
# 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request
entry_disposition = _entry_disposition(info, headers) entry_disposition = _entry_disposition(info, headers)
if entry_disposition == "FRESH": if entry_disposition == "FRESH":
if not cached_value: if not cached_value:
info['status'] = '504' info['status'] = '504'
@@ -1113,7 +1113,7 @@ a string that contains the response entity body.
if response.status == 304 and method == "GET": if response.status == 304 and method == "GET":
# Rewrite the cache entry with the new end-to-end headers # Rewrite the cache entry with the new end-to-end headers
# Take all headers that are in response # Take all headers that are in response
# and overwrite their values in info. # and overwrite their values in info.
# unless they are hop-by-hop, or are listed in the connection header. # unless they are hop-by-hop, or are listed in the connection header.
@@ -1125,14 +1125,14 @@ a string that contains the response entity body.
_updateCache(headers, merged_response, content, self.cache, cachekey) _updateCache(headers, merged_response, content, self.cache, cachekey)
response = merged_response response = merged_response
response.status = 200 response.status = 200
response.fromcache = True response.fromcache = True
elif response.status == 200: elif response.status == 200:
content = new_content content = new_content
else: else:
self.cache.delete(cachekey) self.cache.delete(cachekey)
content = new_content content = new_content
else: else:
cc = _parse_cache_control(headers) cc = _parse_cache_control(headers)
if cc.has_key('only-if-cached'): if cc.has_key('only-if-cached'):
info['status'] = '504' info['status'] = '504'
@@ -1146,7 +1146,7 @@ a string that contains the response entity body.
response = e.response response = e.response
content = e.content content = e.content
response.status = 500 response.status = 500
response.reason = str(e) response.reason = str(e)
elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)):
content = "Request Timeout" content = "Request Timeout"
response = Response( { response = Response( {
@@ -1156,24 +1156,24 @@ a string that contains the response entity body.
}) })
response.reason = "Request Timeout" response.reason = "Request Timeout"
else: else:
content = str(e) content = str(e)
response = Response( { response = Response( {
"content-type": "text/plain", "content-type": "text/plain",
"status": "400", "status": "400",
"content-length": len(content) "content-length": len(content)
}) })
response.reason = "Bad Request" response.reason = "Bad Request"
else: else:
raise raise
return (response, content) return (response, content)
class Response(dict): class Response(dict):
"""An object more like email.Message than httplib.HTTPResponse.""" """An object more like email.Message than httplib.HTTPResponse."""
"""Is this response from our local cache""" """Is this response from our local cache"""
fromcache = False fromcache = False
@@ -1189,27 +1189,27 @@ class Response(dict):
previous = None previous = None
def __init__(self, info): def __init__(self, info):
# info is either an email.Message or # info is either an email.Message or
# an httplib.HTTPResponse object. # an httplib.HTTPResponse object.
if isinstance(info, httplib.HTTPResponse): if isinstance(info, httplib.HTTPResponse):
for key, value in info.getheaders(): for key, value in info.getheaders():
self[key.lower()] = value self[key.lower()] = value
self.status = info.status self.status = info.status
self['status'] = str(self.status) self['status'] = str(self.status)
self.reason = info.reason self.reason = info.reason
self.version = info.version self.version = info.version
elif isinstance(info, email.Message.Message): elif isinstance(info, email.Message.Message):
for key, value in info.items(): for key, value in info.items():
self[key] = value self[key] = value
self.status = int(self['status']) self.status = int(self['status'])
else: else:
for key, value in info.iteritems(): for key, value in info.iteritems():
self[key] = value self[key] = value
self.status = int(self.get('status', self.status)) self.status = int(self.get('status', self.status))
def __getattr__(self, name): def __getattr__(self, name):
if name == 'dict': if name == 'dict':
return self return self
else: else:
raise AttributeError, name raise AttributeError, name
+9 -9
View File
@@ -16,7 +16,7 @@ import urlparse
# Convert an IRI to a URI following the rules in RFC 3987 # Convert an IRI to a URI following the rules in RFC 3987
# #
# The characters we need to enocde and escape are defined in the spec: # The characters we need to enocde and escape are defined in the spec:
# #
# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
@@ -49,7 +49,7 @@ escape_range = [
(0xF0000, 0xFFFFD ), (0xF0000, 0xFFFFD ),
(0x100000, 0x10FFFD) (0x100000, 0x10FFFD)
] ]
def encode(c): def encode(c):
retval = c retval = c
i = ord(c) i = ord(c)
@@ -63,19 +63,19 @@ def encode(c):
def iri2uri(uri): def iri2uri(uri):
"""Convert an IRI to a URI. Note that IRIs must be """Convert an IRI to a URI. Note that IRIs must be
passed in a unicode strings. That is, do not utf-8 encode passed in a unicode strings. That is, do not utf-8 encode
the IRI before passing it into the function.""" the IRI before passing it into the function."""
if isinstance(uri ,unicode): if isinstance(uri ,unicode):
(scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
authority = authority.encode('idna') authority = authority.encode('idna')
# For each character in 'ucschar' or 'iprivate' # For each character in 'ucschar' or 'iprivate'
# 1. encode as utf-8 # 1. encode as utf-8
# 2. then %-encode each octet of that utf-8 # 2. then %-encode each octet of that utf-8
uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
uri = "".join([encode(c) for c in uri]) uri = "".join([encode(c) for c in uri])
return uri return uri
if __name__ == "__main__": if __name__ == "__main__":
import unittest import unittest
@@ -83,7 +83,7 @@ if __name__ == "__main__":
def test_uris(self): def test_uris(self):
"""Test that URIs are invariant under the transformation.""" """Test that URIs are invariant under the transformation."""
invariant = [ invariant = [
u"ftp://ftp.is.co.za/rfc/rfc1808.txt", u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
u"http://www.ietf.org/rfc/rfc2396.txt", u"http://www.ietf.org/rfc/rfc2396.txt",
u"ldap://[2001:db8::7]/c=GB?objectClass?one", u"ldap://[2001:db8::7]/c=GB?objectClass?one",
@@ -94,7 +94,7 @@ if __name__ == "__main__":
u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
for uri in invariant: for uri in invariant:
self.assertEqual(uri, iri2uri(uri)) self.assertEqual(uri, iri2uri(uri))
def test_iri(self): def test_iri(self):
""" Test that the right type of escaping is done for each part of the URI.""" """ Test that the right type of escaping is done for each part of the URI."""
self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
@@ -107,4 +107,4 @@ if __name__ == "__main__":
unittest.main() unittest.main()
+2 -2
View File
@@ -48,7 +48,7 @@ def autohandler(template, context, name='autohandler'):
if len(tokens) == 1: if len(tokens) == 1:
break break
tokens[-2:] = [name] tokens[-2:] = [name]
if not lookup.filesystem_checks: if not lookup.filesystem_checks:
return lookup._uri_cache.setdefault( return lookup._uri_cache.setdefault(
(autohandler, _template_uri, name), None) (autohandler, _template_uri, name), None)
@@ -62,4 +62,4 @@ def _file_exists(lookup, path):
return True return True
else: else:
return False return False
+3 -3
View File
@@ -4,16 +4,16 @@
# This module is part of Mako and is released under # This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php # the MIT License: http://www.opensource.org/licenses/mit-license.php
"""preprocessing functions, used with the 'preprocessor' """preprocessing functions, used with the 'preprocessor'
argument on Template, TemplateLookup""" argument on Template, TemplateLookup"""
import re import re
def convert_comments(text): def convert_comments(text):
"""preprocess old style comments. """preprocess old style comments.
example: example:
from mako.ext.preprocessors import convert_comments from mako.ext.preprocessors import convert_comments
t = Template(..., preprocessor=preprocess_comments)""" t = Template(..., preprocessor=preprocess_comments)"""
return re.sub(r'(?<=\n)\s*#[^#]', "##", text) return re.sub(r'(?<=\n)\s*#[^#]', "##", text)
+4 -4
View File
@@ -279,7 +279,7 @@ def auth(u, p):
global user, password global user, password
user = u user = u
password = p password = p
def hpauth(u, p): def hpauth(u, p):
"""Set the username and password to be used in subsequent queries to """Set the username and password to be used in subsequent queries to
the MusicBrainz XML API that require authentication. the MusicBrainz XML API that require authentication.
@@ -574,7 +574,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
whether exceptions should be raised if the client and whether exceptions should be raised if the client and
username/password are left unspecified, respectively. username/password are left unspecified, respectively.
""" """
global parser_fun global parser_fun
if args is None: if args is None:
args = {} args = {}
@@ -638,7 +638,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
if hostname == '144.76.94.239:8181': if hostname == '144.76.94.239:8181':
base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '') base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string) req.add_header("Authorization", "Basic %s" % base64string)
_log.debug("requesting with UA %s" % _useragent) _log.debug("requesting with UA %s" % _useragent)
if body: if body:
req.add_header('Content-Type', 'application/xml; charset=UTF-8') req.add_header('Content-Type', 'application/xml; charset=UTF-8')
@@ -908,7 +908,7 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True):
The `toc` should have to same format as :attr:`discid.Disc.toc_string`. The `toc` should have to same format as :attr:`discid.Disc.toc_string`.
If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does, If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does,
the CD Stub will be returned. Prevent this from happening by the CD Stub will be returned. Prevent this from happening by
passing `cdstubs=False`. passing `cdstubs=False`.
The result is a dict with either a 'disc' , a 'cdstub' key The result is a dict with either a 'disc' , a 'cdstub' key
+1 -1
View File
@@ -22,7 +22,7 @@ class EasyMP4Tags(DictMixin, Metadata):
strings, and values are a list of Unicode strings (and these lists strings, and values are a list of Unicode strings (and these lists
are always of length 0 or 1). are always of length 0 or 1).
If you need access to the full MP4 metadata feature set, you should use If you need access to the full MP4 metadata feature set, you should use
MP4, not EasyMP4. MP4, not EasyMP4.
""" """
+60 -60
View File
@@ -85,11 +85,11 @@ def generate_verifier(length=8):
class Consumer(object): class Consumer(object):
"""A consumer of OAuth-protected services. """A consumer of OAuth-protected services.
The OAuth consumer is a "third-party" service that wants to access The OAuth consumer is a "third-party" service that wants to access
protected resources from an OAuth service provider on behalf of an end protected resources from an OAuth service provider on behalf of an end
user. It's kind of the OAuth client. user. It's kind of the OAuth client.
Usually a consumer must be registered with the service provider by the Usually a consumer must be registered with the service provider by the
developer of the consumer software. As part of that process, the service developer of the consumer software. As part of that process, the service
provider gives the consumer a *key* and a *secret* with which the consumer provider gives the consumer a *key* and a *secret* with which the consumer
@@ -97,7 +97,7 @@ class Consumer(object):
key in each request to identify itself, but will use its secret only when key in each request to identify itself, but will use its secret only when
signing requests, to prove that the request is from that particular signing requests, to prove that the request is from that particular
registered consumer. registered consumer.
Once registered, the consumer can then use its consumer credentials to ask Once registered, the consumer can then use its consumer credentials to ask
the service provider for a request token, kicking off the OAuth the service provider for a request token, kicking off the OAuth
authorization process. authorization process.
@@ -125,12 +125,12 @@ class Consumer(object):
class Token(object): class Token(object):
"""An OAuth credential used to request authorization or a protected """An OAuth credential used to request authorization or a protected
resource. resource.
Tokens in OAuth comprise a *key* and a *secret*. The key is included in Tokens in OAuth comprise a *key* and a *secret*. The key is included in
requests to identify the token being used, but the secret is used only in requests to identify the token being used, but the secret is used only in
the signature, to prove that the requester is who the server gave the the signature, to prove that the requester is who the server gave the
token to. token to.
When first negotiating the authorization, the consumer asks for a *request When first negotiating the authorization, the consumer asks for a *request
token* that the live user authorizes with the service provider. The token* that the live user authorizes with the service provider. The
consumer then exchanges the request token for an *access token* that can consumer then exchanges the request token for an *access token* that can
@@ -175,7 +175,7 @@ class Token(object):
def to_string(self): def to_string(self):
"""Returns this token as a plain string, suitable for storage. """Returns this token as a plain string, suitable for storage.
The resulting string includes the token's secret, so you should never The resulting string includes the token's secret, so you should never
send or store this string where a third party can read it. send or store this string where a third party can read it.
""" """
@@ -188,7 +188,7 @@ class Token(object):
if self.callback_confirmed is not None: if self.callback_confirmed is not None:
data['oauth_callback_confirmed'] = self.callback_confirmed data['oauth_callback_confirmed'] = self.callback_confirmed
return urllib.urlencode(data) return urllib.urlencode(data)
@staticmethod @staticmethod
def from_string(s): def from_string(s):
"""Deserializes a token from a string like one returned by """Deserializes a token from a string like one returned by
@@ -209,7 +209,7 @@ class Token(object):
try: try:
secret = params['oauth_token_secret'][0] secret = params['oauth_token_secret'][0]
except Exception: except Exception:
raise ValueError("'oauth_token_secret' not found in " raise ValueError("'oauth_token_secret' not found in "
"OAuth request.") "OAuth request.")
token = Token(key, secret) token = Token(key, secret)
@@ -225,45 +225,45 @@ class Token(object):
def setter(attr): def setter(attr):
name = attr.__name__ name = attr.__name__
def getter(self): def getter(self):
try: try:
return self.__dict__[name] return self.__dict__[name]
except KeyError: except KeyError:
raise AttributeError(name) raise AttributeError(name)
def deleter(self): def deleter(self):
del self.__dict__[name] del self.__dict__[name]
return property(getter, attr, deleter) return property(getter, attr, deleter)
class Request(dict): class Request(dict):
"""The parameters and information for an HTTP request, suitable for """The parameters and information for an HTTP request, suitable for
authorizing with OAuth credentials. authorizing with OAuth credentials.
When a consumer wants to access a service's protected resources, it does When a consumer wants to access a service's protected resources, it does
so using a signed HTTP request identifying itself (the consumer) with its so using a signed HTTP request identifying itself (the consumer) with its
key, and providing an access token authorized by the end user to access key, and providing an access token authorized by the end user to access
those resources. those resources.
""" """
http_method = HTTP_METHOD http_method = HTTP_METHOD
http_url = None http_url = None
version = VERSION version = VERSION
def __init__(self, method=HTTP_METHOD, url=None, parameters=None): def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
if method is not None: if method is not None:
self.method = method self.method = method
if url is not None: if url is not None:
self.url = url self.url = url
if parameters is not None: if parameters is not None:
self.update(parameters) self.update(parameters)
@setter @setter
def url(self, value): def url(self, value):
parts = urlparse.urlparse(value) parts = urlparse.urlparse(value)
@@ -280,33 +280,33 @@ class Request(dict):
value = '%s://%s%s' % (scheme, netloc, path) value = '%s://%s%s' % (scheme, netloc, path)
self.__dict__['url'] = value self.__dict__['url'] = value
@setter @setter
def method(self, value): def method(self, value):
self.__dict__['method'] = value.upper() self.__dict__['method'] = value.upper()
def _get_timestamp_nonce(self): def _get_timestamp_nonce(self):
return self['oauth_timestamp'], self['oauth_nonce'] return self['oauth_timestamp'], self['oauth_nonce']
def get_nonoauth_parameters(self): def get_nonoauth_parameters(self):
"""Get any non-OAuth parameters.""" """Get any non-OAuth parameters."""
return dict([(k, v) for k, v in self.iteritems() return dict([(k, v) for k, v in self.iteritems()
if not k.startswith('oauth_')]) if not k.startswith('oauth_')])
def to_header(self, realm=''): def to_header(self, realm=''):
"""Serialize as a header for an HTTPAuth request.""" """Serialize as a header for an HTTPAuth request."""
oauth_params = ((k, v) for k, v in self.items() oauth_params = ((k, v) for k, v in self.items()
if k.startswith('oauth_')) if k.startswith('oauth_'))
stringy_params = ((k, escape(str(v))) for k, v in oauth_params) stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
params_header = ', '.join(header_params) params_header = ', '.join(header_params)
auth_header = 'OAuth realm="%s"' % realm auth_header = 'OAuth realm="%s"' % realm
if params_header: if params_header:
auth_header = "%s, %s" % (auth_header, params_header) auth_header = "%s, %s" % (auth_header, params_header)
return {'Authorization': auth_header} return {'Authorization': auth_header}
def to_postdata(self): def to_postdata(self):
"""Serialize as post data for a POST request.""" """Serialize as post data for a POST request."""
return self.encode_postdata(self) return self.encode_postdata(self)
@@ -327,7 +327,7 @@ class Request(dict):
raise Error('Parameter not found: %s' % parameter) raise Error('Parameter not found: %s' % parameter)
return ret return ret
def get_normalized_parameters(self): def get_normalized_parameters(self):
"""Return a string that contains the parameters that must be signed.""" """Return a string that contains the parameters that must be signed."""
items = [(k, v) for k, v in self.items() if k != 'oauth_signature'] items = [(k, v) for k, v in self.items() if k != 'oauth_signature']
@@ -337,7 +337,7 @@ class Request(dict):
# (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
# Spaces must be encoded with "%20" instead of "+" # Spaces must be encoded with "%20" instead of "+"
return encoded_str.replace('+', '%20') return encoded_str.replace('+', '%20')
def sign_request(self, signature_method, consumer, token): def sign_request(self, signature_method, consumer, token):
"""Set the signature parameter to the result of sign.""" """Set the signature parameter to the result of sign."""
@@ -349,24 +349,24 @@ class Request(dict):
self['oauth_signature_method'] = signature_method.name self['oauth_signature_method'] = signature_method.name
self['oauth_signature'] = signature_method.sign(self, consumer, token) self['oauth_signature'] = signature_method.sign(self, consumer, token)
@classmethod @classmethod
def make_timestamp(cls): def make_timestamp(cls):
"""Get seconds since epoch (UTC).""" """Get seconds since epoch (UTC)."""
return str(int(time.time())) return str(int(time.time()))
@classmethod @classmethod
def make_nonce(cls): def make_nonce(cls):
"""Generate pseudorandom number.""" """Generate pseudorandom number."""
return str(random.randint(0, 100000000)) return str(random.randint(0, 100000000))
@classmethod @classmethod
def from_request(cls, http_method, http_url, headers=None, parameters=None, def from_request(cls, http_method, http_url, headers=None, parameters=None,
query_string=None): query_string=None):
"""Combines multiple parameter sources.""" """Combines multiple parameter sources."""
if parameters is None: if parameters is None:
parameters = {} parameters = {}
# Headers # Headers
if headers and 'Authorization' in headers: if headers and 'Authorization' in headers:
auth_header = headers['Authorization'] auth_header = headers['Authorization']
@@ -380,57 +380,57 @@ class Request(dict):
except: except:
raise Error('Unable to parse OAuth parameters from ' raise Error('Unable to parse OAuth parameters from '
'Authorization header.') 'Authorization header.')
# GET or POST query string. # GET or POST query string.
if query_string: if query_string:
query_params = cls._split_url_string(query_string) query_params = cls._split_url_string(query_string)
parameters.update(query_params) parameters.update(query_params)
# URL parameters. # URL parameters.
param_str = urlparse.urlparse(http_url)[4] # query param_str = urlparse.urlparse(http_url)[4] # query
url_params = cls._split_url_string(param_str) url_params = cls._split_url_string(param_str)
parameters.update(url_params) parameters.update(url_params)
if parameters: if parameters:
return cls(http_method, http_url, parameters) return cls(http_method, http_url, parameters)
return None return None
@classmethod @classmethod
def from_consumer_and_token(cls, consumer, token=None, def from_consumer_and_token(cls, consumer, token=None,
http_method=HTTP_METHOD, http_url=None, parameters=None): http_method=HTTP_METHOD, http_url=None, parameters=None):
if not parameters: if not parameters:
parameters = {} parameters = {}
defaults = { defaults = {
'oauth_consumer_key': consumer.key, 'oauth_consumer_key': consumer.key,
'oauth_timestamp': cls.make_timestamp(), 'oauth_timestamp': cls.make_timestamp(),
'oauth_nonce': cls.make_nonce(), 'oauth_nonce': cls.make_nonce(),
'oauth_version': cls.version, 'oauth_version': cls.version,
} }
defaults.update(parameters) defaults.update(parameters)
parameters = defaults parameters = defaults
if token: if token:
parameters['oauth_token'] = token.key parameters['oauth_token'] = token.key
return Request(http_method, http_url, parameters) return Request(http_method, http_url, parameters)
@classmethod @classmethod
def from_token_and_callback(cls, token, callback=None, def from_token_and_callback(cls, token, callback=None,
http_method=HTTP_METHOD, http_url=None, parameters=None): http_method=HTTP_METHOD, http_url=None, parameters=None):
if not parameters: if not parameters:
parameters = {} parameters = {}
parameters['oauth_token'] = token.key parameters['oauth_token'] = token.key
if callback: if callback:
parameters['oauth_callback'] = callback parameters['oauth_callback'] = callback
return cls(http_method, http_url, parameters) return cls(http_method, http_url, parameters)
@staticmethod @staticmethod
def _split_header(header): def _split_header(header):
"""Turn Authorization: header into parameters.""" """Turn Authorization: header into parameters."""
@@ -447,7 +447,7 @@ class Request(dict):
# Remove quotes and unescape the value. # Remove quotes and unescape the value.
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
return params return params
@staticmethod @staticmethod
def _split_url_string(param_str): def _split_url_string(param_str):
"""Turn URL string into parameters.""" """Turn URL string into parameters."""
@@ -460,7 +460,7 @@ class Request(dict):
class Server(object): class Server(object):
"""A skeletal implementation of a service provider, providing protected """A skeletal implementation of a service provider, providing protected
resources to requests from authorized consumers. resources to requests from authorized consumers.
This class implements the logic to check requests for authorization. You This class implements the logic to check requests for authorization. You
can use it with your web server or web framework to protect certain can use it with your web server or web framework to protect certain
resources with OAuth. resources with OAuth.
@@ -536,7 +536,7 @@ class Server(object):
if not valid: if not valid:
key, base = signature_method.signing_base(request, consumer, token) key, base = signature_method.signing_base(request, consumer, token)
raise Error('Invalid signature. Expected signature base ' raise Error('Invalid signature. Expected signature base '
'string: %s' % base) 'string: %s' % base)
built = signature_method.sign(request, consumer, token) built = signature_method.sign(request, consumer, token)
@@ -567,7 +567,7 @@ class Client(httplib2.Http):
self.token = token self.token = token
self.method = SignatureMethod_HMAC_SHA1() self.method = SignatureMethod_HMAC_SHA1()
httplib2.Http.__init__(self, cache=cache, timeout=timeout, httplib2.Http.__init__(self, cache=cache, timeout=timeout,
proxy_info=proxy_info) proxy_info=proxy_info)
def set_signature_method(self, method): def set_signature_method(self, method):
@@ -576,10 +576,10 @@ class Client(httplib2.Http):
self.method = method self.method = method
def request(self, uri, method="GET", body=None, headers=None, def request(self, uri, method="GET", body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None, redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None,
force_auth_header=False): force_auth_header=False):
if not isinstance(headers, dict): if not isinstance(headers, dict):
headers = {} headers = {}
@@ -587,7 +587,7 @@ class Client(httplib2.Http):
parameters = dict(parse_qsl(body)) parameters = dict(parse_qsl(body))
elif method == "GET": elif method == "GET":
parsed = urlparse.urlparse(uri) parsed = urlparse.urlparse(uri)
parameters = parse_qs(parsed.query) parameters = parse_qs(parsed.query)
else: else:
parameters = None parameters = None
@@ -614,14 +614,14 @@ class Client(httplib2.Http):
# don't call update twice. # don't call update twice.
headers.update(req.to_header()) headers.update(req.to_header())
return httplib2.Http.request(self, uri, method=method, body=body, return httplib2.Http.request(self, uri, method=method, body=body,
headers=headers, redirections=redirections, headers=headers, redirections=redirections,
connection_type=connection_type) connection_type=connection_type)
class SignatureMethod(object): class SignatureMethod(object):
"""A way of signing requests. """A way of signing requests.
The OAuth protocol lets consumers and service providers pick a way to sign The OAuth protocol lets consumers and service providers pick a way to sign
requests. This interface shows the methods expected by the other `oauth` requests. This interface shows the methods expected by the other `oauth`
modules for signing requests. Subclass it and implement its methods to modules for signing requests. Subclass it and implement its methods to
@@ -657,7 +657,7 @@ class SignatureMethod(object):
class SignatureMethod_HMAC_SHA1(SignatureMethod): class SignatureMethod_HMAC_SHA1(SignatureMethod):
name = 'HMAC-SHA1' name = 'HMAC-SHA1'
def signing_base(self, request, consumer, token): def signing_base(self, request, consumer, token):
sig = ( sig = (
escape(request.method), escape(request.method),
+1 -1
View File
@@ -36,6 +36,6 @@ class Library:
if attributes.get('Play Count'): if attributes.get('Play Count'):
s.play_count = int(attributes.get('Play Count')) s.play_count = int(attributes.get('Play Count'))
if attributes.get('Location'): if attributes.get('Location'):
s.location = attributes.get('Location') s.location = attributes.get('Location')
songs.append(s) songs.append(s)
return songs return songs
+1 -1
View File
@@ -42,5 +42,5 @@ class Song:
album_rating = None album_rating = None
play_count = None play_count = None
location = None location = None
#title = property(getTitle,setTitle) #title = property(getTitle,setTitle)
+1 -1
View File
@@ -5,7 +5,7 @@ class XMLLibraryParser:
s = f.read() s = f.read()
lines = s.split("\n") lines = s.split("\n")
self.dictionary = self.parser(lines) self.dictionary = self.parser(lines)
def getValue(self,restOfLine): def getValue(self,restOfLine):
value = re.sub("<.*?>","",restOfLine) value = re.sub("<.*?>","",restOfLine)
u = unicode(value,"utf-8") u = unicode(value,"utf-8")
+1 -1
View File
@@ -201,7 +201,7 @@ class GazelleAPI(object):
Returns the inbox Mailbox for the logged in user Returns the inbox Mailbox for the logged in user
""" """
return Mailbox(self, 'inbox', page, sort) return Mailbox(self, 'inbox', page, sort)
def get_sentbox(self, page='1', sort='unread'): def get_sentbox(self, page='1', sort='unread'):
""" """
Returns the sentbox Mailbox for the logged in user Returns the sentbox Mailbox for the logged in user
+2 -2
View File
@@ -58,9 +58,9 @@ class Mailbox(object):
""" """
This class represents the logged in user's inbox/sentbox This class represents the logged in user's inbox/sentbox
""" """
def __init__(self, parent_api, boxtype='inbox', page='1', sort='unread'): def __init__(self, parent_api, boxtype='inbox', page='1', sort='unread'):
self.parent_api = parent_api self.parent_api = parent_api
self.boxtype = boxtype self.boxtype = boxtype
self.current_page = page self.current_page = page
self.total_pages = None self.total_pages = None
self.sort = sort self.sort = sort
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python
from pynma import PyNMA from pynma import PyNMA
+4 -4
View File
@@ -99,7 +99,7 @@ class PyNMA(object):
res = self.callapi('POST', ADD_PATH, datas) res = self.callapi('POST', ADD_PATH, datas)
results[datas['apikey']] = res results[datas['apikey']] = res
return results return results
def callapi(self, method, path, args): def callapi(self, method, path, args):
headers = { 'User-Agent': USER_AGENT } headers = { 'User-Agent': USER_AGENT }
if method == "POST": if method == "POST":
@@ -116,7 +116,7 @@ class PyNMA(object):
'message': str(e) 'message': str(e)
} }
pass pass
return res return res
def _parse_reponse(self, response): def _parse_reponse(self, response):
@@ -133,5 +133,5 @@ class PyNMA(object):
res['message'] = elem.firstChild.nodeValue res['message'] = elem.firstChild.nodeValue
res['type'] = elem.tagName res['type'] = elem.tagName
return res return res
+1 -1
View File
@@ -440,7 +440,7 @@ def merge_cookies(cookiejar, cookies):
""" """
if not isinstance(cookiejar, cookielib.CookieJar): if not isinstance(cookiejar, cookielib.CookieJar):
raise ValueError('You can only merge into CookieJar') raise ValueError('You can only merge into CookieJar')
if isinstance(cookies, dict): if isinstance(cookies, dict):
cookiejar = cookiejar_from_dict( cookiejar = cookiejar_from_dict(
cookies, cookiejar=cookiejar, overwrite=False) cookies, cookiejar=cookiejar, overwrite=False)
@@ -1,11 +1,11 @@
######################## BEGIN LICENSE BLOCK ######################## ######################## BEGIN LICENSE BLOCK ########################
# The Original Code is Mozilla Communicator client code. # The Original Code is Mozilla Communicator client code.
# #
# The Initial Developer of the Original Code is # The Initial Developer of the Original Code is
# Netscape Communications Corporation. # Netscape Communications Corporation.
# Portions created by the Initial Developer are Copyright (C) 1998 # Portions created by the Initial Developer are Copyright (C) 1998
# the Initial Developer. All Rights Reserved. # the Initial Developer. All Rights Reserved.
# #
# Contributor(s): # Contributor(s):
# Mark Pilgrim - port to Python # Mark Pilgrim - port to Python
# #
@@ -13,12 +13,12 @@
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 2.1 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+2 -2
View File
@@ -14,12 +14,12 @@
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 2.1 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+5 -5
View File
@@ -13,12 +13,12 @@
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 2.1 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
@@ -35,14 +35,14 @@
# #
# Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24 # Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24
# Random Distribution Ration = 512 / (2350-512) = 0.279. # Random Distribution Ration = 512 / (2350-512) = 0.279.
# #
# Typical Distribution Ratio # Typical Distribution Ratio
EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0 EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0
EUCKR_TABLE_SIZE = 2352 EUCKR_TABLE_SIZE = 2352
# Char to FreqOrder table , # Char to FreqOrder table ,
EUCKRCharToFreqOrder = ( \ EUCKRCharToFreqOrder = ( \
13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87, 13, 130, 120,1396, 481,1719,1720, 328, 609, 212,1721, 707, 400, 299,1722, 87,
1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398, 1397,1723, 104, 536,1117,1203,1724,1267, 685,1268, 508,1725,1726,1727,1728,1398,
+2 -2
View File
@@ -13,12 +13,12 @@
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 2.1 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
@@ -13,12 +13,12 @@
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 2.1 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+1 -1
View File
@@ -39,7 +39,7 @@ def unidecode(string):
if codepoint < 0x80: # Basic ASCII if codepoint < 0x80: # Basic ASCII
retval.append(str(char)) retval.append(str(char))
continue continue
if codepoint > 0xeffff: if codepoint > 0xeffff:
continue # Characters in Private Use Area and above are ignored continue # Characters in Private Use Area and above are ignored
+1 -1
View File
@@ -287,7 +287,7 @@ class SafeConstructor(BaseConstructor):
return str(value).decode('base64') return str(value).decode('base64')
except (binascii.Error, UnicodeEncodeError), exc: except (binascii.Error, UnicodeEncodeError), exc:
raise ConstructorError(None, None, raise ConstructorError(None, None,
"failed to decode base64 data: %s" % exc, node.start_mark) "failed to decode base64 data: %s" % exc, node.start_mark)
timestamp_regexp = re.compile( timestamp_regexp = re.compile(
ur'''^(?P<year>[0-9][0-9][0-9][0-9]) ur'''^(?P<year>[0-9][0-9][0-9][0-9])
+1 -1
View File
@@ -674,7 +674,7 @@ class Emitter(object):
# Check for indicators. # Check for indicators.
if index == 0: if index == 0:
# Leading indicators are special characters. # Leading indicators are special characters.
if ch in u'#,[]{}&*!|>\'\"%@`': if ch in u'#,[]{}&*!|>\'\"%@`':
flow_indicators = True flow_indicators = True
block_indicators = True block_indicators = True
if ch in u'?:': if ch in u'?:':
+1 -1
View File
@@ -482,7 +482,7 @@ class Parser(object):
token = self.peek_token() token = self.peek_token()
raise ParserError("while parsing a flow sequence", self.marks[-1], raise ParserError("while parsing a flow sequence", self.marks[-1],
"expected ',' or ']', but got %r" % token.id, token.start_mark) "expected ',' or ']', but got %r" % token.id, token.start_mark)
if self.check_token(KeyToken): if self.check_token(KeyToken):
token = self.peek_token() token = self.peek_token()
event = MappingStartEvent(None, None, True, event = MappingStartEvent(None, None, True,
+9 -9
View File
@@ -314,7 +314,7 @@ class Scanner(object):
# Remove the saved possible key position at the current flow level. # Remove the saved possible key position at the current flow level.
if self.flow_level in self.possible_simple_keys: if self.flow_level in self.possible_simple_keys:
key = self.possible_simple_keys[self.flow_level] key = self.possible_simple_keys[self.flow_level]
if key.required: if key.required:
raise ScannerError("while scanning a simple key", key.mark, raise ScannerError("while scanning a simple key", key.mark,
"could not found expected ':'", self.get_mark()) "could not found expected ':'", self.get_mark())
@@ -363,11 +363,11 @@ class Scanner(object):
# Read the token. # Read the token.
mark = self.get_mark() mark = self.get_mark()
# Add STREAM-START. # Add STREAM-START.
self.tokens.append(StreamStartToken(mark, mark, self.tokens.append(StreamStartToken(mark, mark,
encoding=self.encoding)) encoding=self.encoding))
def fetch_stream_end(self): def fetch_stream_end(self):
@@ -381,7 +381,7 @@ class Scanner(object):
# Read the token. # Read the token.
mark = self.get_mark() mark = self.get_mark()
# Add STREAM-END. # Add STREAM-END.
self.tokens.append(StreamEndToken(mark, mark)) self.tokens.append(StreamEndToken(mark, mark))
@@ -389,7 +389,7 @@ class Scanner(object):
self.done = True self.done = True
def fetch_directive(self): def fetch_directive(self):
# Set the current intendation to -1. # Set the current intendation to -1.
self.unwind_indent(-1) self.unwind_indent(-1)
@@ -516,7 +516,7 @@ class Scanner(object):
self.tokens.append(BlockEntryToken(start_mark, end_mark)) self.tokens.append(BlockEntryToken(start_mark, end_mark))
def fetch_key(self): def fetch_key(self):
# Block context needs additional checks. # Block context needs additional checks.
if not self.flow_level: if not self.flow_level:
@@ -566,7 +566,7 @@ class Scanner(object):
# It must be a part of a complex key. # It must be a part of a complex key.
else: else:
# Block context needs additional checks. # Block context needs additional checks.
# (Do we really need them? They will be catched by the parser # (Do we really need them? They will be catched by the parser
# anyway.) # anyway.)
@@ -1024,14 +1024,14 @@ class Scanner(object):
# Unfortunately, folding rules are ambiguous. # Unfortunately, folding rules are ambiguous.
# #
# This is the folding according to the specification: # This is the folding according to the specification:
if folded and line_break == u'\n' \ if folded and line_break == u'\n' \
and leading_non_space and self.peek() not in u' \t': and leading_non_space and self.peek() not in u' \t':
if not breaks: if not breaks:
chunks.append(u' ') chunks.append(u' ')
else: else:
chunks.append(line_break) chunks.append(line_break)
# This is Clark Evans's interpretation (also in the spec # This is Clark Evans's interpretation (also in the spec
# examples): # examples):
# #