mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-21 08:24:00 +01:00
Dropping trailing whitespaces
This commit is contained in:
@@ -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
@@ -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
@@ -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())) + ")"
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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():
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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":
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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')])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+105
-105
@@ -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('&', '&')) for k, v in attrs]
|
attrs = [(k, v.replace('&', '&')) 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'<!\1', data)
|
data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'<!\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(''', "'")
|
data = data.replace(''', "'")
|
||||||
data = data.replace('"', '"')
|
data = data.replace('"', '"')
|
||||||
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 '©', ref will be 'copy'
|
# called for each entity reference, e.g. for '©', 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('"', '"')
|
data = data.replace('"', '"')
|
||||||
data = data.replace(''', "'")
|
data = data.replace(''', "'")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def strattrs(self, attrs):
|
def strattrs(self, attrs):
|
||||||
return ''.join([' %s="%s"' % (n,v.replace('"','"')) for n,v in attrs])
|
return ''.join([' %s="%s"' % (n,v.replace('"','"')) 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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
from pynma import PyNMA
|
from pynma import PyNMA
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user