mirror of
https://github.com/rembo10/headphones.git
synced 2026-06-17 08:03:50 +01:00
Dropping trailing whitespaces
This commit is contained in:
@@ -39,11 +39,11 @@ def switch(AlbumID, ReleaseID):
|
||||
"ReleaseCountry": newalbumdata['ReleaseCountry'],
|
||||
"ReleaseFormat": newalbumdata['ReleaseFormat']
|
||||
}
|
||||
|
||||
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
for track in newtrackdata:
|
||||
|
||||
|
||||
controlValueDict = {"TrackID": track['TrackID'],
|
||||
"AlbumID": AlbumID}
|
||||
|
||||
@@ -60,23 +60,23 @@ def switch(AlbumID, ReleaseID):
|
||||
"Format": track['Format'],
|
||||
"BitRate": track['BitRate']
|
||||
}
|
||||
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
# Mark albums as downloaded if they have at least 80% (by default, configurable) of the album
|
||||
total_track_count = len(newtrackdata)
|
||||
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)):
|
||||
myDB.action('UPDATE albums SET Status=? WHERE AlbumID=?', ['Downloaded', AlbumID])
|
||||
|
||||
|
||||
# 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']]))
|
||||
havetracks = len(myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL', [newalbumdata['ArtistID']]))
|
||||
|
||||
controlValueDict = {"ArtistID": newalbumdata['ArtistID']}
|
||||
|
||||
|
||||
newValueDict = { "TotalTracks": totaltracks,
|
||||
"HaveTracks": havetracks}
|
||||
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
+88
-88
@@ -21,29 +21,29 @@ import lib.simplejson as simplejson
|
||||
from xml.dom.minidom import Document
|
||||
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',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
|
||||
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
|
||||
'addAlbum', 'queueAlbum', 'unqueueAlbum', 'forceSearch', 'forceProcess', 'getVersion', 'checkGithub',
|
||||
'shutdown', 'restart', 'update', 'getArtistArt', 'getAlbumArt', 'getArtistInfo', 'getAlbumInfo',
|
||||
'getArtistThumb', 'getAlbumThumb', 'choose_specific_download', 'download_specific_release']
|
||||
|
||||
class Api(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
|
||||
self.apikey = None
|
||||
self.cmd = None
|
||||
self.id = None
|
||||
|
||||
|
||||
self.kwargs = None
|
||||
|
||||
self.data = None
|
||||
|
||||
self.callback = None
|
||||
|
||||
|
||||
|
||||
def checkParams(self,*args,**kwargs):
|
||||
|
||||
|
||||
if not headphones.API_ENABLED:
|
||||
self.data = 'API not enabled'
|
||||
return
|
||||
@@ -53,32 +53,32 @@ class Api(object):
|
||||
if len(headphones.API_KEY) != 32:
|
||||
self.data = 'API key not generated correctly'
|
||||
return
|
||||
|
||||
|
||||
if 'apikey' not in kwargs:
|
||||
self.data = 'Missing api key'
|
||||
return
|
||||
|
||||
|
||||
if kwargs['apikey'] != headphones.API_KEY:
|
||||
self.data = 'Incorrect API key'
|
||||
return
|
||||
else:
|
||||
self.apikey = kwargs.pop('apikey')
|
||||
|
||||
|
||||
if 'cmd' not in kwargs:
|
||||
self.data = 'Missing parameter: cmd'
|
||||
return
|
||||
|
||||
|
||||
if kwargs['cmd'] not in cmd_list:
|
||||
self.data = 'Unknown command: %s' % kwargs['cmd']
|
||||
return
|
||||
else:
|
||||
self.cmd = kwargs.pop('cmd')
|
||||
|
||||
|
||||
self.kwargs = kwargs
|
||||
self.data = 'OK'
|
||||
|
||||
def fetchData(self):
|
||||
|
||||
|
||||
if self.data == 'OK':
|
||||
logger.info('Recieved API command: %s', self.cmd)
|
||||
methodToCall = getattr(self, "_" + self.cmd)
|
||||
@@ -95,74 +95,74 @@ class Api(object):
|
||||
return self.data
|
||||
else:
|
||||
return self.data
|
||||
|
||||
|
||||
def _dic_from_query(self,query):
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
rows = myDB.select(query)
|
||||
|
||||
|
||||
rows_as_dic = []
|
||||
|
||||
|
||||
for row in rows:
|
||||
row_as_dic = dict(zip(row.keys(), row))
|
||||
rows_as_dic.append(row_as_dic)
|
||||
|
||||
|
||||
return rows_as_dic
|
||||
|
||||
|
||||
def _getIndex(self, **kwargs):
|
||||
|
||||
|
||||
self.data = self._dic_from_query('SELECT * from artists order by ArtistSortName COLLATE NOCASE')
|
||||
return
|
||||
|
||||
return
|
||||
|
||||
def _getArtist(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['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')
|
||||
description = self._dic_from_query('SELECT * from descriptions WHERE ArtistID="' + self.id + '"')
|
||||
|
||||
|
||||
self.data = { 'artist': artist, 'albums': albums, 'description' : description }
|
||||
return
|
||||
|
||||
|
||||
def _getAlbum(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['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 + '"')
|
||||
description = self._dic_from_query('SELECT * from descriptions WHERE ReleaseGroupID="' + self.id + '"')
|
||||
|
||||
|
||||
self.data = { 'album' : album, 'tracks' : tracks, 'description' : description }
|
||||
return
|
||||
|
||||
|
||||
def _getHistory(self, **kwargs):
|
||||
self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC')
|
||||
return
|
||||
|
||||
|
||||
def _getUpcoming(self, **kwargs):
|
||||
self.data = self._dic_from_query("SELECT * from albums WHERE ReleaseDate > date('now') order by ReleaseDate DESC")
|
||||
return
|
||||
|
||||
|
||||
def _getWanted(self, **kwargs):
|
||||
self.data = self._dic_from_query("SELECT * from albums WHERE Status='Wanted'")
|
||||
return
|
||||
|
||||
|
||||
def _getSimilar(self, **kwargs):
|
||||
self.data = self._dic_from_query('SELECT * from lastfmcloud')
|
||||
return
|
||||
|
||||
|
||||
def _getLogs(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def _findArtist(self, **kwargs):
|
||||
if 'name' not in kwargs:
|
||||
self.data = 'Missing parameter: name'
|
||||
@@ -171,7 +171,7 @@ class Api(object):
|
||||
limit = kwargs['limit']
|
||||
else:
|
||||
limit=50
|
||||
|
||||
|
||||
self.data = mb.findArtist(kwargs['name'], limit)
|
||||
|
||||
def _findAlbum(self, **kwargs):
|
||||
@@ -182,216 +182,216 @@ class Api(object):
|
||||
limit = kwargs['limit']
|
||||
else:
|
||||
limit=50
|
||||
|
||||
|
||||
self.data = mb.findRelease(kwargs['name'], limit)
|
||||
|
||||
|
||||
def _addArtist(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
try:
|
||||
importer.addArtisttoDB(self.id)
|
||||
except Exception, e:
|
||||
self.data = e
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _delArtist(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('DELETE from artists WHERE ArtistID="' + self.id + '"')
|
||||
myDB.action('DELETE from albums WHERE ArtistID="' + self.id + '"')
|
||||
myDB.action('DELETE from tracks WHERE ArtistID="' + self.id + '"')
|
||||
|
||||
|
||||
def _pauseArtist(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'ArtistID': self.id}
|
||||
newValueDict = {'Status': 'Paused'}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
def _resumeArtist(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'ArtistID': self.id}
|
||||
newValueDict = {'Status': 'Active'}
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
def _refreshArtist(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
try:
|
||||
importer.addArtisttoDB(self.id)
|
||||
except Exception, e:
|
||||
self.data = e
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _addAlbum(self, **kwargs):
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
try:
|
||||
importer.addReleaseById(self.id)
|
||||
except Exception, e:
|
||||
self.data = e
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
def _queueAlbum(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
if 'new' in kwargs:
|
||||
new = kwargs['new']
|
||||
else:
|
||||
new = False
|
||||
|
||||
|
||||
if 'lossless' in kwargs:
|
||||
lossless = kwargs['lossless']
|
||||
else:
|
||||
lossless = False
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'AlbumID': self.id}
|
||||
if lossless:
|
||||
newValueDict = {'Status': 'Wanted Lossless'}
|
||||
else:
|
||||
else:
|
||||
newValueDict = {'Status': 'Wanted'}
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
searcher.searchforalbum(self.id, new)
|
||||
|
||||
searcher.searchforalbum(self.id, new)
|
||||
|
||||
def _unqueueAlbum(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'AlbumID': self.id}
|
||||
newValueDict = {'Status': 'Skipped'}
|
||||
myDB.upsert("albums", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
def _forceSearch(self, **kwargs):
|
||||
searcher.searchforalbum()
|
||||
|
||||
|
||||
def _forceProcess(self, **kwargs):
|
||||
self.dir = None
|
||||
if 'dir' in kwargs:
|
||||
self.dir = kwargs['dir']
|
||||
postprocessor.forcePostProcess(self.dir)
|
||||
|
||||
|
||||
def _getVersion(self, **kwargs):
|
||||
self.data = {
|
||||
self.data = {
|
||||
'git_path' : headphones.GIT_PATH,
|
||||
'install_type' : headphones.INSTALL_TYPE,
|
||||
'current_version' : headphones.CURRENT_VERSION,
|
||||
'latest_version' : headphones.LATEST_VERSION,
|
||||
'commits_behind' : headphones.COMMITS_BEHIND,
|
||||
}
|
||||
|
||||
|
||||
def _checkGithub(self, **kwargs):
|
||||
versioncheck.checkGithub()
|
||||
self._getVersion()
|
||||
|
||||
|
||||
def _shutdown(self, **kwargs):
|
||||
headphones.SIGNAL = 'shutdown'
|
||||
|
||||
|
||||
def _restart(self, **kwargs):
|
||||
headphones.SIGNAL = 'restart'
|
||||
|
||||
|
||||
def _update(self, **kwargs):
|
||||
headphones.SIGNAL = 'update'
|
||||
|
||||
|
||||
def _getArtistArt(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getArtwork(ArtistID=self.id)
|
||||
|
||||
def _getAlbumArt(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getArtwork(AlbumID=self.id)
|
||||
|
||||
|
||||
def _getArtistInfo(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getInfo(ArtistID=self.id)
|
||||
|
||||
|
||||
def _getAlbumInfo(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getInfo(AlbumID=self.id)
|
||||
|
||||
|
||||
def _getArtistThumb(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getThumb(ArtistID=self.id)
|
||||
|
||||
def _getAlbumThumb(self, **kwargs):
|
||||
|
||||
|
||||
if 'id' not in kwargs:
|
||||
self.data = 'Missing parameter: id'
|
||||
return
|
||||
else:
|
||||
self.id = kwargs['id']
|
||||
|
||||
|
||||
self.data = cache.getThumb(AlbumID=self.id)
|
||||
|
||||
def _choose_specific_download(self, **kwargs):
|
||||
@@ -403,9 +403,9 @@ class Api(object):
|
||||
self.id = kwargs['id']
|
||||
|
||||
results = searcher.searchforalbum(self.id, choose_specific_download=True)
|
||||
|
||||
|
||||
results_as_dicts = []
|
||||
|
||||
|
||||
for result in results:
|
||||
|
||||
result_dict = {
|
||||
|
||||
+17
-17
@@ -31,7 +31,7 @@ from headphones import logger
|
||||
def dbFilename(filename="headphones.db"):
|
||||
|
||||
return os.path.join(headphones.DATA_DIR, filename)
|
||||
|
||||
|
||||
def getCacheSize():
|
||||
#this will protect against typecasting problems produced by empty string and None settings
|
||||
if not headphones.CACHE_SIZEMB:
|
||||
@@ -42,25 +42,25 @@ def getCacheSize():
|
||||
class DBConnection:
|
||||
|
||||
def __init__(self, filename="headphones.db"):
|
||||
|
||||
|
||||
self.filename = filename
|
||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||
#don't wait for the disk to finish writing
|
||||
self.connection.execute("PRAGMA synchronous = OFF")
|
||||
#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
|
||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize()*1024))
|
||||
self.connection.row_factory = sqlite3.Row
|
||||
|
||||
|
||||
def action(self, query, args=None):
|
||||
|
||||
if query == None:
|
||||
return
|
||||
|
||||
|
||||
sqlResult = None
|
||||
attempt = 0
|
||||
|
||||
|
||||
while attempt < 5:
|
||||
try:
|
||||
if args == None:
|
||||
@@ -82,28 +82,28 @@ class DBConnection:
|
||||
except sqlite3.DatabaseError, e:
|
||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||
raise
|
||||
|
||||
|
||||
return sqlResult
|
||||
|
||||
|
||||
def select(self, query, args=None):
|
||||
|
||||
|
||||
sqlResults = self.action(query, args).fetchall()
|
||||
|
||||
|
||||
if sqlResults == None:
|
||||
return []
|
||||
|
||||
|
||||
return sqlResults
|
||||
|
||||
|
||||
def upsert(self, tableName, valueDict, keyDict):
|
||||
|
||||
|
||||
changesBefore = self.connection.total_changes
|
||||
|
||||
|
||||
genParams = lambda myDict : [x + " = ?" for x in myDict.keys()]
|
||||
|
||||
|
||||
query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
||||
|
||||
|
||||
self.action(query, valueDict.values() + keyDict.values())
|
||||
|
||||
|
||||
if self.connection.total_changes == changesBefore:
|
||||
query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \
|
||||
" VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")"
|
||||
|
||||
@@ -8,7 +8,7 @@ from headphones import logger
|
||||
def getXldProfile(xldProfile):
|
||||
xldProfileNotFound = xldProfile
|
||||
expandedPath = os.path.expanduser('~/Library/Preferences/jp.tmkk.XLD.plist')
|
||||
try:
|
||||
try:
|
||||
preferences = plistlib.Plist.fromFile(expandedPath)
|
||||
except (expat.ExpatError):
|
||||
os.system("/usr/bin/plutil -convert xml1 %s" % expandedPath )
|
||||
@@ -61,7 +61,7 @@ def getXldProfile(xldProfile):
|
||||
elif 'TVBR' in ShortDesc:
|
||||
XLDAacOutput2_VBRQuality = int(profile.get('XLDAacOutput2_VBRQuality'))
|
||||
if XLDAacOutput2_VBRQuality > 122:
|
||||
xldBitrate = 320
|
||||
xldBitrate = 320
|
||||
elif XLDAacOutput2_VBRQuality > 113 and XLDAacOutput2_VBRQuality <= 122:
|
||||
xldBitrate = 285
|
||||
elif XLDAacOutput2_VBRQuality > 104 and XLDAacOutput2_VBRQuality <= 113:
|
||||
|
||||
+10
-10
@@ -226,27 +226,27 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
skip_log = 0
|
||||
#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
|
||||
|
||||
|
||||
rg_exists = myDB.action("SELECT * from albums WHERE AlbumID=?", [rg['id']]).fetchone()
|
||||
|
||||
if not forcefull:
|
||||
|
||||
|
||||
new_release_group = False
|
||||
|
||||
|
||||
try:
|
||||
check_release_date = rg_exists['ReleaseDate']
|
||||
except TypeError:
|
||||
check_release_date = None
|
||||
new_release_group = True
|
||||
|
||||
|
||||
|
||||
|
||||
if new_release_group:
|
||||
|
||||
|
||||
logger.info("[%s] Now adding: %s (New Release Group)" % (artist['artist_name'], rg['title']))
|
||||
new_releases = mb.get_new_releases(rgid,includeExtras)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
|
||||
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']))
|
||||
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 is a release, check the ReleaseID against the AlbumID to see if they differ (user updated)
|
||||
# check if the album already exists
|
||||
|
||||
|
||||
if not rg_exists:
|
||||
releaseid = rg['id']
|
||||
else:
|
||||
@@ -410,7 +410,7 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False):
|
||||
if rg_exists:
|
||||
newValueDict['DateAdded'] = rg_exists['DateAdded']
|
||||
newValueDict['Status'] = rg_exists['Status']
|
||||
|
||||
|
||||
else:
|
||||
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:
|
||||
return
|
||||
|
||||
|
||||
if not dir:
|
||||
if not headphones.MUSIC_DIR:
|
||||
return
|
||||
else:
|
||||
dir = headphones.MUSIC_DIR
|
||||
|
||||
|
||||
# If we're appending a dir, it's coming from the post processor which is
|
||||
# already bytestring
|
||||
if not append:
|
||||
dir = dir.encode(headphones.SYS_ENCODING)
|
||||
|
||||
|
||||
if not os.path.isdir(dir):
|
||||
logger.warn('Cannot find directory: %s. Not scanning' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
return
|
||||
@@ -47,7 +47,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
new_artists = []
|
||||
|
||||
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
if not append:
|
||||
# Clean up bad filepaths
|
||||
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):
|
||||
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']])
|
||||
|
||||
|
||||
del_have_tracks = myDB.select('SELECT Location, Matched, ArtistName from have')
|
||||
|
||||
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')
|
||||
|
||||
bitrates = []
|
||||
|
||||
|
||||
song_list = []
|
||||
new_song_count = 0
|
||||
file_count = 0
|
||||
|
||||
latest_subdirectory = []
|
||||
|
||||
|
||||
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
|
||||
#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:
|
||||
logger.error('Cannot read file: ' + unicode_song_path)
|
||||
continue
|
||||
|
||||
|
||||
# Grab the bitrates for the auto detect bit rate option
|
||||
if f.bitrate:
|
||||
bitrates.append(f.bitrate)
|
||||
|
||||
|
||||
# Use the album artist over the artist if available
|
||||
if 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
|
||||
else:
|
||||
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)
|
||||
|
||||
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,
|
||||
'CleanName' : CleanName
|
||||
}
|
||||
|
||||
|
||||
#song_list.append(song_dict)
|
||||
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.
|
||||
@@ -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')+"%"])
|
||||
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....")
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
##############song_list = helpers.multikeysort(song_list, ['ReleaseID', 'TrackID'])
|
||||
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
|
||||
song_count = 0
|
||||
latest_artist = []
|
||||
|
||||
|
||||
for song in song_list:
|
||||
|
||||
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'])
|
||||
elif latest_artist[song_count] != latest_artist[song_count-1] and song_count !=0:
|
||||
logger.info("Now matching songs by %s" % song['ArtistName'])
|
||||
|
||||
|
||||
#print song['ArtistName']+' - '+song['AlbumTitle']+' - '+song['TrackTitle']
|
||||
song_count += 1
|
||||
completion_percentage = float(song_count)/total_number_of_songs * 100
|
||||
|
||||
|
||||
if completion_percentage%10 == 0:
|
||||
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
|
||||
#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
|
||||
#(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']:
|
||||
|
||||
|
||||
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:
|
||||
controlValueDict = { 'ArtistName' : track['ArtistName'],
|
||||
'AlbumTitle' : track['AlbumTitle'],
|
||||
'TrackTitle' : track['TrackTitle'] }
|
||||
'TrackTitle' : track['TrackTitle'] }
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
@@ -231,10 +231,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
else:
|
||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
if track:
|
||||
controlValueDict = { 'CleanName' : track['CleanName']}
|
||||
controlValueDict = { 'CleanName' : track['CleanName']}
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
'Format' : song['Format'] }
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = { 'Location' : song['Location']}
|
||||
@@ -244,9 +244,9 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
controlValueDict2 = { 'Location' : song['Location']}
|
||||
newValueDict2 = { 'Matched' : "Failed"}
|
||||
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:
|
||||
controlValueDict = { 'ArtistName' : alltrack['ArtistName'],
|
||||
'AlbumTitle' : alltrack['AlbumTitle'],
|
||||
@@ -262,10 +262,10 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
else:
|
||||
alltrack = myDB.action('SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?', [song['CleanName']]).fetchone()
|
||||
if alltrack:
|
||||
controlValueDict = { 'CleanName' : alltrack['CleanName']}
|
||||
controlValueDict = { 'CleanName' : alltrack['CleanName']}
|
||||
newValueDict = { 'Location' : song['Location'],
|
||||
'BitRate' : song['BitRate'],
|
||||
'Format' : song['Format'] }
|
||||
'Format' : song['Format'] }
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
|
||||
controlValueDict2 = { 'Location' : song['Location']}
|
||||
@@ -279,35 +279,35 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
controlValueDict2 = { 'Location' : song['Location']}
|
||||
newValueDict2 = { 'Matched' : "Failed"}
|
||||
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']])
|
||||
|
||||
logger.info('Completed matching tracks from directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
|
||||
if not append:
|
||||
logger.info('Updating scanned artist track counts')
|
||||
|
||||
|
||||
# Clean up the new artist list
|
||||
unique_artists = {}.fromkeys(new_artists).keys()
|
||||
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]]
|
||||
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
|
||||
|
||||
|
||||
for artist in artists_checked:
|
||||
# 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
|
||||
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)
|
||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
|
||||
|
||||
|
||||
logger.info('Found %i new artists' % len(artist_list))
|
||||
|
||||
|
||||
if len(artist_list):
|
||||
if headphones.ADD_ARTISTS:
|
||||
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')
|
||||
for artist in artist_list:
|
||||
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||
|
||||
|
||||
if headphones.DETECT_BITRATE:
|
||||
headphones.PREFERRED_BITRATE = sum(bitrates)/len(bitrates)/1000
|
||||
|
||||
|
||||
else:
|
||||
# If we're appending a new album to the database, update the artists total 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]))
|
||||
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':
|
||||
new_album_status = "Downloaded"
|
||||
|
||||
|
||||
# I don't think we want to change Downloaded->Skipped.....
|
||||
# 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
|
||||
@@ -369,7 +369,7 @@ def update_album_status(AlbumID=None):
|
||||
# new_album_status = album['Status']
|
||||
else:
|
||||
new_album_status = album['Status']
|
||||
|
||||
|
||||
myDB.upsert("albums", {'Status' : new_album_status}, {'AlbumID' : album['AlbumID']})
|
||||
if new_album_status != 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'
|
||||
data = request.request_minidom(url, params=params)
|
||||
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
||||
url = data.getElementsByTagName("url")
|
||||
|
||||
|
||||
if url:
|
||||
lyricsurl = url[0].firstChild.nodeValue
|
||||
else:
|
||||
logger.info('No lyrics found for %s - %s' % (artist, song))
|
||||
return
|
||||
|
||||
|
||||
lyricspage = request.request_content(lyricsurl)
|
||||
|
||||
|
||||
if not lyricspage:
|
||||
logger.warn('Error fetching lyrics from: %s' % lyricsurl)
|
||||
return
|
||||
|
||||
m = re.compile('''<div class='lyricbox'><div class='rtMatcher'>.*?</div>(.*?)<!--''').search(lyricspage)
|
||||
|
||||
|
||||
if not m:
|
||||
m = re.compile('''<div class='lyricbox'><span style="padding:1em"><a href="/Category:Instrumental" title="Instrumental">''').search(lyricspage)
|
||||
if m:
|
||||
@@ -54,10 +54,10 @@ def getLyrics(artist, song):
|
||||
else:
|
||||
logger.warn('Cannot find lyrics on: %s' % lyricsurl)
|
||||
return
|
||||
|
||||
|
||||
lyrics = convert_html_entities(m.group(1)).replace('<br />', '\n')
|
||||
lyrics = re.sub('<.*?>', '', lyrics)
|
||||
|
||||
|
||||
return lyrics
|
||||
|
||||
def convert_html_entities(s):
|
||||
|
||||
+74
-74
@@ -34,7 +34,7 @@ def startmb():
|
||||
|
||||
mbuser = None
|
||||
mbpass = None
|
||||
|
||||
|
||||
if headphones.MIRROR == "musicbrainz.org":
|
||||
mbhost = "musicbrainz.org"
|
||||
mbport = 80
|
||||
@@ -51,7 +51,7 @@ def startmb():
|
||||
sleepytime = 0
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones")
|
||||
musicbrainzngs.set_hostname(mbhost + ":" + str(mbport))
|
||||
if sleepytime == 0:
|
||||
@@ -66,17 +66,17 @@ def startmb():
|
||||
logger.warn("No username or password set for VIP server")
|
||||
else:
|
||||
musicbrainzngs.hpauth(mbuser,mbpass)
|
||||
|
||||
|
||||
logger.debug('Using the following server values: MBHost: %s, MBPort: %i, Sleep Interval: %i', mbhost, mbport, sleepytime)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def findArtist(name, limit=1):
|
||||
|
||||
with mb_lock:
|
||||
with mb_lock:
|
||||
artistlist = []
|
||||
artistResults = None
|
||||
|
||||
|
||||
chars = set('!?*-')
|
||||
if any((c in chars) for c in name):
|
||||
name = '"'+name+'"'
|
||||
@@ -88,9 +88,9 @@ def findArtist(name, limit=1):
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if not artistResults:
|
||||
return False
|
||||
return False
|
||||
for result in artistResults:
|
||||
if 'disambiguation' in result:
|
||||
uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
|
||||
@@ -98,7 +98,7 @@ def findArtist(name, limit=1):
|
||||
uniquename = unicode(result['sort-name'])
|
||||
if result['name'] != uniquename and limit == 1:
|
||||
logger.info('Found an artist with a disambiguation: %s - doing an album based search' % name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
artistdict = findArtistbyAlbum(name)
|
||||
if not artistdict:
|
||||
logger.info('Cannot determine the best match from an artist/album search. Using top match instead')
|
||||
artistlist.append({
|
||||
@@ -108,10 +108,10 @@ def findArtist(name, limit=1):
|
||||
'id': unicode(result['id']),
|
||||
# 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
|
||||
# 'score': int(result['ext:score'])
|
||||
})
|
||||
})
|
||||
else:
|
||||
artistlist.append(artistdict)
|
||||
else:
|
||||
else:
|
||||
artistlist.append({
|
||||
'name': unicode(result['sort-name']),
|
||||
'uniquename': uniquename,
|
||||
@@ -120,10 +120,10 @@ def findArtist(name, limit=1):
|
||||
'score': int(result['ext:score'])
|
||||
})
|
||||
return artistlist
|
||||
|
||||
|
||||
def findRelease(name, limit=1, artist=None):
|
||||
|
||||
with mb_lock:
|
||||
with mb_lock:
|
||||
releaselist = []
|
||||
releaseResults = None
|
||||
|
||||
@@ -193,38 +193,38 @@ def findRelease(name, limit=1, artist=None):
|
||||
|
||||
def getArtist(artistid, extrasonly=False):
|
||||
|
||||
with mb_lock:
|
||||
with mb_lock:
|
||||
artist_dict = {}
|
||||
|
||||
|
||||
artist = None
|
||||
|
||||
|
||||
try:
|
||||
limit = 200
|
||||
artist = musicbrainzngs.get_artist_by_id(artistid)['artist']
|
||||
newRgs = None
|
||||
artist['release-group-list'] = []
|
||||
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
|
||||
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)
|
||||
except Exception,e:
|
||||
pass
|
||||
|
||||
|
||||
if not artist:
|
||||
return False
|
||||
|
||||
|
||||
#if 'disambiguation' in artist:
|
||||
# uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")")
|
||||
#else:
|
||||
# uniquename = unicode(artist['sort-name'])
|
||||
|
||||
|
||||
artist_dict['artist_name'] = unicode(artist['name'])
|
||||
|
||||
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
#artist_dict['artist_sortname'] = unicode(artist['sort-name'])
|
||||
#artist_dict['artist_uniquename'] = uniquename
|
||||
#artist_dict['artist_type'] = unicode(artist['type'])
|
||||
@@ -235,11 +235,11 @@ def getArtist(artistid, extrasonly=False):
|
||||
# if 'begin' in artist['life-span']:
|
||||
# artist_dict['artist_begindate'] = unicode(artist['life-span']['begin'])
|
||||
# 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 = []
|
||||
|
||||
|
||||
if not extrasonly:
|
||||
for rg in artist['release-group-list']:
|
||||
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']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
# See if we need to grab extras. Artist specific extras take precedence over global option
|
||||
# Global options are set when adding a new artist
|
||||
myDB = db.DBConnection()
|
||||
@@ -260,14 +260,14 @@ def getArtist(artistid, extrasonly=False):
|
||||
includeExtras = db_artist['IncludeExtras']
|
||||
except IndexError:
|
||||
includeExtras = False
|
||||
|
||||
|
||||
if includeExtras:
|
||||
|
||||
|
||||
# Need to convert extras string from something like '2,5.6' to ['ep','live','remix']
|
||||
extras = db_artist['Extras']
|
||||
extras_list = ["single", "ep", "compilation", "soundtrack", "live", "remix", "dj-mix", "mixtape/street", "spokenword", "audiobook", "broadcast", "interview", "other"]
|
||||
includes = []
|
||||
|
||||
|
||||
i = 1
|
||||
for extra in extras_list:
|
||||
if str(i) in extras:
|
||||
@@ -282,7 +282,7 @@ def getArtist(artistid, extrasonly=False):
|
||||
limit = 200
|
||||
newRgs = None
|
||||
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
|
||||
except WebServiceError, 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']),
|
||||
'url': u"http://musicbrainz.org/release-group/" + rg['id'],
|
||||
'type': unicode(rg['type'])
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
artist_dict['releasegroups'] = releasegroups
|
||||
|
||||
|
||||
return artist_dict
|
||||
|
||||
|
||||
def getReleaseGroup(rgid):
|
||||
"""
|
||||
Returns a list of releases in a release group
|
||||
"""
|
||||
with mb_lock:
|
||||
|
||||
|
||||
releaselist = []
|
||||
|
||||
|
||||
releaseGroup = None
|
||||
|
||||
|
||||
try:
|
||||
releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group']
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if not releaseGroup:
|
||||
return False
|
||||
else:
|
||||
return releaseGroup['release-list']
|
||||
|
||||
|
||||
def getRelease(releaseid, include_artist_info=True):
|
||||
"""
|
||||
Deep release search to get track info
|
||||
"""
|
||||
with mb_lock:
|
||||
|
||||
|
||||
release = {}
|
||||
results = None
|
||||
|
||||
|
||||
try:
|
||||
if include_artist_info:
|
||||
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')
|
||||
except WebServiceError, 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:
|
||||
return False
|
||||
|
||||
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['date'] = unicode(results['date']) if 'date' in results else None
|
||||
try:
|
||||
release['format'] = unicode(results['medium-list'][0]['format'])
|
||||
except:
|
||||
release['format'] = u'Unknown'
|
||||
|
||||
|
||||
try:
|
||||
release['country'] = unicode(results['country'])
|
||||
except:
|
||||
release['country'] = u'Unknown'
|
||||
|
||||
|
||||
|
||||
if include_artist_info:
|
||||
|
||||
|
||||
if 'release-group' in results:
|
||||
release['rgid'] = unicode(results['release-group']['id'])
|
||||
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['tracks'] = getTracksFromRelease(results)
|
||||
|
||||
|
||||
return release
|
||||
|
||||
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 ?
|
||||
newResults = newResults['release-list']
|
||||
results += newResults
|
||||
|
||||
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e)))
|
||||
time.sleep(5)
|
||||
return False
|
||||
|
||||
|
||||
if not results or len(results) == 0:
|
||||
return False
|
||||
|
||||
@@ -426,7 +426,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
|
||||
#all official releases should have the Official status included
|
||||
if not includeExtras and releasedata.get('status') != 'Official':
|
||||
continue
|
||||
|
||||
|
||||
release = {}
|
||||
rel_id_check = releasedata['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['AlbumID'] = unicode(rgid)
|
||||
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']
|
||||
if 'release-group' not in releasedata:
|
||||
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:
|
||||
logger.warn('Release ' + releasedata['id'] + ' has no Artists associated.')
|
||||
return False
|
||||
|
||||
|
||||
|
||||
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
|
||||
@@ -472,7 +472,7 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
|
||||
release['ReleaseFormat'] = unicode(packaged_medium)
|
||||
except:
|
||||
release['ReleaseFormat'] = u'Unknown'
|
||||
|
||||
|
||||
release['Tracks'] = getTracksFromRelease(releasedata)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
for track in release['Tracks']:
|
||||
|
||||
cleanname = helpers.cleanName(release['ArtistName'] + ' ' + release['AlbumTitle'] + ' ' + track['title'])
|
||||
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"ReleaseID": release['ReleaseID']}
|
||||
|
||||
@@ -510,20 +510,20 @@ def get_new_releases(rgid,includeExtras=False,forcefull=False):
|
||||
"TrackNumber": track['number'],
|
||||
"CleanName": cleanname
|
||||
}
|
||||
|
||||
|
||||
match = myDB.action('SELECT Location, BitRate, Format from have WHERE CleanName=?', [cleanname]).fetchone()
|
||||
|
||||
|
||||
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()
|
||||
#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:
|
||||
newValueDict['Location'] = match['Location']
|
||||
newValueDict['BitRate'] = match['BitRate']
|
||||
newValueDict['Format'] = match['Format']
|
||||
#myDB.action('UPDATE have SET Matched="True" WHERE Location=?', [match['Location']])
|
||||
myDB.action('UPDATE have SET Matched=? WHERE Location=?', (release['AlbumID'], match['Location']))
|
||||
|
||||
|
||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
||||
num_new_releases = num_new_releases + 1
|
||||
#print releasedata['title']
|
||||
@@ -556,19 +556,19 @@ def getTracksFromRelease(release):
|
||||
'url': u"http://musicbrainz.org/track/" + track['recording']['id'],
|
||||
'duration': int(track['length']) if 'length' in track else 0
|
||||
})
|
||||
totalTracks += 1
|
||||
totalTracks += 1
|
||||
return tracks
|
||||
|
||||
# Used when there is a disambiguation
|
||||
def findArtistbyAlbum(name):
|
||||
|
||||
myDB = db.DBConnection()
|
||||
|
||||
|
||||
artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone()
|
||||
|
||||
|
||||
if not artist:
|
||||
return False
|
||||
|
||||
|
||||
# Probably not neccessary but just want to double check
|
||||
if not artist['AlbumTitle']:
|
||||
return False
|
||||
@@ -576,20 +576,20 @@ def findArtistbyAlbum(name):
|
||||
term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"'
|
||||
|
||||
results = None
|
||||
|
||||
|
||||
try:
|
||||
results = musicbrainzngs.search_release_groups(term).get('release-group-list')
|
||||
except WebServiceError, e:
|
||||
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if not results:
|
||||
return False
|
||||
|
||||
artist_dict = {}
|
||||
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
|
||||
#if 'disambiguation' in newArtist:
|
||||
# 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['score'] = int(releaseGroup['ext:score'])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return artist_dict
|
||||
|
||||
|
||||
def findAlbumID(artist=None, album=None):
|
||||
|
||||
results = None
|
||||
@@ -632,6 +632,6 @@ def findAlbumID(artist=None, album=None):
|
||||
return False
|
||||
|
||||
if len(results) < 1:
|
||||
return False
|
||||
return False
|
||||
rgid = unicode(results[0]['id'])
|
||||
return rgid
|
||||
|
||||
@@ -123,7 +123,7 @@ class PROWL:
|
||||
def __init__(self):
|
||||
self.enabled = headphones.PROWL_ENABLED
|
||||
self.keys = headphones.PROWL_KEYS
|
||||
self.priority = headphones.PROWL_PRIORITY
|
||||
self.priority = headphones.PROWL_PRIORITY
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Prowl', options)
|
||||
@@ -150,7 +150,7 @@ class PROWL:
|
||||
if request_status == 200:
|
||||
logger.info(u"Prowl notifications sent.")
|
||||
return True
|
||||
elif request_status == 401:
|
||||
elif request_status == 401:
|
||||
logger.info(u"Prowl auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
@@ -413,7 +413,7 @@ class NMA:
|
||||
logger.error(u'Could not send notification to NotifyMyAndroid')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
class PUSHBULLET:
|
||||
|
||||
@@ -449,7 +449,7 @@ class PUSHBULLET:
|
||||
if request_status == 200:
|
||||
logger.info(u"PushBullet notifications sent.")
|
||||
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)
|
||||
return False
|
||||
else:
|
||||
@@ -482,7 +482,7 @@ class PUSHALOT:
|
||||
|
||||
http_handler = HTTPSConnection("pushalot.com")
|
||||
|
||||
data = {'AuthorizationToken': pushalot_authorizationtoken,
|
||||
data = {'AuthorizationToken': pushalot_authorizationtoken,
|
||||
'Title': event.encode('utf-8'),
|
||||
'Body': message.encode("utf-8") }
|
||||
|
||||
@@ -500,7 +500,7 @@ class PUSHALOT:
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushalot notifications sent.")
|
||||
return True
|
||||
elif request_status == 410:
|
||||
elif request_status == 410:
|
||||
logger.info(u"Pushalot auth failed: %s" % response.reason)
|
||||
return False
|
||||
else:
|
||||
@@ -552,7 +552,7 @@ class PUSHOVER:
|
||||
def __init__(self):
|
||||
self.enabled = headphones.PUSHOVER_ENABLED
|
||||
self.keys = headphones.PUSHOVER_KEYS
|
||||
self.priority = headphones.PUSHOVER_PRIORITY
|
||||
self.priority = headphones.PUSHOVER_PRIORITY
|
||||
if headphones.PUSHOVER_APITOKEN:
|
||||
self.application_token = headphones.PUSHOVER_APITOKEN
|
||||
pass
|
||||
@@ -566,7 +566,7 @@ class PUSHOVER:
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net")
|
||||
|
||||
data = {'token': self.application_token,
|
||||
data = {'token': self.application_token,
|
||||
'user': headphones.PUSHOVER_KEYS,
|
||||
'title': event,
|
||||
'message': message.encode("utf-8"),
|
||||
@@ -585,7 +585,7 @@ class PUSHOVER:
|
||||
if request_status == 200:
|
||||
logger.info(u"Pushover notifications sent.")
|
||||
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)
|
||||
return False
|
||||
else:
|
||||
|
||||
+153
-153
@@ -38,9 +38,9 @@ def checkFolder():
|
||||
snatched = myDB.select('SELECT * from snatched WHERE Status="Snatched"')
|
||||
|
||||
for album in snatched:
|
||||
|
||||
|
||||
if album['FolderName']:
|
||||
|
||||
|
||||
if album['Kind'] == 'nzb':
|
||||
download_dir = headphones.DOWNLOAD_DIR
|
||||
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: odd things can happen when there are diacritic characters in the folder name, need to translate them?
|
||||
release_list = None
|
||||
|
||||
try:
|
||||
|
||||
try:
|
||||
release_list = mb.getReleaseGroup(albumid)
|
||||
except Exception, e:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Error: %s' % (albumid, e))
|
||||
return
|
||||
|
||||
|
||||
if not release_list:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s' % albumid)
|
||||
return
|
||||
@@ -82,36 +82,36 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
releaseid = release_list[0]['id']
|
||||
|
||||
release_dict = mb.getRelease(releaseid)
|
||||
|
||||
|
||||
if not release_dict:
|
||||
logger.error('Unable to get release information for manual album with rgid: %s. Cannot continue' % albumid)
|
||||
return
|
||||
|
||||
logger.info(u"Now adding/updating artist: " + release_dict['artist_name'])
|
||||
|
||||
|
||||
if release_dict['artist_name'].startswith('The '):
|
||||
sortname = release_dict['artist_name'][4:]
|
||||
else:
|
||||
sortname = release_dict['artist_name']
|
||||
|
||||
|
||||
|
||||
|
||||
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
||||
newValueDict = {"ArtistName": release_dict['artist_name'],
|
||||
"ArtistSortName": sortname,
|
||||
"DateAdded": helpers.today(),
|
||||
"Status": "Paused"}
|
||||
|
||||
|
||||
logger.info("ArtistID: " + release_dict['artist_id'] + " , ArtistName: " + release_dict['artist_name'])
|
||||
|
||||
if headphones.INCLUDE_EXTRAS:
|
||||
newValueDict['IncludeExtras'] = 1
|
||||
newValueDict['Extras'] = headphones.EXTRAS
|
||||
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
|
||||
logger.info(u"Now adding album: " + release_dict['title'])
|
||||
controlValueDict = {"AlbumID": albumid}
|
||||
|
||||
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ReleaseID": albumid,
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
@@ -124,14 +124,14 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
}
|
||||
|
||||
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
|
||||
myDB.action('DELETE from tracks WHERE AlbumID=?', [albumid])
|
||||
for track in release_dict['tracks']:
|
||||
|
||||
|
||||
controlValueDict = {"TrackID": track['id'],
|
||||
"AlbumID": albumid}
|
||||
|
||||
|
||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||
"ArtistName": release_dict['artist_name'],
|
||||
"AlbumTitle": release_dict['title'],
|
||||
@@ -140,25 +140,25 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
"TrackDuration": track['duration'],
|
||||
"TrackNumber": track['number']
|
||||
}
|
||||
|
||||
|
||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||
|
||||
|
||||
controlValueDict = {"ArtistID": release_dict['artist_id']}
|
||||
newValueDict = {"Status": "Paused"}
|
||||
|
||||
|
||||
myDB.upsert("artists", newValueDict, controlValueDict)
|
||||
logger.info(u"Addition complete for: " + release_dict['title'] + " - " + release_dict['artist_name'])
|
||||
|
||||
release = myDB.action('SELECT * from albums WHERE AlbumID=?', [albumid]).fetchone()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
|
||||
|
||||
downloaded_track_list = []
|
||||
downloaded_cuecount = 0
|
||||
|
||||
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for files in f:
|
||||
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'):
|
||||
downloaded_cuecount += 1
|
||||
# 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")
|
||||
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):
|
||||
|
||||
|
||||
import getXldProfile
|
||||
|
||||
|
||||
(xldProfile, xldFormat, xldBitrate) = getXldProfile.getXldProfile(headphones.XLDPROFILE)
|
||||
if not xldFormat:
|
||||
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')
|
||||
else:
|
||||
xldencoder = os.path.join('/Applications','xld')
|
||||
|
||||
|
||||
for r,d,f in os.walk(albumpath):
|
||||
xldfolder = r
|
||||
xldfile = ''
|
||||
@@ -191,7 +191,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
xldfile = os.path.join(r, file)
|
||||
elif file.lower().endswith('.cue') and not xldcue:
|
||||
xldcue = os.path.join(r, file)
|
||||
|
||||
|
||||
if xldfile and xldcue and xldfolder:
|
||||
xldcmd = xldencoder
|
||||
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.debug(xldcmd)
|
||||
os.system(xldcmd)
|
||||
|
||||
|
||||
# count files, should now be more than original if xld successfully split
|
||||
|
||||
|
||||
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:
|
||||
if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
new_downloaded_track_list_count += 1
|
||||
|
||||
|
||||
if new_downloaded_track_list_count > len(downloaded_track_list):
|
||||
|
||||
|
||||
# rename original unsplit files
|
||||
for downloaded_track in downloaded_track_list:
|
||||
os.rename(downloaded_track, downloaded_track + '.original')
|
||||
|
||||
|
||||
#reload
|
||||
|
||||
|
||||
downloaded_track_list = []
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for file in f:
|
||||
if any(file.lower().endswith('.' + x.lower()) for x in headphones.MEDIA_FORMATS):
|
||||
downloaded_track_list.append(os.path.join(r, file))
|
||||
@@ -236,58 +236,58 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
except Exception, e:
|
||||
logger.info(u"Exception from MediaFile for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + u" : " + unicode(e))
|
||||
continue
|
||||
|
||||
|
||||
if not f.artist:
|
||||
continue
|
||||
if not f.album:
|
||||
continue
|
||||
|
||||
|
||||
metaartist = helpers.latinToAscii(f.artist.lower()).encode('UTF-8')
|
||||
dbartist = helpers.latinToAscii(release['ArtistName'].lower()).encode('UTF-8')
|
||||
metaalbum = helpers.latinToAscii(f.album.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 album: %s with album name: %s' % (metaalbum, dbalbum))
|
||||
|
||||
|
||||
if metaartist == dbartist and metaalbum == dbalbum:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
|
||||
return
|
||||
|
||||
|
||||
# test #2: filenames
|
||||
logger.debug('Metadata check failed. Verifying filenames...')
|
||||
for downloaded_track in downloaded_track_list:
|
||||
track_name = os.path.splitext(downloaded_track)[0]
|
||||
split_track_name = re.sub('[\.\-\_]', ' ', track_name).lower()
|
||||
for track in tracks:
|
||||
|
||||
|
||||
if not track['TrackTitle']:
|
||||
continue
|
||||
|
||||
|
||||
dbtrack = helpers.latinToAscii(track['TrackTitle'].lower()).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))
|
||||
|
||||
|
||||
if dbtrack in filetrack:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
|
||||
return
|
||||
|
||||
|
||||
# test #3: number of songs and duration
|
||||
logger.debug('Filename check failed. Verifying album length...')
|
||||
db_track_duration = 0
|
||||
downloaded_track_duration = 0
|
||||
|
||||
|
||||
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))
|
||||
if len(tracks) == len(downloaded_track_list):
|
||||
|
||||
|
||||
for track in tracks:
|
||||
try:
|
||||
db_track_duration += track['TrackDuration']/1000
|
||||
except:
|
||||
downloaded_track_duration = False
|
||||
break
|
||||
|
||||
|
||||
for downloaded_track in downloaded_track_list:
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
@@ -295,7 +295,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
except:
|
||||
downloaded_track_duration = False
|
||||
break
|
||||
|
||||
|
||||
if downloaded_track_duration and db_track_duration:
|
||||
logger.debug('Downloaded album duration: %i' % downloaded_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:
|
||||
doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind)
|
||||
return
|
||||
|
||||
|
||||
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])
|
||||
processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath)
|
||||
@@ -311,7 +311,7 @@ def verify(albumid, albumpath, Kind=None, forced=False):
|
||||
renameUnprocessedFolder(albumpath)
|
||||
else:
|
||||
logger.info(u"Already marked as unprocessed: " + albumpath.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
|
||||
|
||||
def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, Kind=None):
|
||||
|
||||
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:
|
||||
logger.warn("Cannot copy/move files to temp folder: " + new_folder.decode(headphones.SYS_ENCODING, 'replace') + ". Not continuing. Error: " + str(e))
|
||||
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,
|
||||
# but this is good to make sure we're not counting files that may have failed to move
|
||||
downloaded_track_list = []
|
||||
downloaded_cuecount = 0
|
||||
|
||||
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for files in f:
|
||||
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'):
|
||||
downloaded_cuecount += 1
|
||||
|
||||
@@ -363,14 +363,14 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
#start encoding
|
||||
if headphones.MUSIC_ENCODER:
|
||||
downloaded_track_list=music_encoder.encode(albumpath)
|
||||
|
||||
|
||||
if not downloaded_track_list:
|
||||
return
|
||||
|
||||
artwork = None
|
||||
album_art_path = albumart.getAlbumArt(albumid)
|
||||
if headphones.EMBED_ALBUM_ART or headphones.ADD_ALBUM_ART:
|
||||
|
||||
|
||||
if album_art_path:
|
||||
artwork = request.request_content(album_art_path)
|
||||
else:
|
||||
@@ -382,25 +382,25 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
if not artwork or len(artwork) < 100:
|
||||
artwork = False
|
||||
logger.info("No suitable album art found from Last.FM. Not adding album art")
|
||||
|
||||
|
||||
if headphones.EMBED_ALBUM_ART and artwork:
|
||||
embedAlbumArt(artwork, downloaded_track_list)
|
||||
|
||||
|
||||
if headphones.CLEANUP_FILES:
|
||||
cleanupFiles(albumpath)
|
||||
|
||||
|
||||
if headphones.ADD_ALBUM_ART and artwork:
|
||||
addAlbumArt(artwork, albumpath, release)
|
||||
|
||||
|
||||
if headphones.CORRECT_METADATA:
|
||||
correctMetadata(albumid, release, downloaded_track_list)
|
||||
|
||||
|
||||
if headphones.EMBED_LYRICS:
|
||||
embedLyrics(downloaded_track_list)
|
||||
|
||||
|
||||
if headphones.RENAME_FILES:
|
||||
renameFiles(albumpath, downloaded_track_list, release)
|
||||
|
||||
|
||||
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')
|
||||
albumpaths = [albumpath]
|
||||
@@ -408,9 +408,9 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
albumpaths = moveFiles(albumpath, release, tracks)
|
||||
else:
|
||||
albumpaths = [albumpath]
|
||||
|
||||
|
||||
updateFilePermissions(albumpaths)
|
||||
|
||||
|
||||
myDB = db.DBConnection()
|
||||
myDB.action('UPDATE albums SET status = "Downloaded" 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:
|
||||
for albumpath in albumpaths:
|
||||
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']))
|
||||
|
||||
if headphones.GROWL_ENABLED:
|
||||
@@ -432,18 +432,18 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
logger.info(u"Prowl request")
|
||||
prowl = notifiers.PROWL()
|
||||
prowl.notify(pushmessage,"Download and Postprocessing completed")
|
||||
|
||||
|
||||
if headphones.XBMC_ENABLED:
|
||||
xbmc = notifiers.XBMC()
|
||||
if headphones.XBMC_UPDATE:
|
||||
xbmc.update()
|
||||
if headphones.XBMC_NOTIFY:
|
||||
xbmc.notify(release['ArtistName'], release['AlbumTitle'], album_art_path)
|
||||
|
||||
|
||||
if headphones.LMS_ENABLED:
|
||||
lms = notifiers.LMS()
|
||||
lms.update()
|
||||
|
||||
|
||||
if headphones.PLEX_ENABLED:
|
||||
plex = notifiers.Plex()
|
||||
if headphones.PLEX_UPDATE:
|
||||
@@ -465,7 +465,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
syno = notifiers.Synoindex()
|
||||
for albumpath in albumpaths:
|
||||
syno.notify(albumpath)
|
||||
|
||||
|
||||
if headphones.PUSHOVER_ENABLED:
|
||||
pushmessage = release['ArtistName'] + ' - ' + release['AlbumTitle']
|
||||
logger.info(u"Pushover request")
|
||||
@@ -501,31 +501,31 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
||||
|
||||
def embedAlbumArt(artwork, downloaded_track_list):
|
||||
logger.info('Embedding album art')
|
||||
|
||||
|
||||
for downloaded_track in downloaded_track_list:
|
||||
try:
|
||||
f = MediaFile(downloaded_track)
|
||||
except:
|
||||
logger.error(u'Could not read %s. Not adding album art' % downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
continue
|
||||
|
||||
|
||||
logger.debug('Adding album art to: %s' % downloaded_track)
|
||||
|
||||
|
||||
try:
|
||||
f.art = artwork
|
||||
f.save()
|
||||
except Exception, e:
|
||||
logger.error(u'Error ebedding album art to: %s. Error: %s' % (downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), str(e)))
|
||||
continue
|
||||
|
||||
|
||||
def addAlbumArt(artwork, albumpath, release):
|
||||
logger.info('Adding album art to folder')
|
||||
|
||||
|
||||
try:
|
||||
year = release['ReleaseDate'][:4]
|
||||
except TypeError:
|
||||
year = ''
|
||||
|
||||
|
||||
values = { '$Artist': release['ArtistName'],
|
||||
'$Album': release['AlbumTitle'],
|
||||
'$Year': year,
|
||||
@@ -533,7 +533,7 @@ def addAlbumArt(artwork, albumpath, release):
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
|
||||
|
||||
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')
|
||||
@@ -551,7 +551,7 @@ def addAlbumArt(artwork, albumpath, release):
|
||||
except Exception, e:
|
||||
logger.error('Error saving album art: %s' % str(e))
|
||||
return
|
||||
|
||||
|
||||
def cleanupFiles(albumpath):
|
||||
logger.info('Cleaning up files')
|
||||
for r,d,f in os.walk(albumpath):
|
||||
@@ -562,14 +562,14 @@ def cleanupFiles(albumpath):
|
||||
os.remove(os.path.join(r, files))
|
||||
except Exception, e:
|
||||
logger.error(u'Could not remove file: %s. Error: %s' % (files.decode(headphones.SYS_ENCODING, 'replace'), e))
|
||||
|
||||
|
||||
def moveFiles(albumpath, release, tracks):
|
||||
|
||||
try:
|
||||
year = release['ReleaseDate'][:4]
|
||||
except TypeError:
|
||||
year = ''
|
||||
|
||||
|
||||
artist = release['ArtistName'].replace('/', '_')
|
||||
album = release['AlbumTitle'].replace('/', '_')
|
||||
if headphones.FILE_UNDERSCORES:
|
||||
@@ -582,12 +582,12 @@ def moveFiles(albumpath, release, tracks):
|
||||
sortname = release['ArtistName'][4:] + ", The"
|
||||
else:
|
||||
sortname = release['ArtistName']
|
||||
|
||||
|
||||
if sortname[0].isdigit():
|
||||
firstchar = '0-9'
|
||||
else:
|
||||
firstchar = sortname[0]
|
||||
|
||||
|
||||
|
||||
values = { '$Artist': artist,
|
||||
'$SortArtist': sortname,
|
||||
@@ -602,24 +602,24 @@ def moveFiles(albumpath, release, tracks):
|
||||
'$type': releasetype.lower(),
|
||||
'$first': firstchar.lower()
|
||||
}
|
||||
|
||||
|
||||
folder = helpers.replace_all(headphones.FOLDER_FORMAT.strip(), values)
|
||||
|
||||
folder = helpers.replace_illegal_chars(folder, type="folder")
|
||||
folder = folder.replace('./', '_/').replace('/.','/_')
|
||||
|
||||
|
||||
if folder.endswith('.'):
|
||||
folder = folder[:-1] + '_'
|
||||
|
||||
|
||||
if folder.startswith('.'):
|
||||
folder = '_' + folder[1:]
|
||||
|
||||
|
||||
# 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
|
||||
files_to_move = []
|
||||
lossy_media = False
|
||||
lossless_media = False
|
||||
|
||||
|
||||
for r,d,f in os.walk(albumpath):
|
||||
for files in f:
|
||||
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:
|
||||
make_lossy_folder = False
|
||||
make_lossless_folder = False
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
# 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 lossy_media:
|
||||
@@ -646,7 +646,7 @@ def moveFiles(albumpath, release, tracks):
|
||||
make_lossy_folder = True
|
||||
|
||||
last_folder = headphones.FOLDER_FORMAT.strip().split('/')[-1]
|
||||
|
||||
|
||||
if make_lossless_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():
|
||||
@@ -662,7 +662,7 @@ def moveFiles(albumpath, release, tracks):
|
||||
|
||||
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
temp_folder = folder
|
||||
|
||||
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
@@ -672,7 +672,7 @@ def moveFiles(albumpath, release, tracks):
|
||||
else:
|
||||
temp_folder = newfolder
|
||||
break
|
||||
|
||||
|
||||
if not os.path.exists(lossless_destination_path):
|
||||
try:
|
||||
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))
|
||||
if not make_lossy_folder:
|
||||
return [albumpath]
|
||||
|
||||
|
||||
if make_lossy_folder:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
if not headphones.REPLACE_EXISTING_FOLDERS or create_duplicate_folder:
|
||||
temp_folder = folder
|
||||
|
||||
|
||||
i = 1
|
||||
while True:
|
||||
newfolder = temp_folder + '[%i]' % i
|
||||
@@ -705,35 +705,35 @@ def moveFiles(albumpath, release, tracks):
|
||||
else:
|
||||
temp_folder = newfolder
|
||||
break
|
||||
|
||||
|
||||
if not os.path.exists(lossy_destination_path):
|
||||
try:
|
||||
os.makedirs(lossy_destination_path)
|
||||
except Exception, e:
|
||||
logger.error('Could not create folder for %s. Not moving: %s' % (release['AlbumTitle'], e))
|
||||
return [albumpath]
|
||||
|
||||
|
||||
logger.info('Checking which files we need to move.....')
|
||||
|
||||
# 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 make_lossy_folder and make_lossless_folder:
|
||||
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
|
||||
|
||||
if any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSY_MEDIA_FORMATS):
|
||||
helpers.smartMove(file_to_move, lossy_destination_path)
|
||||
|
||||
|
||||
elif any(file_to_move.lower().endswith('.' + x.lower()) for x in headphones.LOSSLESS_MEDIA_FORMATS):
|
||||
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
|
||||
else:
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if moved_to_lossy_folder or moved_to_lossless_folder:
|
||||
try:
|
||||
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")
|
||||
else:
|
||||
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:
|
||||
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
helpers.smartMove(file_to_move, lossless_destination_path)
|
||||
|
||||
|
||||
else:
|
||||
|
||||
for file_to_move in files_to_move:
|
||||
helpers.smartMove(file_to_move, lossy_destination_path)
|
||||
|
||||
# Chmod the directories using the folder_format (script courtesy of premiso!)
|
||||
folder_list = folder.split('/')
|
||||
folder_list = folder.split('/')
|
||||
temp_fs = []
|
||||
|
||||
|
||||
if make_lossless_folder:
|
||||
temp_fs.append(headphones.LOSSLESS_DESTINATION_DIR)
|
||||
|
||||
|
||||
if make_lossy_folder:
|
||||
temp_fs.append(headphones.DESTINATION_DIR)
|
||||
|
||||
|
||||
for temp_f in temp_fs:
|
||||
|
||||
|
||||
for f in folder_list:
|
||||
|
||||
|
||||
temp_f = os.path.join(temp_f, f)
|
||||
|
||||
|
||||
try:
|
||||
os.chmod(os.path.normpath(temp_f).encode(headphones.SYS_ENCODING, 'replace'), int(headphones.FOLDER_PERMISSIONS, 8))
|
||||
except Exception, 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
|
||||
try:
|
||||
shutil.rmtree(albumpath)
|
||||
except Exception, e:
|
||||
logger.error('Could not remove directory: %s. %s', albumpath, e)
|
||||
|
||||
|
||||
destination_paths = []
|
||||
|
||||
|
||||
if make_lossy_folder:
|
||||
destination_paths.append(lossy_destination_path)
|
||||
if make_lossless_folder:
|
||||
destination_paths.append(lossless_destination_path)
|
||||
|
||||
|
||||
return destination_paths
|
||||
|
||||
|
||||
def correctMetadata(albumid, release, downloaded_track_list):
|
||||
|
||||
|
||||
logger.info('Preparing to write metadata to tracks....')
|
||||
lossy_items = []
|
||||
lossless_items = []
|
||||
|
||||
|
||||
# Process lossless & lossy media formats separately
|
||||
for downloaded_track in downloaded_track_list:
|
||||
|
||||
|
||||
try:
|
||||
|
||||
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:
|
||||
logger.warn("Skipping: %s because it is not a mutagen friendly file format", downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
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))
|
||||
|
||||
for items in [lossy_items, lossless_items]:
|
||||
|
||||
|
||||
if not items:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
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:
|
||||
@@ -822,29 +822,29 @@ def correctMetadata(albumid, release, downloaded_track_list):
|
||||
if str(rec) == 'recommendation.none':
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
return
|
||||
|
||||
|
||||
if candidates:
|
||||
dist, info, mapping, extra_items, extra_tracks = candidates[0]
|
||||
else:
|
||||
logger.warn('No accurate album match found for %s, %s - not writing metadata', release['ArtistName'], release['AlbumTitle'])
|
||||
return
|
||||
|
||||
|
||||
logger.info('Beets recommendation for tagging items: %s' % rec)
|
||||
|
||||
# TODO: Handle extra_items & extra_tracks
|
||||
|
||||
|
||||
autotag.apply_metadata(info, mapping)
|
||||
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
item.write()
|
||||
logger.info("Successfully applied metadata to: %s", item.path.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
except Exception, e:
|
||||
logger.warn("Error writing metadata to '%s': %s", item.path.decode(headphones.SYS_ENCODING, 'replace'), str(e))
|
||||
|
||||
|
||||
def embedLyrics(downloaded_track_list):
|
||||
logger.info('Adding lyrics')
|
||||
|
||||
|
||||
# TODO: If adding lyrics for flac & lossy, only fetch the lyrics once
|
||||
# and apply it to both files
|
||||
for downloaded_track in downloaded_track_list:
|
||||
@@ -854,7 +854,7 @@ def embedLyrics(downloaded_track_list):
|
||||
except:
|
||||
logger.error('Could not read %s. Not checking lyrics', track_title)
|
||||
continue
|
||||
|
||||
|
||||
if f.albumartist and f.title:
|
||||
metalyrics = lyrics.getLyrics(f.albumartist, f.title)
|
||||
elif f.artist and f.title:
|
||||
@@ -862,7 +862,7 @@ def embedLyrics(downloaded_track_list):
|
||||
else:
|
||||
logger.info('No artist/track metadata found for track: %s. Not fetching lyrics', track_title)
|
||||
metalyrics = None
|
||||
|
||||
|
||||
if metalyrics:
|
||||
logger.debug('Adding lyrics to: %s', track_title)
|
||||
f.lyrics = metalyrics
|
||||
@@ -899,28 +899,28 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
tracknumber = ''
|
||||
else:
|
||||
tracknumber = '%02d' % f.track
|
||||
|
||||
|
||||
if not f.title:
|
||||
|
||||
|
||||
basename = os.path.basename(downloaded_track.decode(headphones.SYS_ENCODING, 'replace'))
|
||||
title = os.path.splitext(basename)[0]
|
||||
ext = os.path.splitext(basename)[1]
|
||||
|
||||
|
||||
new_file_name = helpers.cleanTitle(title) + ext
|
||||
|
||||
|
||||
else:
|
||||
title = f.title
|
||||
|
||||
|
||||
if release['ArtistName'] == "Various Artists" and f.artist:
|
||||
artistname = f.artist
|
||||
else:
|
||||
artistname = release['ArtistName']
|
||||
|
||||
|
||||
if artistname.startswith('The '):
|
||||
sortname = artistname[4:] + ", The"
|
||||
else:
|
||||
sortname = artistname
|
||||
|
||||
|
||||
values = { '$Disc': discnumber,
|
||||
'$Track': tracknumber,
|
||||
'$Title': title,
|
||||
@@ -936,12 +936,12 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
'$album': release['AlbumTitle'].lower(),
|
||||
'$year': year
|
||||
}
|
||||
|
||||
|
||||
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_illegal_chars(new_file_name).encode(headphones.SYS_ENCODING, 'replace')
|
||||
|
||||
if headphones.FILE_UNDERSCORES:
|
||||
@@ -949,9 +949,9 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
|
||||
if new_file_name.startswith('.'):
|
||||
new_file_name = new_file_name.replace(".", "_", 1)
|
||||
|
||||
|
||||
new_file = os.path.join(albumpath, new_file_name)
|
||||
|
||||
|
||||
if downloaded_track == new_file_name:
|
||||
logger.debug("Renaming for: " + downloaded_track.decode(headphones.SYS_ENCODING, 'replace') + " is not neccessary")
|
||||
continue
|
||||
@@ -962,7 +962,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
||||
except Exception, e:
|
||||
logger.error('Error renaming file: %s. Error: %s', downloaded_track.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||
continue
|
||||
|
||||
|
||||
def updateFilePermissions(albumpaths):
|
||||
|
||||
for folder in albumpaths:
|
||||
@@ -977,21 +977,21 @@ def updateFilePermissions(albumpaths):
|
||||
continue
|
||||
|
||||
def renameUnprocessedFolder(albumpath):
|
||||
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
if i == 0:
|
||||
new_folder_name = albumpath + ' (Unprocessed)'
|
||||
else:
|
||||
new_folder_name = albumpath + ' (Unprocessed)[%i]' % i
|
||||
|
||||
|
||||
if os.path.exists(new_folder_name):
|
||||
i += 1
|
||||
|
||||
|
||||
else:
|
||||
os.rename(albumpath, new_folder_name)
|
||||
return
|
||||
|
||||
|
||||
def forcePostProcess(dir=None, expand_subfolders=True, album_dir=None):
|
||||
|
||||
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
|
||||
myDB = db.DBConnection()
|
||||
|
||||
|
||||
for folder in folders:
|
||||
folder_basename = os.path.basename(folder).decode(headphones.SYS_ENCODING, 'replace')
|
||||
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
|
||||
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
|
||||
download_path = os.path.join(headphones.TORRENTBLACKHOLE_DIR, torrent_name)
|
||||
|
||||
|
||||
if bestqual[2].startswith("magnet:"):
|
||||
if headphones.OPEN_MAGNET_LINKS:
|
||||
try:
|
||||
@@ -689,7 +689,7 @@ def send_to_downloader(data, bestqual, album):
|
||||
|
||||
else:# if headphones.TORRENT_DOWNLOADER == 2:
|
||||
logger.info("Sending torrent to uTorrent")
|
||||
|
||||
|
||||
# rutracker needs cookies to be set, pass the .torrent file instead of url
|
||||
if bestqual[3] == 'rutracker.org':
|
||||
file_or_url, _hash = rutracker.get_torrent(bestqual[2])
|
||||
|
||||
@@ -24,7 +24,7 @@ class Rutracker():
|
||||
|
||||
# Stores a number of login attempts to prevent recursion.
|
||||
#login_counter = 0
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.cookiejar = cookielib.CookieJar()
|
||||
@@ -40,11 +40,11 @@ class Rutracker():
|
||||
return False
|
||||
|
||||
#self.login_counter += 1
|
||||
|
||||
|
||||
# No recursion wanted.
|
||||
#if self.login_counter > 1:
|
||||
# return False
|
||||
|
||||
|
||||
params = urllib.urlencode({"login_username" : login,
|
||||
"login_password" : password,
|
||||
"login" : "Вход"})
|
||||
@@ -53,19 +53,19 @@ class Rutracker():
|
||||
self.opener.open("http://login.rutracker.org/forum/login.php", params)
|
||||
except :
|
||||
pass
|
||||
|
||||
|
||||
# Check if we're logged in
|
||||
for cookie in self.cookiejar:
|
||||
if cookie.name == 'bb_data':
|
||||
self.logged_in = True
|
||||
|
||||
|
||||
return self.logged_in
|
||||
|
||||
def searchurl(self, artist, album, year, format):
|
||||
"""
|
||||
Return the search url
|
||||
"""
|
||||
|
||||
|
||||
# Build search url
|
||||
searchterm = ''
|
||||
if artist != 'Various Artists':
|
||||
@@ -74,69 +74,69 @@ class Rutracker():
|
||||
searchterm = searchterm + album
|
||||
searchterm = searchterm + ' '
|
||||
searchterm = searchterm + year
|
||||
|
||||
|
||||
providerurl = "http://rutracker.org/forum/tracker.php"
|
||||
|
||||
|
||||
if format == 'lossless':
|
||||
format = '+lossless'
|
||||
elif format == 'lossless+mp3':
|
||||
format = '+lossless||mp3||aac'
|
||||
else:
|
||||
format = '+mp3||aac'
|
||||
|
||||
|
||||
# sort by size, descending.
|
||||
sort = '&o=7&s=2'
|
||||
|
||||
|
||||
searchurl = "%s?nm=%s%s%s" % (providerurl, urllib.quote(searchterm), format, sort)
|
||||
|
||||
|
||||
return searchurl
|
||||
|
||||
|
||||
def search(self, searchurl, maxsize, minseeders, albumid):
|
||||
"""
|
||||
Parse the search results and return valid torrent list
|
||||
"""
|
||||
|
||||
|
||||
titles = []
|
||||
urls = []
|
||||
seeders = []
|
||||
sizes = []
|
||||
torrentlist = []
|
||||
torrentlist = []
|
||||
rulist = []
|
||||
|
||||
|
||||
try:
|
||||
|
||||
|
||||
page = self.opener.open(searchurl, timeout=60)
|
||||
soup = BeautifulSoup(page.read())
|
||||
|
||||
|
||||
# Debug
|
||||
#logger.debug (soup.prettify())
|
||||
|
||||
#logger.debug (soup.prettify())
|
||||
|
||||
# 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()
|
||||
titles.append(title)
|
||||
|
||||
|
||||
# Download URL
|
||||
for link in soup.find_all('a', attrs={'class' : 'small tr-dl dl-stub'}):
|
||||
url = link.get('href')
|
||||
urls.append(url)
|
||||
|
||||
|
||||
# Seeders
|
||||
for link in soup.find_all('b', attrs={'class' : 'seedmed'}):
|
||||
seeder = link.get_text()
|
||||
seeders.append(seeder)
|
||||
|
||||
|
||||
# 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
|
||||
sizes.append(size)
|
||||
|
||||
|
||||
except :
|
||||
pass
|
||||
|
||||
|
||||
# Combine lists
|
||||
torrentlist = zip(titles, urls, seeders, sizes)
|
||||
|
||||
|
||||
# return if nothing found
|
||||
if not torrentlist:
|
||||
return False
|
||||
@@ -151,20 +151,20 @@ class Rutracker():
|
||||
myDB = db.DBConnection()
|
||||
tracks = myDB.select('SELECT * from tracks WHERE AlbumID=?', [albumid])
|
||||
hptrackcount = len(tracks)
|
||||
|
||||
|
||||
if not hptrackcount:
|
||||
logger.info('headphones track info not found, cannot compare to torrent')
|
||||
return False
|
||||
|
||||
|
||||
# Return all valid entries, ignored, required words now checked in searcher.py
|
||||
|
||||
|
||||
#unwantedlist = ['promo', 'vinyl', '[lp]', 'songbook', 'tvrip', 'hdtv', 'dvd']
|
||||
|
||||
formatlist = ['ape', 'flac', 'ogg', 'm4a', 'aac', 'mp3', 'wav', 'aif']
|
||||
deluxelist = ['deluxe', 'edition', 'japanese', 'exclusive']
|
||||
|
||||
|
||||
for torrent in torrentlist:
|
||||
|
||||
|
||||
returntitle = torrent[0].encode('utf-8')
|
||||
url = torrent[1]
|
||||
seeders = torrent[2]
|
||||
@@ -183,11 +183,11 @@ class Rutracker():
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
# Debug
|
||||
#for cookie in self.cookiejar:
|
||||
# logger.debug ('Cookie: %s' % cookie)
|
||||
|
||||
|
||||
try:
|
||||
page = self.opener.open(url)
|
||||
torrent = page.read()
|
||||
@@ -198,11 +198,11 @@ class Rutracker():
|
||||
except Exception, e:
|
||||
logger.error('Error getting torrent: %s' % e)
|
||||
return False
|
||||
|
||||
|
||||
# get torrent track count and check for cue
|
||||
trackcount = 0
|
||||
cuecount = 0
|
||||
|
||||
|
||||
if 'files' in metainfo: # multi
|
||||
for pathfile in metainfo['files']:
|
||||
path = pathfile['path']
|
||||
@@ -240,22 +240,22 @@ class Rutracker():
|
||||
else:
|
||||
break
|
||||
totallogcount = totallogcount + logcount
|
||||
|
||||
|
||||
if totallogcount > 0:
|
||||
trackcount = totallogcount
|
||||
logger.debug ('rutracker logtrackcount: %s' % totallogcount)
|
||||
|
||||
|
||||
# If torrent track count = hp track count then return torrent,
|
||||
# if greater, check for deluxe/special/foreign editions
|
||||
# if less, then allow if it's a single track with a cue
|
||||
valid = False
|
||||
|
||||
|
||||
if trackcount == hptrackcount:
|
||||
valid = True
|
||||
elif trackcount > hptrackcount:
|
||||
if any(deluxe in title for deluxe in deluxelist):
|
||||
valid = True
|
||||
|
||||
|
||||
# Add to list
|
||||
if valid:
|
||||
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')
|
||||
logger.info('Starting update for %i active artists' % len(activeartists))
|
||||
|
||||
|
||||
for artist in activeartists:
|
||||
|
||||
|
||||
artistid = artist[0]
|
||||
importer.addArtisttoDB(artistid=artistid, extrasonly=False, forcefull=forcefull)
|
||||
|
||||
|
||||
logger.info('Active artist update complete')
|
||||
|
||||
@@ -27,7 +27,7 @@ class utorrentclient(object):
|
||||
UTSetting = namedtuple("UTSetting", ["name", "int", "str", "access"])
|
||||
|
||||
def __init__(self, base_url = None, username = None, password = None,):
|
||||
|
||||
|
||||
host = headphones.UTORRENT_HOST
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
|
||||
@@ -324,9 +324,9 @@ class WebInterface(object):
|
||||
|
||||
def choose_specific_download(self, AlbumID):
|
||||
results = searcher.searchforalbum(AlbumID, choose_specific_download=True)
|
||||
|
||||
|
||||
results_as_dicts = []
|
||||
|
||||
|
||||
for result in results:
|
||||
|
||||
result_dict = {
|
||||
@@ -341,7 +341,7 @@ class WebInterface(object):
|
||||
s = simplejson.dumps(results_as_dicts)
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return s
|
||||
|
||||
|
||||
choose_specific_download.exposed = True
|
||||
|
||||
def download_specific_release(self, AlbumID, title, size, url, provider, kind, **kwargs):
|
||||
@@ -878,9 +878,9 @@ class WebInterface(object):
|
||||
def getArtistjson(self, ArtistID, **kwargs):
|
||||
myDB = db.DBConnection()
|
||||
artist = myDB.action('SELECT * FROM artists WHERE ArtistID=?', [ArtistID]).fetchone()
|
||||
artist_json = json.dumps({
|
||||
artist_json = json.dumps({
|
||||
'ArtistName': artist['ArtistName'],
|
||||
'Status': artist['Status']
|
||||
'Status': artist['Status']
|
||||
})
|
||||
return artist_json
|
||||
getArtistjson.exposed=True
|
||||
|
||||
+10
-10
@@ -43,7 +43,7 @@ def initialize(options={}):
|
||||
logger.warn(u"Disabled HTTPS because of missing CERT and KEY files")
|
||||
headphones.ENABLE_HTTPS = False
|
||||
enable_https = False
|
||||
|
||||
|
||||
options_dict = {
|
||||
'log.screen': False,
|
||||
'server.thread_pool': 10,
|
||||
@@ -54,14 +54,14 @@ def initialize(options={}):
|
||||
'tools.encode.encoding' : 'utf-8',
|
||||
'tools.decode.on' : True,
|
||||
}
|
||||
|
||||
|
||||
if enable_https:
|
||||
options_dict['server.ssl_certificate'] = https_cert
|
||||
options_dict['server.ssl_private_key'] = https_key
|
||||
protocol = "https"
|
||||
else:
|
||||
protocol = "http"
|
||||
|
||||
|
||||
logger.info("Starting Headphones on %s://%s:%d/", protocol, options['http_host'], options['http_port'])
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
@@ -95,7 +95,7 @@ def initialize(options={}):
|
||||
'tools.staticdir.dir': headphones.CACHE_DIR
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if options['http_password'] != "":
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
@@ -104,20 +104,20 @@ def initialize(options={}):
|
||||
{options['http_username']:options['http_password']})
|
||||
})
|
||||
conf['/api'] = { 'tools.auth_basic.on': False }
|
||||
|
||||
|
||||
|
||||
# Prevent time-outs
|
||||
cherrypy.engine.timeout_monitor.unsubscribe()
|
||||
|
||||
|
||||
cherrypy.tree.mount(WebInterface(), options['http_root'], config = conf)
|
||||
|
||||
|
||||
try:
|
||||
cherrypy.process.servers.check_port(options['http_host'], options['http_port'])
|
||||
cherrypy.server.start()
|
||||
except IOError:
|
||||
print 'Failed to start on port: %i. Is something else running?' % (options['http_port'])
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
cherrypy.server.wait()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from lib.apscheduler.jobstores.base import JobStore
|
||||
class RAMJobStore(JobStore):
|
||||
def __init__(self):
|
||||
self.jobs = []
|
||||
|
||||
|
||||
def add_job(self, job):
|
||||
self.jobs.append(job)
|
||||
|
||||
|
||||
@@ -242,6 +242,6 @@ def apply_metadata(album_info, mapping):
|
||||
item[field] = value
|
||||
if track_info.disctitle is not None:
|
||||
item.disctitle = track_info.disctitle
|
||||
|
||||
|
||||
# Headphones seal of approval
|
||||
item.comments = 'tagged by headphones/beets'
|
||||
|
||||
@@ -147,7 +147,7 @@ def transform_value(value):
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
return value
|
||||
|
||||
def transform_data(data):
|
||||
@@ -209,7 +209,7 @@ def transform_data(data):
|
||||
if section == 'importfeeds':
|
||||
if key.startswith(IMPORTFEEDS_PREFIX):
|
||||
key = key[len(IMPORTFEEDS_PREFIX):]
|
||||
|
||||
|
||||
sec_out[key] = transform_value(value)
|
||||
|
||||
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
|
||||
# database. Do nothing.
|
||||
return
|
||||
|
||||
|
||||
if os.path.exists(destfn):
|
||||
if replace:
|
||||
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 += 'ing'
|
||||
return gerund
|
||||
|
||||
|
||||
def _reasonstr(self):
|
||||
"""Get the reason as a string."""
|
||||
if isinstance(self.reason, unicode):
|
||||
|
||||
+1
-1
@@ -299,7 +299,7 @@ class EncodingDetector:
|
||||
else:
|
||||
xml_endpos = 1024
|
||||
html_endpos = max(2048, int(len(markup) * 0.05))
|
||||
|
||||
|
||||
declared_encoding = None
|
||||
declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos)
|
||||
if not declared_encoding_match and is_html:
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ def rword(length=5):
|
||||
def rsentence(length=4):
|
||||
"Generate a random sentence-like string."
|
||||
return " ".join(rword(random.randint(4,9)) for i in range(length))
|
||||
|
||||
|
||||
def rdoc(num_elements=1000):
|
||||
"""Randomly generate an invalid HTML document."""
|
||||
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__
|
||||
data = rdoc(num_elements)
|
||||
print "Generated a large invalid HTML document (%d bytes)." % len(data)
|
||||
|
||||
|
||||
for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]:
|
||||
success = False
|
||||
try:
|
||||
|
||||
+70
-70
@@ -95,20 +95,20 @@ engine.listeners['before_request'] = set()
|
||||
engine.listeners['after_request'] = set()
|
||||
|
||||
class _TimeoutMonitor(process.plugins.Monitor):
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.servings = []
|
||||
process.plugins.Monitor.__init__(self, bus, self.run)
|
||||
|
||||
|
||||
def before_request(self):
|
||||
self.servings.append((serving.request, serving.response))
|
||||
|
||||
|
||||
def after_request(self):
|
||||
try:
|
||||
self.servings.remove((serving.request, serving.response))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Check timeout on all responses. (Internal)"""
|
||||
for req, resp in self.servings:
|
||||
@@ -132,7 +132,7 @@ server.subscribe()
|
||||
|
||||
def quickstart(root=None, script_name="", config=None):
|
||||
"""Mount the given root, start the builtin server (and engine), then block.
|
||||
|
||||
|
||||
root: an instance of a "controller class" (a collection of page handler
|
||||
methods) which represents the root 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
|
||||
handle requests to "http://www.example.com:8080/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 root
|
||||
of the URI, it MUST be an empty string (not "/").
|
||||
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:
|
||||
_global_conf_alias.update(config)
|
||||
|
||||
|
||||
tree.mount(root, script_name, config)
|
||||
|
||||
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, "console_control_handler"):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
|
||||
engine.start()
|
||||
engine.block()
|
||||
|
||||
@@ -165,7 +165,7 @@ from cherrypy._cpcompat import threadlocal as _local
|
||||
|
||||
class _Serving(_local):
|
||||
"""An interface for registering request and response objects.
|
||||
|
||||
|
||||
Rather than have a separate "thread local" object for the request and
|
||||
the response, this class works as a single threadlocal container for
|
||||
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
|
||||
thread-safe way.
|
||||
"""
|
||||
|
||||
|
||||
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
|
||||
_httputil.Host("127.0.0.1", 1111))
|
||||
"""
|
||||
The request object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
|
||||
response = _cprequest.Response()
|
||||
"""
|
||||
The response object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
|
||||
def load(self, request, response):
|
||||
self.request = request
|
||||
self.response = response
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Remove all attributes of self."""
|
||||
self.__dict__.clear()
|
||||
@@ -197,54 +197,54 @@ serving = _Serving()
|
||||
|
||||
|
||||
class _ThreadLocalProxy(object):
|
||||
|
||||
|
||||
__slots__ = ['__attrname__', '__dict__']
|
||||
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.__attrname__ = attrname
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return getattr(child, name)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ("__attrname__", ):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
child = getattr(serving, self.__attrname__)
|
||||
setattr(child, name, value)
|
||||
|
||||
|
||||
def __delattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
delattr(child, name)
|
||||
|
||||
|
||||
def _get_dict(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
d = child.__class__.__dict__.copy()
|
||||
d.update(child.__dict__)
|
||||
return d
|
||||
__dict__ = property(_get_dict)
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return child[key]
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
child[key] = value
|
||||
|
||||
|
||||
def __delitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
del child[key]
|
||||
|
||||
|
||||
def __contains__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return key in child
|
||||
|
||||
|
||||
def __len__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return len(child)
|
||||
|
||||
|
||||
def __nonzero__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return bool(child)
|
||||
@@ -285,14 +285,14 @@ from cherrypy import _cplogging
|
||||
|
||||
class _GlobalLogManager(_cplogging.LogManager):
|
||||
"""A site-wide LogManager; routes to app.log or global log as appropriate.
|
||||
|
||||
|
||||
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
|
||||
cherrypy.log() and cherrypy.log.access(). If either
|
||||
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
|
||||
request, the message will be sent to the site-wide logger.
|
||||
"""
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""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
|
||||
@@ -301,7 +301,7 @@ class _GlobalLogManager(_cplogging.LogManager):
|
||||
else:
|
||||
log = self
|
||||
return log.error(*args, **kwargs)
|
||||
|
||||
|
||||
def access(self):
|
||||
"""Log an access message to the app.log or global log as appropriate."""
|
||||
try:
|
||||
@@ -335,7 +335,7 @@ def expose(func=None, alias=None):
|
||||
for a in alias:
|
||||
parents[a.replace(".", "_")] = func
|
||||
return func
|
||||
|
||||
|
||||
import sys, types
|
||||
if isinstance(func, (types.FunctionType, types.MethodType)):
|
||||
if alias is None:
|
||||
@@ -364,23 +364,23 @@ def expose(func=None, alias=None):
|
||||
return expose_
|
||||
|
||||
def popargs(*args, **kwargs):
|
||||
"""A decorator for _cp_dispatch
|
||||
"""A decorator for _cp_dispatch
|
||||
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
|
||||
|
||||
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
|
||||
is then forwarded on to the next vpath element.
|
||||
|
||||
|
||||
Note that any existing (and exposed) member function of the class that
|
||||
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
|
||||
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
|
||||
a "blank class" whose only function is to provide _cp_dispatch.
|
||||
|
||||
|
||||
If there are path elements after the arguments, or more arguments
|
||||
are requested than are available in the vpath, then the 'handler'
|
||||
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
|
||||
function used as the next object INSTEAD of adding the parameters to
|
||||
cherrypy.request.args.
|
||||
|
||||
|
||||
This decorator may be used in one of two ways:
|
||||
|
||||
|
||||
As a class decorator:
|
||||
@cherrypy.popargs('year', 'month', 'day')
|
||||
class Blog:
|
||||
@@ -399,47 +399,47 @@ def popargs(*args, **kwargs):
|
||||
#Process the parameters here; any url like
|
||||
#/, /2009, /2009/12, or /2009/12/31
|
||||
#will fill in the appropriate parameters.
|
||||
|
||||
|
||||
def create(self):
|
||||
#This link will still be available at /create. Defined functions
|
||||
#take precedence over arguments.
|
||||
|
||||
|
||||
Or as a member of a class:
|
||||
class Blog:
|
||||
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
||||
#...
|
||||
|
||||
|
||||
The handler argument may be used to mix arguments with built in functions.
|
||||
For instance, the following setup allows different activities at the
|
||||
day, month, and year level:
|
||||
|
||||
|
||||
class DayHandler:
|
||||
def index(self, year, month, day):
|
||||
#Do something with this day; probably list entries
|
||||
|
||||
|
||||
def delete(self, year, month, day):
|
||||
#Delete all entries for this day
|
||||
|
||||
|
||||
@cherrypy.popargs('day', handler=DayHandler())
|
||||
class MonthHandler:
|
||||
def index(self, year, month):
|
||||
#Do something with this month; probably list entries
|
||||
|
||||
|
||||
def delete(self, year, month):
|
||||
#Delete all entries for this month
|
||||
|
||||
|
||||
@cherrypy.popargs('month', handler=MonthHandler())
|
||||
class YearHandler:
|
||||
def index(self, year):
|
||||
#Do something with this year
|
||||
|
||||
|
||||
#...
|
||||
|
||||
|
||||
@cherrypy.popargs('year', handler=YearHandler())
|
||||
class Root:
|
||||
def index(self):
|
||||
#...
|
||||
|
||||
|
||||
"""
|
||||
|
||||
#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 \
|
||||
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
|
||||
handler_call = True
|
||||
|
||||
|
||||
def decorated(cls_or_self=None, vpath=None):
|
||||
if inspect.isclass(cls_or_self):
|
||||
#cherrypy.popargs is a class decorator
|
||||
cls = cls_or_self
|
||||
setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
|
||||
return cls
|
||||
|
||||
|
||||
#We're in the actual function
|
||||
self = cls_or_self
|
||||
parms = {}
|
||||
@@ -476,16 +476,16 @@ def popargs(*args, **kwargs):
|
||||
if not vpath:
|
||||
break
|
||||
parms[arg] = vpath.pop(0)
|
||||
|
||||
|
||||
if handler is not None:
|
||||
if handler_call:
|
||||
return handler(**parms)
|
||||
else:
|
||||
request.params.update(parms)
|
||||
return handler
|
||||
|
||||
|
||||
request.params.update(parms)
|
||||
|
||||
|
||||
#If we are the ultimate handler, then to prevent our _cp_dispatch
|
||||
#from being called again, we will resolve remaining elements through
|
||||
#getattr() directly.
|
||||
@@ -493,28 +493,28 @@ def popargs(*args, **kwargs):
|
||||
return getattr(self, vpath.pop(0), None)
|
||||
else:
|
||||
return self
|
||||
|
||||
|
||||
return decorated
|
||||
|
||||
def url(path="", qs="", script_name=None, base=None, relative=None):
|
||||
"""Create an absolute URL for the given path.
|
||||
|
||||
|
||||
If 'path' starts with a slash ('/'), this will return
|
||||
(base + script_name + path + qs).
|
||||
If it does not start with a slash, this returns
|
||||
(base + script_name [+ request.path_info] + path + qs).
|
||||
|
||||
|
||||
If script_name is None, cherrypy.request will be used
|
||||
to find a script_name, 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.
|
||||
|
||||
|
||||
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.
|
||||
If you call url(qs=cherrypy.request.query_string), you should get the
|
||||
original browser URL (assuming no internal redirections).
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
|
||||
|
||||
if request.app:
|
||||
if not path.startswith("/"):
|
||||
# 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:
|
||||
if pi.endswith('/') and pi != '/':
|
||||
pi = pi[:-1]
|
||||
|
||||
|
||||
if path == "":
|
||||
path = pi
|
||||
else:
|
||||
path = _urljoin(pi, path)
|
||||
|
||||
|
||||
if script_name is None:
|
||||
script_name = request.script_name
|
||||
if base is None:
|
||||
base = request.base
|
||||
|
||||
|
||||
newurl = base + script_name + path + qs
|
||||
else:
|
||||
# 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 base is None:
|
||||
base = server.base()
|
||||
|
||||
|
||||
path = (script_name or "") + path
|
||||
newurl = base + path + qs
|
||||
|
||||
|
||||
if './' in newurl:
|
||||
# Normalize the URL by removing ./ and ../
|
||||
atoms = []
|
||||
@@ -574,12 +574,12 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
||||
else:
|
||||
atoms.append(atom)
|
||||
newurl = '/'.join(atoms)
|
||||
|
||||
|
||||
# At this point, we should have a fully-qualified absolute URL.
|
||||
|
||||
|
||||
if relative is None:
|
||||
relative = getattr(request.app, "relative_urls", False)
|
||||
|
||||
|
||||
# See http://www.ietf.org/rfc/rfc2396.txt
|
||||
if relative == 'server':
|
||||
# "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 = (['..'] * len(old)) + new
|
||||
newurl = '/'.join(new)
|
||||
|
||||
|
||||
return newurl
|
||||
|
||||
|
||||
|
||||
+42
-42
@@ -7,25 +7,25 @@ from cherrypy._cpcompat import iteritems, copykeys, builtins
|
||||
|
||||
class Checker(object):
|
||||
"""A checker for CherryPy sites and their mounted applications.
|
||||
|
||||
|
||||
When this object is called at engine startup, it executes each
|
||||
of its own methods whose names start with ``check_``. If you wish
|
||||
to disable selected checks, simply add a line in your global
|
||||
config which sets the appropriate method to False::
|
||||
|
||||
|
||||
[global]
|
||||
checker.check_skipped_app_config = False
|
||||
|
||||
|
||||
You may also dynamically add or replace ``check_*`` methods in this way.
|
||||
"""
|
||||
|
||||
|
||||
on = True
|
||||
"""If True (the default), run all checks; if False, turn off all checks."""
|
||||
|
||||
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._populate_known_types()
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Run all check_* methods."""
|
||||
if self.on:
|
||||
@@ -39,14 +39,14 @@ class Checker(object):
|
||||
method()
|
||||
finally:
|
||||
warnings.formatwarning = oldformatwarning
|
||||
|
||||
|
||||
def formatwarning(self, message, category, filename, lineno, line=None):
|
||||
"""Function to format a warning."""
|
||||
return "CherryPy Checker:\n%s\n\n" % message
|
||||
|
||||
|
||||
# This value should be set inside _cpconfig.
|
||||
global_config_contained_paths = False
|
||||
|
||||
|
||||
def check_app_config_entries_dont_start_with_script_name(self):
|
||||
"""Check for Application config with sections that repeat script_name."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
@@ -63,13 +63,13 @@ class Checker(object):
|
||||
warnings.warn(
|
||||
"The application mounted at %r has config " \
|
||||
"entries that start with its script name: %r" % (sn, key))
|
||||
|
||||
|
||||
def check_site_config_entries_in_app_config(self):
|
||||
"""Check for mounted Applications that have site-scoped config."""
|
||||
for sn, app in iteritems(cherrypy.tree.apps):
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
|
||||
|
||||
msg = []
|
||||
for section, entries in iteritems(app.config):
|
||||
if section.startswith('/'):
|
||||
@@ -84,7 +84,7 @@ class Checker(object):
|
||||
"config. Move them to a [global] section and pass them "
|
||||
"to cherrypy.config.update() instead of tree.mount()." % sn)
|
||||
warnings.warn(os.linesep.join(msg))
|
||||
|
||||
|
||||
def check_skipped_app_config(self):
|
||||
"""Check for mounted Applications that have no config."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
@@ -100,7 +100,7 @@ class Checker(object):
|
||||
"cherrypy.tree.mount(..., config=app_config)")
|
||||
warnings.warn(msg)
|
||||
return
|
||||
|
||||
|
||||
def check_app_config_brackets(self):
|
||||
"""Check for Application config with extraneous brackets in section names."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
@@ -115,7 +115,7 @@ class Checker(object):
|
||||
"section names with extraneous brackets: %r. "
|
||||
"Config *files* need brackets; config *dicts* "
|
||||
"(e.g. passed to tree.mount) do not." % (sn, key))
|
||||
|
||||
|
||||
def check_static_paths(self):
|
||||
"""Check Application config for incorrect static paths."""
|
||||
# Use the dummy Request object in the main thread.
|
||||
@@ -128,7 +128,7 @@ class Checker(object):
|
||||
# get_resource will populate request.config
|
||||
request.get_resource(section + "/dummy.html")
|
||||
conf = request.config.get
|
||||
|
||||
|
||||
if conf("tools.staticdir.on", False):
|
||||
msg = ""
|
||||
root = conf("tools.staticdir.root")
|
||||
@@ -154,20 +154,20 @@ class Checker(object):
|
||||
fulldir = os.path.join(root, dir)
|
||||
if not os.path.isabs(fulldir):
|
||||
msg = "%r is not an absolute path." % fulldir
|
||||
|
||||
|
||||
if fulldir and not os.path.exists(fulldir):
|
||||
if msg:
|
||||
msg += "\n"
|
||||
msg += ("%r (root + dir) is not an existing "
|
||||
"filesystem path." % fulldir)
|
||||
|
||||
|
||||
if msg:
|
||||
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
|
||||
% (msg, section, root, dir))
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------------- Compatibility -------------------------- #
|
||||
|
||||
|
||||
obsolete = {
|
||||
'server.default_content_type': 'tools.response_headers.headers',
|
||||
'log_access_file': 'log.access_file',
|
||||
@@ -181,9 +181,9 @@ class Checker(object):
|
||||
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
|
||||
'cherrypy.Application(Root())))'),
|
||||
}
|
||||
|
||||
|
||||
deprecated = {}
|
||||
|
||||
|
||||
def _compat(self, config):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
for section, conf in config.items():
|
||||
@@ -204,7 +204,7 @@ class Checker(object):
|
||||
elif section in self.deprecated:
|
||||
warnings.warn("%r is deprecated. Use %r instead."
|
||||
% (section, self.deprecated[section]))
|
||||
|
||||
|
||||
def check_compatibility(self):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
self._compat(cherrypy.config)
|
||||
@@ -212,12 +212,12 @@ class Checker(object):
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._compat(app.config)
|
||||
|
||||
|
||||
|
||||
|
||||
# ------------------------ Known Namespaces ------------------------ #
|
||||
|
||||
|
||||
extra_config_namespaces = []
|
||||
|
||||
|
||||
def _known_ns(self, app):
|
||||
ns = ["wsgi"]
|
||||
ns.extend(copykeys(app.toolboxes))
|
||||
@@ -225,7 +225,7 @@ class Checker(object):
|
||||
ns.extend(copykeys(app.request_class.namespaces))
|
||||
ns.extend(copykeys(cherrypy.config.namespaces))
|
||||
ns += self.extra_config_namespaces
|
||||
|
||||
|
||||
for section, conf in app.config.items():
|
||||
is_path_section = section.startswith("/")
|
||||
if is_path_section and isinstance(conf, dict):
|
||||
@@ -250,7 +250,7 @@ class Checker(object):
|
||||
"because the %r tool was not found.\n"
|
||||
"section: [%s]" % (k, atoms[1], section))
|
||||
warnings.warn(msg)
|
||||
|
||||
|
||||
def check_config_namespaces(self):
|
||||
"""Process config and warn on each unknown config namespace."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
@@ -259,16 +259,16 @@ class Checker(object):
|
||||
self._known_ns(app)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------------- Config Types -------------------------- #
|
||||
|
||||
|
||||
known_config_types = {}
|
||||
|
||||
|
||||
def _populate_known_types(self):
|
||||
b = [x for x in vars(builtins).values()
|
||||
if type(x) is type(str)]
|
||||
|
||||
|
||||
def traverse(obj, namespace):
|
||||
for name in dir(obj):
|
||||
# Hack for 3.2's warning about body_params
|
||||
@@ -277,17 +277,17 @@ class Checker(object):
|
||||
vtype = type(getattr(obj, name, None))
|
||||
if vtype in b:
|
||||
self.known_config_types[namespace + "." + name] = vtype
|
||||
|
||||
|
||||
traverse(cherrypy.request, "request")
|
||||
traverse(cherrypy.response, "response")
|
||||
traverse(cherrypy.server, "server")
|
||||
traverse(cherrypy.engine, "engine")
|
||||
traverse(cherrypy.log, "log")
|
||||
|
||||
|
||||
def _known_types(self, config):
|
||||
msg = ("The config entry %r in section %r is of type %r, "
|
||||
"which does not match the expected type %r.")
|
||||
|
||||
|
||||
for section, conf in config.items():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.items():
|
||||
@@ -305,7 +305,7 @@ class Checker(object):
|
||||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
|
||||
|
||||
def check_config_types(self):
|
||||
"""Assert that config values are of the same type as default values."""
|
||||
self._known_types(cherrypy.config)
|
||||
@@ -313,10 +313,10 @@ class Checker(object):
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_types(app.config)
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------- Specific config warnings -------------------- #
|
||||
|
||||
|
||||
def check_localhost(self):
|
||||
"""Warn if any socket_host is 'localhost'. See #711."""
|
||||
for k, v in cherrypy.config.items():
|
||||
|
||||
@@ -50,14 +50,14 @@ attribute. For example::
|
||||
|
||||
class Demo:
|
||||
_cp_config = {'tools.gzip.on': True}
|
||||
|
||||
|
||||
def index(self):
|
||||
return "Hello world"
|
||||
index.exposed = True
|
||||
index._cp_config = {'request.show_tracebacks': False}
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
This behavior is only guaranteed for the default dispatcher.
|
||||
Other dispatchers may have different restrictions on where
|
||||
you can attach _cp_config attributes.
|
||||
@@ -127,13 +127,13 @@ NamespaceSet = reprconf.NamespaceSet
|
||||
|
||||
def merge(base, other):
|
||||
"""Merge one app config (from a dict, file, or filename) into another.
|
||||
|
||||
|
||||
If the given config is a filename, it will be appended to
|
||||
the list of files to monitor for "autoreload" changes.
|
||||
"""
|
||||
if isinstance(other, basestring):
|
||||
cherrypy.engine.autoreload.files.add(other)
|
||||
|
||||
|
||||
# Load other into base
|
||||
for section, value_map in reprconf.as_dict(other).items():
|
||||
if not isinstance(value_map, dict):
|
||||
@@ -164,7 +164,7 @@ class Config(reprconf.Config):
|
||||
if 'tools.staticdir.dir' in config:
|
||||
config['tools.staticdir.section'] = "global"
|
||||
reprconf.Config._apply(self, config)
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Decorator for page handlers to set _cp_config."""
|
||||
if args:
|
||||
@@ -226,14 +226,14 @@ def _server_namespace_handler(k, v):
|
||||
# to configure additional HTTP servers.
|
||||
if not hasattr(cherrypy, "servers"):
|
||||
cherrypy.servers = {}
|
||||
|
||||
|
||||
servername, k = atoms
|
||||
if servername not in cherrypy.servers:
|
||||
from cherrypy import _cpserver
|
||||
cherrypy.servers[servername] = _cpserver.Server()
|
||||
# On by default, but 'on = False' can unsubscribe it (see below).
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
||||
|
||||
if k == 'on':
|
||||
if v:
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
||||
+67
-67
@@ -23,12 +23,12 @@ from cherrypy._cpcompat import set
|
||||
|
||||
class PageHandler(object):
|
||||
"""Callable which sets response.body."""
|
||||
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
self.callable = callable
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
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__'):
|
||||
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
|
||||
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
|
||||
raise
|
||||
|
||||
@@ -117,10 +117,10 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
||||
# 2. not enough body parameters -> 400
|
||||
# 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
|
||||
# possibilities
|
||||
#
|
||||
#
|
||||
# In the case where the method does not allow body
|
||||
# arguments it's definitely a 404.
|
||||
message = None
|
||||
@@ -187,16 +187,16 @@ class LateParamPageHandler(PageHandler):
|
||||
takes that into account, and allows request.params to be 'bound late'
|
||||
(it's more complicated than that, but that's the effect).
|
||||
"""
|
||||
|
||||
|
||||
def _get_kwargs(self):
|
||||
kwargs = cherrypy.serving.request.params.copy()
|
||||
if self._kwargs:
|
||||
kwargs.update(self._kwargs)
|
||||
return kwargs
|
||||
|
||||
|
||||
def _set_kwargs(self, kwargs):
|
||||
self._kwargs = kwargs
|
||||
|
||||
|
||||
kwargs = property(_get_kwargs, _set_kwargs,
|
||||
doc='page handler kwargs (with '
|
||||
'cherrypy.request.params copied in)')
|
||||
@@ -217,7 +217,7 @@ else:
|
||||
|
||||
class Dispatcher(object):
|
||||
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
|
||||
|
||||
|
||||
The tree is rooted at cherrypy.request.app.root, and each hierarchical
|
||||
component in the path_info argument is matched to a corresponding nested
|
||||
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
|
||||
"default" may match a portion of the path_info (but only when no longer
|
||||
substring of the path_info matches some other object).
|
||||
|
||||
|
||||
This is the default, built-in dispatcher for CherryPy.
|
||||
"""
|
||||
|
||||
|
||||
dispatch_method_name = '_cp_dispatch'
|
||||
"""
|
||||
The name of the dispatch method that nodes may optionally implement
|
||||
to provide their own dynamic dispatch algorithm.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, dispatch_method_name=None,
|
||||
translate=punctuation_to_underscores):
|
||||
validate_translator(translate)
|
||||
@@ -246,27 +246,27 @@ class Dispatcher(object):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
func, vpath = self.find_handler(path_info)
|
||||
|
||||
|
||||
if func:
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
def find_handler(self, path):
|
||||
"""Return the appropriate page handler, plus any virtual path.
|
||||
|
||||
|
||||
This will return two objects. The first will be a callable,
|
||||
which can be used to generate page output. Any parameters from
|
||||
the query string or request body will be sent to that callable
|
||||
as keyword arguments.
|
||||
|
||||
|
||||
The callable is found by traversing the application's tree,
|
||||
starting from cherrypy.request.app.root, and matching path
|
||||
components to successive objects in the tree. For example, the
|
||||
URL "/path/to/handler" might return root.path.to.handler.
|
||||
|
||||
|
||||
The second object returned will be a list of names which are
|
||||
'virtual path' components: parts of the URL which are dynamic,
|
||||
and were not used when looking up the handler.
|
||||
@@ -277,7 +277,7 @@ class Dispatcher(object):
|
||||
app = request.app
|
||||
root = app.root
|
||||
dispatch_name = self.dispatch_method_name
|
||||
|
||||
|
||||
# Get config for the root object/path.
|
||||
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
|
||||
fullpath_len = len(fullpath)
|
||||
@@ -288,14 +288,14 @@ class Dispatcher(object):
|
||||
if "/" in app.config:
|
||||
nodeconf.update(app.config["/"])
|
||||
object_trail = [['root', root, nodeconf, segleft]]
|
||||
|
||||
|
||||
node = root
|
||||
iternames = fullpath[:]
|
||||
while iternames:
|
||||
name = iternames[0]
|
||||
# map to legal Python identifiers (e.g. replace '.' with '_')
|
||||
objname = name.translate(self.translate)
|
||||
|
||||
|
||||
nodeconf = {}
|
||||
subnode = getattr(node, objname, None)
|
||||
pre_len = len(iternames)
|
||||
@@ -327,7 +327,7 @@ class Dispatcher(object):
|
||||
)
|
||||
elif segleft == pre_len:
|
||||
#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)
|
||||
iternames.pop(0)
|
||||
segleft -= 1
|
||||
@@ -337,7 +337,7 @@ class Dispatcher(object):
|
||||
# Get _cp_config attached to this node.
|
||||
if hasattr(node, "_cp_config"):
|
||||
nodeconf.update(node._cp_config)
|
||||
|
||||
|
||||
# Mix in values from app.config for this path.
|
||||
existing_len = fullpath_len - pre_len
|
||||
if existing_len != 0:
|
||||
@@ -349,9 +349,9 @@ class Dispatcher(object):
|
||||
curpath += '/' + seg
|
||||
if curpath in app.config:
|
||||
nodeconf.update(app.config[curpath])
|
||||
|
||||
|
||||
object_trail.append([name, node, nodeconf, segleft])
|
||||
|
||||
|
||||
def set_conf():
|
||||
"""Collapse all object_trail config into cherrypy.request.config."""
|
||||
base = cherrypy.config.copy()
|
||||
@@ -362,15 +362,15 @@ class Dispatcher(object):
|
||||
if 'tools.staticdir.dir' in conf:
|
||||
base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
|
||||
return base
|
||||
|
||||
|
||||
# Try successive objects (reverse order)
|
||||
num_candidates = len(object_trail) - 1
|
||||
for i in range(num_candidates, -1, -1):
|
||||
|
||||
|
||||
name, candidate, nodeconf, segleft = object_trail[i]
|
||||
if candidate is None:
|
||||
continue
|
||||
|
||||
|
||||
# Try a "default" method on the current leaf.
|
||||
if hasattr(candidate, "default"):
|
||||
defhandler = candidate.default
|
||||
@@ -382,10 +382,10 @@ class Dispatcher(object):
|
||||
# See http://www.cherrypy.org/ticket/613
|
||||
request.is_index = path.endswith("/")
|
||||
return defhandler, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
|
||||
# Uncomment the next line to restrict positional params to "default".
|
||||
# if i < num_candidates - 2: continue
|
||||
|
||||
|
||||
# Try the current leaf.
|
||||
if getattr(candidate, 'exposed', False):
|
||||
request.config = set_conf()
|
||||
@@ -400,7 +400,7 @@ class Dispatcher(object):
|
||||
# positional parameters (virtual paths).
|
||||
request.is_index = False
|
||||
return candidate, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
|
||||
# We didn't find anything
|
||||
request.config = set_conf()
|
||||
return None, []
|
||||
@@ -408,20 +408,20 @@ class Dispatcher(object):
|
||||
|
||||
class MethodDispatcher(Dispatcher):
|
||||
"""Additional dispatch based on cherrypy.request.method.upper().
|
||||
|
||||
|
||||
Methods named GET, POST, etc will be called on an exposed class.
|
||||
The method names must be all caps; the appropriate Allow header
|
||||
will be output showing all capitalized method names as allowable
|
||||
HTTP verbs.
|
||||
|
||||
|
||||
Note that the containing class must be exposed, not the methods.
|
||||
"""
|
||||
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
resource, vpath = self.find_handler(path_info)
|
||||
|
||||
|
||||
if resource:
|
||||
# Set Allow header
|
||||
avail = [m for m in dir(resource) if m.isupper()]
|
||||
@@ -429,7 +429,7 @@ class MethodDispatcher(Dispatcher):
|
||||
avail.append("HEAD")
|
||||
avail.sort()
|
||||
cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
|
||||
|
||||
|
||||
# Find the subhandler
|
||||
meth = request.method.upper()
|
||||
func = getattr(resource, meth, None)
|
||||
@@ -439,7 +439,7 @@ class MethodDispatcher(Dispatcher):
|
||||
# Grab any _cp_config on the subhandler.
|
||||
if hasattr(func, "_cp_config"):
|
||||
request.config.update(func._cp_config)
|
||||
|
||||
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
@@ -451,7 +451,7 @@ class MethodDispatcher(Dispatcher):
|
||||
|
||||
class RoutesDispatcher(object):
|
||||
"""A Routes based dispatcher for CherryPy."""
|
||||
|
||||
|
||||
def __init__(self, full_result=False):
|
||||
"""
|
||||
Routes dispatcher
|
||||
@@ -465,14 +465,14 @@ class RoutesDispatcher(object):
|
||||
self.controllers = {}
|
||||
self.mapper = routes.Mapper()
|
||||
self.mapper.controller_scan = self.controllers.keys
|
||||
|
||||
|
||||
def connect(self, name, route, controller, **kwargs):
|
||||
self.controllers[name] = controller
|
||||
self.mapper.connect(name, route, controller=name, **kwargs)
|
||||
|
||||
|
||||
def redirect(self, url):
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
func = self.find_handler(path_info)
|
||||
@@ -480,13 +480,13 @@ class RoutesDispatcher(object):
|
||||
cherrypy.serving.request.handler = LateParamPageHandler(func)
|
||||
else:
|
||||
cherrypy.serving.request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
def find_handler(self, path_info):
|
||||
"""Find the right page handler, and set request.config."""
|
||||
import routes
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
config = routes.request_config()
|
||||
config.mapper = self.mapper
|
||||
if hasattr(request, 'wsgi_environ'):
|
||||
@@ -494,9 +494,9 @@ class RoutesDispatcher(object):
|
||||
config.host = request.headers.get('Host', None)
|
||||
config.protocol = request.scheme
|
||||
config.redirect = self.redirect
|
||||
|
||||
|
||||
result = self.mapper.match(path_info)
|
||||
|
||||
|
||||
config.mapper_dict = result
|
||||
params = {}
|
||||
if result:
|
||||
@@ -505,23 +505,23 @@ class RoutesDispatcher(object):
|
||||
params.pop('controller', None)
|
||||
params.pop('action', None)
|
||||
request.params.update(params)
|
||||
|
||||
|
||||
# Get config for the root object/path.
|
||||
request.config = base = cherrypy.config.copy()
|
||||
curpath = ""
|
||||
|
||||
|
||||
def merge(nodeconf):
|
||||
if 'tools.staticdir.dir' in nodeconf:
|
||||
nodeconf['tools.staticdir.section'] = curpath or "/"
|
||||
base.update(nodeconf)
|
||||
|
||||
|
||||
app = request.app
|
||||
root = app.root
|
||||
if hasattr(root, "_cp_config"):
|
||||
merge(root._cp_config)
|
||||
if "/" in app.config:
|
||||
merge(app.config["/"])
|
||||
|
||||
|
||||
# Mix in values from app.config.
|
||||
atoms = [x for x in path_info.split("/") if x]
|
||||
if atoms:
|
||||
@@ -532,7 +532,7 @@ class RoutesDispatcher(object):
|
||||
curpath = "/".join((curpath, atom))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
|
||||
handler = None
|
||||
if result:
|
||||
controller = result.get('controller')
|
||||
@@ -543,23 +543,23 @@ class RoutesDispatcher(object):
|
||||
# Get config from the controller.
|
||||
if hasattr(controller, "_cp_config"):
|
||||
merge(controller._cp_config)
|
||||
|
||||
|
||||
action = result.get('action')
|
||||
if action is not None:
|
||||
handler = getattr(controller, action, None)
|
||||
# Get config from the handler
|
||||
if hasattr(handler, "_cp_config"):
|
||||
# Get config from the handler
|
||||
if hasattr(handler, "_cp_config"):
|
||||
merge(handler._cp_config)
|
||||
else:
|
||||
handler = controller
|
||||
|
||||
|
||||
# Do the last path atom here so it can
|
||||
# override the controller's _cp_config.
|
||||
if last:
|
||||
curpath = "/".join((curpath, last))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
@@ -574,33 +574,33 @@ def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
||||
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
|
||||
"""
|
||||
Select a different handler based on the Host header.
|
||||
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different parts of a single
|
||||
website structure. For example::
|
||||
|
||||
|
||||
http://www.domain.example -> root
|
||||
http://www.domain2.example -> root/domain2/
|
||||
http://www.domain2.example:443 -> root/secure
|
||||
|
||||
|
||||
can be accomplished via the following config::
|
||||
|
||||
|
||||
[/]
|
||||
request.dispatch = cherrypy.dispatch.VirtualHost(
|
||||
**{'www.domain2.example': '/domain2',
|
||||
'www.domain2.example:443': '/secure',
|
||||
})
|
||||
|
||||
|
||||
next_dispatcher
|
||||
The next dispatcher object in the dispatch chain.
|
||||
The VirtualHost dispatcher adds a prefix to the URL and calls
|
||||
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
|
||||
|
||||
|
||||
use_x_forwarded_host
|
||||
If True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying.
|
||||
|
||||
|
||||
``**domains``
|
||||
A dict of {host header value: virtual prefix} pairs.
|
||||
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):
|
||||
request = cherrypy.serving.request
|
||||
header = request.headers.get
|
||||
|
||||
|
||||
domain = header('Host', '')
|
||||
if use_x_forwarded_host:
|
||||
domain = header("X-Forwarded-Host", domain)
|
||||
|
||||
|
||||
prefix = domains.get(domain, "")
|
||||
if prefix:
|
||||
path_info = httputil.urljoin(prefix, path_info)
|
||||
|
||||
|
||||
result = next_dispatcher(path_info)
|
||||
|
||||
|
||||
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
|
||||
section = request.config.get('tools.staticdir.section')
|
||||
if section:
|
||||
section = section[len(prefix):]
|
||||
request.config['tools.staticdir.section'] = section
|
||||
|
||||
|
||||
return result
|
||||
return vhost_dispatch
|
||||
|
||||
|
||||
+61
-61
@@ -123,37 +123,37 @@ class TimeoutError(CherryPyException):
|
||||
|
||||
class InternalRedirect(CherryPyException):
|
||||
"""Exception raised to switch to the handler for a different URL.
|
||||
|
||||
|
||||
This exception will redirect processing to another path within the site
|
||||
(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.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, path, query_string=""):
|
||||
import cherrypy
|
||||
self.request = cherrypy.serving.request
|
||||
|
||||
|
||||
self.query_string = query_string
|
||||
if "?" in path:
|
||||
# Separate any params included in the path
|
||||
path, self.query_string = path.split("?", 1)
|
||||
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a URL relative to root (e.g. "/dummy")
|
||||
# 2. a URL relative to the current path
|
||||
# Note that any query string will be discarded.
|
||||
path = _urljoin(self.request.path_info, path)
|
||||
|
||||
|
||||
# Set a 'path' member attribute so that code which traps this
|
||||
# error can have access to it.
|
||||
self.path = path
|
||||
|
||||
|
||||
CherryPyException.__init__(self, path, self.query_string)
|
||||
|
||||
|
||||
class HTTPRedirect(CherryPyException):
|
||||
"""Exception raised when the request should be redirected.
|
||||
|
||||
|
||||
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,
|
||||
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
|
||||
using the default encoding or the one passed in parameter.
|
||||
|
||||
|
||||
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
|
||||
303 (or 302 if responding with HTTP/1.0).
|
||||
|
||||
|
||||
Examples::
|
||||
|
||||
|
||||
raise cherrypy.HTTPRedirect("")
|
||||
raise cherrypy.HTTPRedirect("/abs/path", 307)
|
||||
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
|
||||
|
||||
|
||||
See :ref:`redirectingpost` for additional caveats.
|
||||
"""
|
||||
|
||||
|
||||
status = None
|
||||
"""The integer HTTP status code to emit."""
|
||||
|
||||
|
||||
urls = None
|
||||
"""The list of URL's to emit."""
|
||||
|
||||
encoding = 'utf-8'
|
||||
"""The encoding when passed urls are not native strings"""
|
||||
|
||||
|
||||
def __init__(self, urls, status=None, encoding=None):
|
||||
import cherrypy
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
if isinstance(urls, basestring):
|
||||
urls = [urls]
|
||||
|
||||
|
||||
abs_urls = []
|
||||
for url in urls:
|
||||
url = tonative(url, encoding or self.encoding)
|
||||
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a complete URL with host (e.g. "http://www.example.com/test")
|
||||
# 2. a URL relative to root (e.g. "/dummy")
|
||||
@@ -204,7 +204,7 @@ class HTTPRedirect(CherryPyException):
|
||||
url = _urljoin(cherrypy.url(), url)
|
||||
abs_urls.append(url)
|
||||
self.urls = abs_urls
|
||||
|
||||
|
||||
# RFC 2616 indicates a 301 response code fits our goal; however,
|
||||
# browser support for 301 is quite messy. Do 302/303 instead. See
|
||||
# http://www.alanflavell.org.uk/www/post-redirect.html
|
||||
@@ -217,26 +217,26 @@ class HTTPRedirect(CherryPyException):
|
||||
status = int(status)
|
||||
if status < 300 or status > 399:
|
||||
raise ValueError("status must be between 300 and 399.")
|
||||
|
||||
|
||||
self.status = status
|
||||
CherryPyException.__init__(self, abs_urls, status)
|
||||
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPRedirect object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
response.status = status = self.status
|
||||
|
||||
|
||||
if status in (300, 301, 302, 303, 307):
|
||||
response.headers['Content-Type'] = "text/html;charset=utf-8"
|
||||
# "The ... URI SHOULD be given by the Location field
|
||||
# in the response."
|
||||
response.headers['Location'] = self.urls[0]
|
||||
|
||||
|
||||
# "Unless the request method was HEAD, the entity of the response
|
||||
# SHOULD contain a short hypertext note with a hyperlink to the
|
||||
# new URI(s)."
|
||||
@@ -256,7 +256,7 @@ class HTTPRedirect(CherryPyException):
|
||||
# "The response MUST include the following header fields:
|
||||
# Date, unless its omission is required by section 14.18.1"
|
||||
# The "Date" header should have been set in Response.__init__
|
||||
|
||||
|
||||
# "...the response SHOULD NOT include other entity-headers."
|
||||
for key in ('Allow', 'Content-Encoding', 'Content-Language',
|
||||
'Content-Length', 'Content-Location', 'Content-MD5',
|
||||
@@ -264,7 +264,7 @@ class HTTPRedirect(CherryPyException):
|
||||
'Last-Modified'):
|
||||
if key in response.headers:
|
||||
del response.headers[key]
|
||||
|
||||
|
||||
# "The 304 response MUST NOT contain a message-body."
|
||||
response.body = None
|
||||
# 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)
|
||||
else:
|
||||
raise ValueError("The %s status code is unknown." % status)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
@@ -287,9 +287,9 @@ class HTTPRedirect(CherryPyException):
|
||||
def clean_headers(status):
|
||||
"""Remove any headers which should not apply to an error response."""
|
||||
import cherrypy
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# Remove headers which applied to the original content,
|
||||
# but do not apply to the error page.
|
||||
respheaders = response.headers
|
||||
@@ -298,7 +298,7 @@ def clean_headers(status):
|
||||
"Content-Location", "Content-MD5", "Last-Modified"]:
|
||||
if key in respheaders:
|
||||
del respheaders[key]
|
||||
|
||||
|
||||
if status != 416:
|
||||
# A server sending a response with status code 416 (Requested
|
||||
# range not satisfiable) SHOULD include a Content-Range field
|
||||
@@ -312,7 +312,7 @@ def clean_headers(status):
|
||||
|
||||
class HTTPError(CherryPyException):
|
||||
"""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
|
||||
code, with an appropriate error page. It takes an optional
|
||||
``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
|
||||
`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.
|
||||
|
||||
|
||||
Examples::
|
||||
|
||||
|
||||
raise cherrypy.HTTPError(403)
|
||||
raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
|
||||
"""
|
||||
|
||||
|
||||
status = None
|
||||
"""The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
|
||||
|
||||
|
||||
code = None
|
||||
"""The integer HTTP status code."""
|
||||
|
||||
|
||||
reason = None
|
||||
"""The HTTP Reason-Phrase string."""
|
||||
|
||||
|
||||
def __init__(self, status=500, message=None):
|
||||
self.status = status
|
||||
try:
|
||||
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
|
||||
except ValueError:
|
||||
raise self.__class__(500, _exc_info()[1].args[0])
|
||||
|
||||
|
||||
if self.code < 400 or self.code > 599:
|
||||
raise ValueError("status must be between 400 and 599.")
|
||||
|
||||
|
||||
# See http://www.python.org/dev/peps/pep-0352/
|
||||
# self.message = message
|
||||
self._message = message or defaultmsg
|
||||
CherryPyException.__init__(self, status, message)
|
||||
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPError object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
clean_headers(self.code)
|
||||
|
||||
|
||||
# In all cases, finalize will be called after this method,
|
||||
# so don't bother cleaning up response values here.
|
||||
response.status = self.status
|
||||
@@ -371,16 +371,16 @@ class HTTPError(CherryPyException):
|
||||
tb = format_exc()
|
||||
response.headers['Content-Type'] = "text/html;charset=utf-8"
|
||||
response.headers.pop('Content-Length', None)
|
||||
|
||||
|
||||
content = ntob(self.get_error_page(self.status, traceback=tb,
|
||||
message=self._message), 'utf-8')
|
||||
response.body = content
|
||||
|
||||
|
||||
_be_ie_unfriendly(self.code)
|
||||
|
||||
|
||||
def get_error_page(self, *args, **kwargs):
|
||||
return get_error_page(*args, **kwargs)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
@@ -388,11 +388,11 @@ class HTTPError(CherryPyException):
|
||||
|
||||
class NotFound(HTTPError):
|
||||
"""Exception raised when a URL could not be mapped to any handler (404).
|
||||
|
||||
|
||||
This is equivalent to raising
|
||||
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, path=None):
|
||||
if path is None:
|
||||
import cherrypy
|
||||
@@ -433,17 +433,17 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona
|
||||
|
||||
def get_error_page(status, **kwargs):
|
||||
"""Return an HTML page, containing a pretty error response.
|
||||
|
||||
|
||||
status should be an int or a str.
|
||||
kwargs will be interpolated into the page template.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
|
||||
try:
|
||||
code, reason, message = _httputil.valid_status(status)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
|
||||
|
||||
|
||||
# We can't use setdefault here, because some
|
||||
# callers send None for kwarg values.
|
||||
if kwargs.get('status') is None:
|
||||
@@ -454,13 +454,13 @@ def get_error_page(status, **kwargs):
|
||||
kwargs['traceback'] = ''
|
||||
if kwargs.get('version') is None:
|
||||
kwargs['version'] = cherrypy.__version__
|
||||
|
||||
|
||||
for k, v in iteritems(kwargs):
|
||||
if v is None:
|
||||
kwargs[k] = ""
|
||||
else:
|
||||
kwargs[k] = _escape(kwargs[k])
|
||||
|
||||
|
||||
# Use a custom template or callable for the error page?
|
||||
pages = cherrypy.serving.request.error_page
|
||||
error_page = pages.get(code) or pages.get('default')
|
||||
@@ -478,7 +478,7 @@ def get_error_page(status, **kwargs):
|
||||
m += "<br />"
|
||||
m += "In addition, the custom error page failed:\n<br />%s" % e
|
||||
kwargs['message'] = m
|
||||
|
||||
|
||||
return _HTTPErrorTemplate % kwargs
|
||||
|
||||
|
||||
@@ -492,7 +492,7 @@ _ie_friendly_error_sizes = {
|
||||
def _be_ie_unfriendly(status):
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# For some statuses, Internet Explorer 5+ shows "friendly error
|
||||
# messages" instead of our response.body if the body is smaller
|
||||
# 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):
|
||||
"""Produce status, headers, body for a critical error.
|
||||
|
||||
|
||||
Returns a triple without calling any other questionable functions,
|
||||
so it should be as error-free as possible. Call it from an HTTP server
|
||||
if you get errors outside of the request.
|
||||
|
||||
|
||||
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
|
||||
as-is to the body.
|
||||
"""
|
||||
|
||||
|
||||
# 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;
|
||||
# it cannot be allowed to fail. Therefore, don't add to it!
|
||||
# In particular, don't call any other CP functions.
|
||||
|
||||
|
||||
body = ntob("Unrecoverable error in the server.")
|
||||
if extrabody is not None:
|
||||
if not isinstance(extrabody, bytestr):
|
||||
extrabody = extrabody.encode('utf-8')
|
||||
body += ntob("\n") + extrabody
|
||||
|
||||
|
||||
return (ntob("500 Internal Server Error"),
|
||||
[(ntob('Content-Type'), ntob('text/plain')),
|
||||
(ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))],
|
||||
|
||||
+49
-49
@@ -69,21 +69,21 @@ and uses a RotatingFileHandler instead:
|
||||
|
||||
#python
|
||||
log = app.log
|
||||
|
||||
|
||||
# Remove the default FileHandlers if present.
|
||||
log.error_file = ""
|
||||
log.access_file = ""
|
||||
|
||||
|
||||
maxBytes = getattr(log, "rot_maxBytes", 10000000)
|
||||
backupCount = getattr(log, "rot_backupCount", 1000)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the error log.
|
||||
fname = getattr(log, "rot_error_file", "error.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
h.setLevel(DEBUG)
|
||||
h.setFormatter(_cplogging.logfmt)
|
||||
log.error_log.addHandler(h)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the access log.
|
||||
fname = getattr(log, "rot_access_file", "access.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
@@ -127,38 +127,38 @@ class NullHandler(logging.Handler):
|
||||
|
||||
class LogManager(object):
|
||||
"""An object to assist both simple and advanced logging.
|
||||
|
||||
|
||||
``cherrypy.log`` is an instance of this class.
|
||||
"""
|
||||
|
||||
|
||||
appid = None
|
||||
"""The id() of the Application object which owns this log manager. If this
|
||||
is a global log manager, appid is None."""
|
||||
|
||||
|
||||
error_log = None
|
||||
"""The actual :class:`logging.Logger` instance for error messages."""
|
||||
|
||||
|
||||
access_log = None
|
||||
"""The actual :class:`logging.Logger` instance for access messages."""
|
||||
|
||||
|
||||
if py3k:
|
||||
access_log_format = \
|
||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
||||
else:
|
||||
access_log_format = \
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
|
||||
logger_root = None
|
||||
"""The "top-level" logger name.
|
||||
|
||||
|
||||
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
|
||||
will be of the form::
|
||||
|
||||
|
||||
cherrypy.error.<appid>
|
||||
cherrypy.access.<appid>
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, appid=None, logger_root="cherrypy"):
|
||||
self.logger_root = logger_root
|
||||
self.appid = appid
|
||||
@@ -186,34 +186,34 @@ class LogManager(object):
|
||||
h.stream.close()
|
||||
h.stream = open(h.baseFilename, h.mode)
|
||||
h.release()
|
||||
|
||||
|
||||
def error(self, msg='', context='', severity=logging.INFO, traceback=False):
|
||||
"""Write the given ``msg`` to the error log.
|
||||
|
||||
|
||||
This is not just for errors! Applications may call this at any time
|
||||
to log application-specific information.
|
||||
|
||||
|
||||
If ``traceback`` is True, the traceback of the current exception
|
||||
(if any) will be appended to ``msg``.
|
||||
"""
|
||||
if traceback:
|
||||
msg += _cperror.format_exc()
|
||||
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""An alias for ``error``."""
|
||||
return self.error(*args, **kwargs)
|
||||
|
||||
|
||||
def access(self):
|
||||
"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
|
||||
|
||||
See http://httpd.apache.org/docs/2.0/logs.html#combined for format
|
||||
details.
|
||||
|
||||
|
||||
CherryPy calls this automatically for you. Note there are no arguments;
|
||||
it collects the data itself from
|
||||
:class:`cherrypy.request<cherrypy._cprequest.Request>`.
|
||||
|
||||
|
||||
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
|
||||
\\xhh sequences, where hh stands for the hexadecimal representation
|
||||
@@ -232,7 +232,7 @@ class LogManager(object):
|
||||
status = response.output_status.split(ntob(" "), 1)[0]
|
||||
if py3k:
|
||||
status = status.decode('ISO-8859-1')
|
||||
|
||||
|
||||
atoms = {'h': remote.name or remote.ip,
|
||||
'l': '-',
|
||||
'u': getattr(request, "login", None) or "-",
|
||||
@@ -251,15 +251,15 @@ class LogManager(object):
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
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
|
||||
# resulting in quadruple slashes. Remove the extra one here.
|
||||
v = v.replace('\\\\', '\\')
|
||||
|
||||
|
||||
# Escape double-quote.
|
||||
atoms[k] = v
|
||||
|
||||
|
||||
try:
|
||||
self.access_log.log(logging.INFO, self.access_log_format.format(**atoms))
|
||||
except:
|
||||
@@ -275,12 +275,12 @@ class LogManager(object):
|
||||
v = repr(v)[1:-1]
|
||||
# Escape double-quote.
|
||||
atoms[k] = v.replace('"', '\\"')
|
||||
|
||||
|
||||
try:
|
||||
self.access_log.log(logging.INFO, self.access_log_format % atoms)
|
||||
except:
|
||||
self(traceback=True)
|
||||
|
||||
|
||||
def time(self):
|
||||
"""Return now() in Apache Common Log Format (no timezone)."""
|
||||
now = datetime.datetime.now()
|
||||
@@ -289,15 +289,15 @@ class LogManager(object):
|
||||
month = monthnames[now.month - 1].capitalize()
|
||||
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
|
||||
(now.day, month, now.year, now.hour, now.minute, now.second))
|
||||
|
||||
|
||||
def _get_builtin_handler(self, log, key):
|
||||
for h in log.handlers:
|
||||
if getattr(h, "_cpbuiltin", None) == key:
|
||||
return h
|
||||
|
||||
|
||||
|
||||
|
||||
# ------------------------- Screen handlers ------------------------- #
|
||||
|
||||
|
||||
def _set_screen_handler(self, log, enable, stream=None):
|
||||
h = self._get_builtin_handler(log, "screen")
|
||||
if enable:
|
||||
@@ -310,30 +310,30 @@ class LogManager(object):
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_screen(self):
|
||||
h = self._get_builtin_handler
|
||||
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
|
||||
return bool(has_h)
|
||||
|
||||
|
||||
def _set_screen(self, newvalue):
|
||||
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
|
||||
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
|
||||
screen = property(_get_screen, _set_screen,
|
||||
doc="""Turn stderr/stdout logging on or off.
|
||||
|
||||
|
||||
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.
|
||||
""")
|
||||
|
||||
|
||||
# -------------------------- File handlers -------------------------- #
|
||||
|
||||
|
||||
def _add_builtin_file_handler(self, log, fname):
|
||||
h = logging.FileHandler(fname)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "file"
|
||||
log.addHandler(h)
|
||||
|
||||
|
||||
def _set_file_handler(self, log, filename):
|
||||
h = self._get_builtin_handler(log, "file")
|
||||
if filename:
|
||||
@@ -348,7 +348,7 @@ class LogManager(object):
|
||||
if h:
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_error_file(self):
|
||||
h = self._get_builtin_handler(self.error_log, "file")
|
||||
if h:
|
||||
@@ -358,11 +358,11 @@ class LogManager(object):
|
||||
self._set_file_handler(self.error_log, newvalue)
|
||||
error_file = property(_get_error_file, _set_error_file,
|
||||
doc="""The filename for self.error_log.
|
||||
|
||||
|
||||
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.
|
||||
""")
|
||||
|
||||
|
||||
def _get_access_file(self):
|
||||
h = self._get_builtin_handler(self.access_log, "file")
|
||||
if h:
|
||||
@@ -372,13 +372,13 @@ class LogManager(object):
|
||||
self._set_file_handler(self.access_log, newvalue)
|
||||
access_file = property(_get_access_file, _set_access_file,
|
||||
doc="""The filename for self.access_log.
|
||||
|
||||
|
||||
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.
|
||||
""")
|
||||
|
||||
|
||||
# ------------------------- WSGI handlers ------------------------- #
|
||||
|
||||
|
||||
def _set_wsgi_handler(self, log, enable):
|
||||
h = self._get_builtin_handler(log, "wsgi")
|
||||
if enable:
|
||||
@@ -389,15 +389,15 @@ class LogManager(object):
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_wsgi(self):
|
||||
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
|
||||
|
||||
|
||||
def _set_wsgi(self, newvalue):
|
||||
self._set_wsgi_handler(self.error_log, newvalue)
|
||||
wsgi = property(_get_wsgi, _set_wsgi,
|
||||
doc="""Write errors to wsgi.errors.
|
||||
|
||||
|
||||
If you set this to True, it'll add the appropriate
|
||||
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
|
||||
(which writes errors to ``wsgi.errors``).
|
||||
@@ -407,7 +407,7 @@ class LogManager(object):
|
||||
|
||||
class WSGIErrorHandler(logging.Handler):
|
||||
"A handler class which writes logging records to environ['wsgi.errors']."
|
||||
|
||||
|
||||
def flush(self):
|
||||
"""Flushes the stream."""
|
||||
try:
|
||||
@@ -416,7 +416,7 @@ class WSGIErrorHandler(logging.Handler):
|
||||
pass
|
||||
else:
|
||||
stream.flush()
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a record."""
|
||||
try:
|
||||
|
||||
+28
-28
@@ -35,12 +35,12 @@ Listen 8080
|
||||
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
|
||||
|
||||
<Location "/">
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
SetHandler python-program
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup myapp::setup_server
|
||||
PythonDebug On
|
||||
</Location>
|
||||
</Location>
|
||||
# End
|
||||
|
||||
The actual path to your mod_python.so is dependent on your
|
||||
@@ -70,7 +70,7 @@ from cherrypy.lib import httputil
|
||||
|
||||
def setup(req):
|
||||
from mod_python import apache
|
||||
|
||||
|
||||
# Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
|
||||
options = req.get_options()
|
||||
if 'cherrypy.setup' in options:
|
||||
@@ -83,12 +83,12 @@ def setup(req):
|
||||
mod = __import__(modname, globals(), locals(), [fname])
|
||||
func = getattr(mod, fname)
|
||||
func()
|
||||
|
||||
|
||||
cherrypy.config.update({'log.screen': False,
|
||||
"tools.ignore_headers.on": True,
|
||||
"tools.ignore_headers.headers": ['Range'],
|
||||
})
|
||||
|
||||
|
||||
engine = cherrypy.engine
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.unsubscribe()
|
||||
@@ -96,7 +96,7 @@ def setup(req):
|
||||
engine.console_control_handler.unsubscribe()
|
||||
engine.autoreload.unsubscribe()
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
|
||||
def _log(msg, level):
|
||||
newlevel = apache.APLOG_ERR
|
||||
if logging.DEBUG >= level:
|
||||
@@ -110,9 +110,9 @@ def setup(req):
|
||||
# Also, "When server is not specified...LogLevel does not apply..."
|
||||
apache.log_error(msg, newlevel, req.server)
|
||||
engine.subscribe('log', _log)
|
||||
|
||||
|
||||
engine.start()
|
||||
|
||||
|
||||
def cherrypy_cleanup(data):
|
||||
engine.exit()
|
||||
try:
|
||||
@@ -139,16 +139,16 @@ def handler(req):
|
||||
if not _isSetUp:
|
||||
setup(req)
|
||||
_isSetUp = True
|
||||
|
||||
|
||||
# Obtain a Request object from CherryPy
|
||||
local = req.connection.local_addr
|
||||
local = httputil.Host(local[0], local[1], req.connection.local_host or "")
|
||||
remote = req.connection.remote_addr
|
||||
remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
|
||||
|
||||
|
||||
scheme = req.parsed_uri[0] or 'http'
|
||||
req.get_basic_auth_pw()
|
||||
|
||||
|
||||
try:
|
||||
# apache.mpm_query only became available in mod_python 3.1
|
||||
q = apache.mpm_query
|
||||
@@ -158,7 +158,7 @@ def handler(req):
|
||||
bad_value = ("You must provide a PythonOption '%s', "
|
||||
"either 'on' or 'off', when running a version "
|
||||
"of mod_python < 3.1")
|
||||
|
||||
|
||||
threaded = options.get('multithread', '').lower()
|
||||
if threaded == 'on':
|
||||
threaded = True
|
||||
@@ -166,7 +166,7 @@ def handler(req):
|
||||
threaded = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multithread")
|
||||
|
||||
|
||||
forked = options.get('multiprocess', '').lower()
|
||||
if forked == 'on':
|
||||
forked = True
|
||||
@@ -174,7 +174,7 @@ def handler(req):
|
||||
forked = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multiprocess")
|
||||
|
||||
|
||||
sn = cherrypy.tree.script_name(req.uri or "/")
|
||||
if sn is None:
|
||||
send_response(req, '404 Not Found', [], '')
|
||||
@@ -187,7 +187,7 @@ def handler(req):
|
||||
headers = copyitems(req.headers_in)
|
||||
rfile = _ReadOnlyRequest(req)
|
||||
prev = None
|
||||
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
@@ -198,7 +198,7 @@ def handler(req):
|
||||
request.multiprocess = bool(forked)
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
try:
|
||||
request.run(method, path, qs, reqproto, headers, rfile)
|
||||
@@ -207,7 +207,7 @@ def handler(req):
|
||||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
|
||||
if not recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
@@ -217,13 +217,13 @@ def handler(req):
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
|
||||
# Munge environment and try again.
|
||||
method = "GET"
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = BytesIO()
|
||||
|
||||
|
||||
send_response(req, response.output_status, response.header_list,
|
||||
response.body, response.stream)
|
||||
finally:
|
||||
@@ -239,7 +239,7 @@ def handler(req):
|
||||
def send_response(req, status, headers, body, stream=False):
|
||||
# Set response status
|
||||
req.status = int(status[:3])
|
||||
|
||||
|
||||
# Set response headers
|
||||
req.content_type = "text/plain"
|
||||
for header, value in headers:
|
||||
@@ -247,11 +247,11 @@ def send_response(req, status, headers, body, stream=False):
|
||||
req.content_type = value
|
||||
continue
|
||||
req.headers_out.add(header, value)
|
||||
|
||||
|
||||
if stream:
|
||||
# Flush now so the status and headers are sent immediately.
|
||||
req.flush()
|
||||
|
||||
|
||||
# Set response body
|
||||
if isinstance(body, basestring):
|
||||
req.write(body)
|
||||
@@ -294,7 +294,7 @@ def read_process(cmd, args=""):
|
||||
|
||||
|
||||
class ModPythonServer(object):
|
||||
|
||||
|
||||
template = """
|
||||
# Apache2 server configuration file for running CherryPy with mod_python.
|
||||
|
||||
@@ -309,7 +309,7 @@ LoadModule python_module modules/mod_python.so
|
||||
%(opts)s
|
||||
</Location>
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
|
||||
handler="cherrypy._cpmodpy::handler"):
|
||||
self.loc = loc
|
||||
@@ -317,7 +317,7 @@ LoadModule python_module modules/mod_python.so
|
||||
self.opts = opts
|
||||
self.apache_path = apache_path
|
||||
self.handler = handler
|
||||
|
||||
|
||||
def start(self):
|
||||
opts = "".join([" PythonOption %s %s\n" % (k, v)
|
||||
for k, v in self.opts])
|
||||
@@ -326,18 +326,18 @@ LoadModule python_module modules/mod_python.so
|
||||
"opts": opts,
|
||||
"handler": self.handler,
|
||||
}
|
||||
|
||||
|
||||
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
f.write(conf_data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
|
||||
self.ready = True
|
||||
return response
|
||||
|
||||
|
||||
def stop(self):
|
||||
os.popen("apache -k stop")
|
||||
self.ready = False
|
||||
|
||||
@@ -11,9 +11,9 @@ from cherrypy import wsgiserver
|
||||
|
||||
|
||||
class NativeGateway(wsgiserver.Gateway):
|
||||
|
||||
|
||||
recursive = False
|
||||
|
||||
|
||||
def respond(self):
|
||||
req = self.req
|
||||
try:
|
||||
@@ -22,7 +22,7 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
local = httputil.Host(local[0], local[1], "")
|
||||
remote = req.conn.remote_addr, req.conn.remote_port
|
||||
remote = httputil.Host(remote[0], remote[1], "")
|
||||
|
||||
|
||||
scheme = req.scheme
|
||||
sn = cherrypy.tree.script_name(req.uri or "/")
|
||||
if sn is None:
|
||||
@@ -35,7 +35,7 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
headers = req.inheaders.items()
|
||||
rfile = req.rfile
|
||||
prev = None
|
||||
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
@@ -45,7 +45,7 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
request.multiprocess = False
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
try:
|
||||
request.run(method, path, qs, req.request_protocol, headers, rfile)
|
||||
@@ -54,7 +54,7 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
|
||||
if not self.recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
@@ -64,13 +64,13 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
|
||||
# Munge environment and try again.
|
||||
method = "GET"
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = BytesIO()
|
||||
|
||||
|
||||
self.send_response(
|
||||
response.output_status, response.header_list,
|
||||
response.body)
|
||||
@@ -82,20 +82,20 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
|
||||
s, h, b = bare_error()
|
||||
self.send_response(s, h, b)
|
||||
|
||||
|
||||
def send_response(self, status, headers, body):
|
||||
req = self.req
|
||||
|
||||
|
||||
# Set response status
|
||||
req.status = str(status or "500 Server Error")
|
||||
|
||||
|
||||
# Set response headers
|
||||
for header, value in headers:
|
||||
req.outheaders.append((header, value))
|
||||
if (req.ready and not req.sent_headers):
|
||||
req.sent_headers = True
|
||||
req.send_headers()
|
||||
|
||||
|
||||
# Set response body
|
||||
for seg in body:
|
||||
req.write(seg)
|
||||
@@ -103,26 +103,26 @@ class NativeGateway(wsgiserver.Gateway):
|
||||
|
||||
class CPHTTPServer(wsgiserver.HTTPServer):
|
||||
"""Wrapper for wsgiserver.HTTPServer.
|
||||
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
so that it can be used in other frameworks and applications.
|
||||
Therefore, we wrap it here, so we can apply some attributes
|
||||
from config -> cherrypy.server -> HTTPServer.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
|
||||
wsgiserver.HTTPServer.__init__(
|
||||
self, server_adapter.bind_addr, NativeGateway,
|
||||
minthreads=server_adapter.thread_pool,
|
||||
maxthreads=server_adapter.thread_pool_max,
|
||||
server_name=server_name)
|
||||
|
||||
|
||||
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.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.protocol = self.server_adapter.protocol_version
|
||||
self.nodelay = self.server_adapter.nodelay
|
||||
|
||||
|
||||
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
|
||||
if self.server_adapter.ssl_context:
|
||||
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
|
||||
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::
|
||||
|
||||
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.\"""
|
||||
if not entity.headers.get("Content-Length", ""):
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
|
||||
body = entity.fp.read()
|
||||
try:
|
||||
request.json = json_decode(body)
|
||||
@@ -141,14 +141,14 @@ def process_urlencoded(entity):
|
||||
for pair in aparam.split(ntob(';')):
|
||||
if not pair:
|
||||
continue
|
||||
|
||||
|
||||
atoms = pair.split(ntob('='), 1)
|
||||
if len(atoms) == 1:
|
||||
atoms.append(ntob(''))
|
||||
|
||||
|
||||
key = unquote_plus(atoms[0]).decode(charset)
|
||||
value = unquote_plus(atoms[1]).decode(charset)
|
||||
|
||||
|
||||
if key in params:
|
||||
if not isinstance(params[key], list):
|
||||
params[key] = [params[key]]
|
||||
@@ -164,7 +164,7 @@ def process_urlencoded(entity):
|
||||
raise cherrypy.HTTPError(
|
||||
400, "The request entity could not be decoded. The following "
|
||||
"charsets were attempted: %s" % repr(entity.attempt_charsets))
|
||||
|
||||
|
||||
# Now that all values have been successfully parsed and decoded,
|
||||
# apply them to the entity.params dict.
|
||||
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
|
||||
# on the Content-type line"
|
||||
ib = entity.content_type.params['boundary'].strip('"')
|
||||
|
||||
|
||||
if not re.match("^[ -~]{0,200}[!-~]$", ib):
|
||||
raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
|
||||
|
||||
|
||||
ib = ('--' + ib).encode('ascii')
|
||||
|
||||
|
||||
# Find the first marker
|
||||
while True:
|
||||
b = entity.readline()
|
||||
if not b:
|
||||
return
|
||||
|
||||
|
||||
b = b.strip()
|
||||
if b == ib:
|
||||
break
|
||||
|
||||
|
||||
# Read all parts
|
||||
while True:
|
||||
part = entity.part_class.from_fp(entity.fp, ib)
|
||||
@@ -212,7 +212,7 @@ def process_multipart(entity):
|
||||
def process_multipart_form_data(entity):
|
||||
"""Read all multipart/form-data parts into entity.parts or entity.params."""
|
||||
process_multipart(entity)
|
||||
|
||||
|
||||
kept_parts = []
|
||||
for part in entity.parts:
|
||||
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
|
||||
# has access to its .file and .filename attributes.
|
||||
value = part
|
||||
|
||||
|
||||
if part.name in entity.params:
|
||||
if not isinstance(entity.params[part.name], list):
|
||||
entity.params[part.name] = [entity.params[part.name]]
|
||||
entity.params[part.name].append(value)
|
||||
else:
|
||||
entity.params[part.name] = value
|
||||
|
||||
|
||||
entity.parts = kept_parts
|
||||
|
||||
def _old_process_multipart(entity):
|
||||
"""The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
|
||||
process_multipart(entity)
|
||||
|
||||
|
||||
params = entity.params
|
||||
|
||||
|
||||
for part in entity.parts:
|
||||
if part.name is None:
|
||||
key = ntou('parts')
|
||||
else:
|
||||
key = part.name
|
||||
|
||||
|
||||
if part.filename is None:
|
||||
# It's a regular field
|
||||
value = part.fullvalue()
|
||||
@@ -254,7 +254,7 @@ def _old_process_multipart(entity):
|
||||
# It's a file upload. Retain the whole part so consumer code
|
||||
# has access to its .file and .filename attributes.
|
||||
value = part
|
||||
|
||||
|
||||
if key in params:
|
||||
if not isinstance(params[key], list):
|
||||
params[key] = [params[key]]
|
||||
@@ -269,12 +269,12 @@ def _old_process_multipart(entity):
|
||||
|
||||
class Entity(object):
|
||||
"""An HTTP request body, or MIME multipart body.
|
||||
|
||||
|
||||
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
|
||||
Entity instance, and the set of parts stored in
|
||||
:attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
|
||||
|
||||
|
||||
Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
|
||||
tries to process the request body (if any) by calling
|
||||
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
|
||||
@@ -287,7 +287,7 @@ class Entity(object):
|
||||
processor is still not found, then 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).
|
||||
|
||||
|
||||
CherryPy includes processors for the "application/x-www-form-urlencoded"
|
||||
type, the "multipart/form-data" type, and the "multipart" major type.
|
||||
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
|
||||
``value`` attribute). Each Part is itself a subclass of
|
||||
Entity, and has its own ``process`` method and ``processors`` dict.
|
||||
|
||||
|
||||
There is a separate processor for the "multipart" major type which is more
|
||||
flexible, and simply stores all multipart parts in
|
||||
:attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
|
||||
enable it with::
|
||||
|
||||
|
||||
cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
|
||||
|
||||
|
||||
in an ``on_start_resource`` tool.
|
||||
"""
|
||||
|
||||
|
||||
# http://tools.ietf.org/html/rfc2046#section-4.1.2:
|
||||
# "The default character set, which must be assumed in the
|
||||
# absence of a charset parameter, is US-ASCII."
|
||||
# However, many browsers send data in utf-8 with no charset.
|
||||
attempt_charsets = ['utf-8']
|
||||
"""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
|
||||
encodings will be tried in order. The first one to successfully decode the
|
||||
entity without raising an error is stored as
|
||||
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
|
||||
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>`_),
|
||||
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>`_),
|
||||
but ``['us-ascii', 'utf-8']`` for multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
charset = None
|
||||
"""The successful decoding; see "attempt_charsets" above."""
|
||||
|
||||
|
||||
content_type = None
|
||||
"""The value of the Content-Type request header.
|
||||
|
||||
|
||||
If the Entity is part of a multipart payload, this will be the Content-Type
|
||||
given in the MIME headers for this part.
|
||||
"""
|
||||
|
||||
|
||||
default_content_type = 'application/x-www-form-urlencoded'
|
||||
"""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
|
||||
@@ -344,26 +344,26 @@ class Entity(object):
|
||||
declares that a part with no Content-Type defaults to "text/plain"
|
||||
(see :class:`Part<cherrypy._cpreqbody.Part>`).
|
||||
"""
|
||||
|
||||
|
||||
filename = None
|
||||
"""The ``Content-Disposition.filename`` header, if available."""
|
||||
|
||||
|
||||
fp = None
|
||||
"""The readable socket file object."""
|
||||
|
||||
|
||||
headers = None
|
||||
"""A dict of request/multipart header names and values.
|
||||
|
||||
|
||||
This is a copy of the ``request.headers`` for the ``request.body``;
|
||||
for multipart parts, it is the set of headers for that part.
|
||||
"""
|
||||
|
||||
|
||||
length = None
|
||||
"""The value of the ``Content-Length`` header, if provided."""
|
||||
|
||||
|
||||
name = None
|
||||
"""The "name" parameter of the ``Content-Disposition`` header, if any."""
|
||||
|
||||
|
||||
params = None
|
||||
"""
|
||||
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
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True)."""
|
||||
|
||||
|
||||
processors = {'application/x-www-form-urlencoded': process_urlencoded,
|
||||
'multipart/form-data': process_multipart_form_data,
|
||||
'multipart': process_multipart,
|
||||
}
|
||||
"""A dict of Content-Type names to processor methods."""
|
||||
|
||||
|
||||
parts = None
|
||||
"""A list of Part instances if ``Content-Type`` is of major type "multipart"."""
|
||||
|
||||
|
||||
part_class = None
|
||||
"""The class used for multipart parts.
|
||||
|
||||
|
||||
You can replace this with custom subclasses to alter the processing of
|
||||
multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, fp, headers, params=None, parts=None):
|
||||
# Make an instance-specific copy of the class processors
|
||||
# so Tools, etc. can replace them per-request.
|
||||
self.processors = self.processors.copy()
|
||||
|
||||
|
||||
self.fp = fp
|
||||
self.headers = headers
|
||||
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
|
||||
if parts is None:
|
||||
parts = []
|
||||
self.parts = parts
|
||||
|
||||
|
||||
# Content-Type
|
||||
self.content_type = headers.elements('Content-Type')
|
||||
if self.content_type:
|
||||
@@ -413,7 +413,7 @@ class Entity(object):
|
||||
else:
|
||||
self.content_type = httputil.HeaderElement.from_str(
|
||||
self.default_content_type)
|
||||
|
||||
|
||||
# Copy the class 'attempt_charsets', prepending any Content-Type charset
|
||||
dec = self.content_type.params.get("charset", None)
|
||||
if dec:
|
||||
@@ -421,7 +421,7 @@ class Entity(object):
|
||||
if c != dec]
|
||||
else:
|
||||
self.attempt_charsets = self.attempt_charsets[:]
|
||||
|
||||
|
||||
# Length
|
||||
self.length = None
|
||||
clen = headers.get('Content-Length', None)
|
||||
@@ -431,7 +431,7 @@ class Entity(object):
|
||||
self.length = int(clen)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# Content-Disposition
|
||||
self.name = None
|
||||
self.filename = None
|
||||
@@ -446,23 +446,23 @@ class Entity(object):
|
||||
self.filename = disp.params['filename']
|
||||
if self.filename.startswith('"') and self.filename.endswith('"'):
|
||||
self.filename = self.filename[1:-1]
|
||||
|
||||
|
||||
# The 'type' attribute is deprecated in 3.2; remove it in 3.3.
|
||||
type = property(lambda self: self.content_type,
|
||||
doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
|
||||
|
||||
|
||||
def read(self, size=None, fp_out=None):
|
||||
return self.fp.read(size, fp_out)
|
||||
|
||||
|
||||
def readline(self, size=None):
|
||||
return self.fp.readline(size)
|
||||
|
||||
|
||||
def readlines(self, sizehint=None):
|
||||
return self.fp.readlines(sizehint)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __next__(self):
|
||||
line = self.readline()
|
||||
if not line:
|
||||
@@ -471,21 +471,21 @@ class Entity(object):
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
||||
|
||||
def read_into_file(self, fp_out=None):
|
||||
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
|
||||
if fp_out is None:
|
||||
fp_out = self.make_file()
|
||||
self.read(fp_out=fp_out)
|
||||
return fp_out
|
||||
|
||||
|
||||
def make_file(self):
|
||||
"""Return a file-like object into which the request body will be read.
|
||||
|
||||
|
||||
By default, this will return a TemporaryFile. Override as needed.
|
||||
See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
|
||||
return tempfile.TemporaryFile()
|
||||
|
||||
|
||||
def fullvalue(self):
|
||||
"""Return this entity as a string, whether stored in a file or not."""
|
||||
if self.file:
|
||||
@@ -496,7 +496,7 @@ class Entity(object):
|
||||
else:
|
||||
value = self.value
|
||||
return value
|
||||
|
||||
|
||||
def process(self):
|
||||
"""Execute the best-match processor for the given media type."""
|
||||
proc = None
|
||||
@@ -513,7 +513,7 @@ class Entity(object):
|
||||
self.default_proc()
|
||||
else:
|
||||
proc(self)
|
||||
|
||||
|
||||
def default_proc(self):
|
||||
"""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
|
||||
@@ -524,24 +524,24 @@ class Entity(object):
|
||||
|
||||
class Part(Entity):
|
||||
"""A MIME part entity, part of a multipart entity."""
|
||||
|
||||
|
||||
# "The default character set, which must be assumed in the absence of a
|
||||
# charset parameter, is US-ASCII."
|
||||
attempt_charsets = ['us-ascii', 'utf-8']
|
||||
"""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
|
||||
encodings will be tried in order. The first one to successfully decode the
|
||||
entity without raising an error is stored as
|
||||
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
|
||||
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>`_),
|
||||
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>`_),
|
||||
but ``['us-ascii', 'utf-8']`` for multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
boundary = None
|
||||
"""The MIME multipart boundary."""
|
||||
|
||||
|
||||
default_content_type = 'text/plain'
|
||||
"""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
|
||||
@@ -551,7 +551,7 @@ class Part(Entity):
|
||||
the MIME spec declares that a part with no Content-Type defaults to
|
||||
"text/plain".
|
||||
"""
|
||||
|
||||
|
||||
# This is the default in stdlib cgi. We may want to increase it.
|
||||
maxrambytes = 1000
|
||||
"""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
|
||||
Python's standard library.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, fp, headers, boundary):
|
||||
Entity.__init__(self, fp, headers)
|
||||
self.boundary = boundary
|
||||
self.file = None
|
||||
self.value = None
|
||||
|
||||
|
||||
def from_fp(cls, fp, boundary):
|
||||
headers = cls.read_headers(fp)
|
||||
return cls(fp, headers, boundary)
|
||||
from_fp = classmethod(from_fp)
|
||||
|
||||
|
||||
def read_headers(cls, fp):
|
||||
headers = httputil.HeaderMap()
|
||||
while True:
|
||||
@@ -578,13 +578,13 @@ class Part(Entity):
|
||||
if not line:
|
||||
# No more data--illegal end of headers
|
||||
raise EOFError("Illegal end of headers.")
|
||||
|
||||
|
||||
if line == ntob('\r\n'):
|
||||
# Normal end of headers
|
||||
break
|
||||
if not line.endswith(ntob('\r\n')):
|
||||
raise ValueError("MIME requires CRLF terminators: %r" % line)
|
||||
|
||||
|
||||
if line[0] in ntob(' \t'):
|
||||
# It's a continuation line.
|
||||
v = line.strip().decode('ISO-8859-1')
|
||||
@@ -592,21 +592,21 @@ class Part(Entity):
|
||||
k, v = line.split(ntob(":"), 1)
|
||||
k = k.strip().decode('ISO-8859-1')
|
||||
v = v.strip().decode('ISO-8859-1')
|
||||
|
||||
|
||||
existing = headers.get(k)
|
||||
if existing:
|
||||
v = ", ".join((existing, v))
|
||||
headers[k] = v
|
||||
|
||||
|
||||
return headers
|
||||
read_headers = classmethod(read_headers)
|
||||
|
||||
|
||||
def read_lines_to_boundary(self, fp_out=None):
|
||||
"""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
|
||||
returned in a single byte string.
|
||||
|
||||
|
||||
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,
|
||||
and that fp is returned.
|
||||
@@ -627,9 +627,9 @@ class Part(Entity):
|
||||
if strippedline == endmarker:
|
||||
self.fp.finish()
|
||||
break
|
||||
|
||||
|
||||
line = delim + line
|
||||
|
||||
|
||||
if line.endswith(ntob("\r\n")):
|
||||
delim = ntob("\r\n")
|
||||
line = line[:-2]
|
||||
@@ -641,7 +641,7 @@ class Part(Entity):
|
||||
else:
|
||||
delim = ntob("")
|
||||
prev_lf = False
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
lines.append(line)
|
||||
seen += len(line)
|
||||
@@ -651,7 +651,7 @@ class Part(Entity):
|
||||
fp_out.write(line)
|
||||
else:
|
||||
fp_out.write(line)
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
result = ntob('').join(lines)
|
||||
for charset in self.attempt_charsets:
|
||||
@@ -669,7 +669,7 @@ class Part(Entity):
|
||||
else:
|
||||
fp_out.seek(0)
|
||||
return fp_out
|
||||
|
||||
|
||||
def default_proc(self):
|
||||
"""Called if a more-specific processor is not found for the ``Content-Type``."""
|
||||
if self.filename:
|
||||
@@ -681,7 +681,7 @@ class Part(Entity):
|
||||
self.value = result
|
||||
else:
|
||||
self.file = result
|
||||
|
||||
|
||||
def read_into_file(self, fp_out=None):
|
||||
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
|
||||
if fp_out is None:
|
||||
@@ -711,7 +711,7 @@ comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
|
||||
|
||||
|
||||
class SizedReader:
|
||||
|
||||
|
||||
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False):
|
||||
# Wrap our fp in a buffer so peek() works
|
||||
self.fp = fp
|
||||
@@ -722,25 +722,25 @@ class SizedReader:
|
||||
self.bytes_read = 0
|
||||
self.done = False
|
||||
self.has_trailers = has_trailers
|
||||
|
||||
|
||||
def read(self, size=None, fp_out=None):
|
||||
"""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
|
||||
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
|
||||
client sends fewer bytes, 2) the 'Content-Length' request header
|
||||
specifies fewer bytes than requested, or 3) the number of bytes read
|
||||
exceeds self.maxbytes (in which case, 413 is raised).
|
||||
|
||||
|
||||
If the 'fp_out' argument is None (the default), all bytes read are
|
||||
returned in a single byte string.
|
||||
|
||||
|
||||
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,
|
||||
and None is returned.
|
||||
"""
|
||||
|
||||
|
||||
if self.length is None:
|
||||
if size is None:
|
||||
remaining = inf
|
||||
@@ -756,9 +756,9 @@ class SizedReader:
|
||||
return ntob('')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
chunks = []
|
||||
|
||||
|
||||
# Read bytes from the buffer.
|
||||
if self.buffer:
|
||||
if remaining is inf:
|
||||
@@ -769,18 +769,18 @@ class SizedReader:
|
||||
self.buffer = self.buffer[remaining:]
|
||||
datalen = len(data)
|
||||
remaining -= datalen
|
||||
|
||||
|
||||
# Check lengths.
|
||||
self.bytes_read += datalen
|
||||
if self.maxbytes and self.bytes_read > self.maxbytes:
|
||||
raise cherrypy.HTTPError(413)
|
||||
|
||||
|
||||
# Store the data.
|
||||
if fp_out is None:
|
||||
chunks.append(data)
|
||||
else:
|
||||
fp_out.write(data)
|
||||
|
||||
|
||||
# Read bytes from the socket.
|
||||
while remaining > 0:
|
||||
chunksize = min(remaining, self.bufsize)
|
||||
@@ -799,21 +799,21 @@ class SizedReader:
|
||||
break
|
||||
datalen = len(data)
|
||||
remaining -= datalen
|
||||
|
||||
|
||||
# Check lengths.
|
||||
self.bytes_read += datalen
|
||||
if self.maxbytes and self.bytes_read > self.maxbytes:
|
||||
raise cherrypy.HTTPError(413)
|
||||
|
||||
|
||||
# Store the data.
|
||||
if fp_out is None:
|
||||
chunks.append(data)
|
||||
else:
|
||||
fp_out.write(data)
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
return ntob('').join(chunks)
|
||||
|
||||
|
||||
def readline(self, size=None):
|
||||
"""Read a line from the request body and return it."""
|
||||
chunks = []
|
||||
@@ -834,7 +834,7 @@ class SizedReader:
|
||||
else:
|
||||
chunks.append(data)
|
||||
return ntob('').join(chunks)
|
||||
|
||||
|
||||
def readlines(self, sizehint=None):
|
||||
"""Read lines from the request body and return them."""
|
||||
if self.length is not None:
|
||||
@@ -842,7 +842,7 @@ class SizedReader:
|
||||
sizehint = self.length - self.bytes_read
|
||||
else:
|
||||
sizehint = min(sizehint, self.length - self.bytes_read)
|
||||
|
||||
|
||||
lines = []
|
||||
seen = 0
|
||||
while True:
|
||||
@@ -854,12 +854,12 @@ class SizedReader:
|
||||
if seen >= sizehint:
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def finish(self):
|
||||
self.done = True
|
||||
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
|
||||
self.trailers = {}
|
||||
|
||||
|
||||
try:
|
||||
for line in self.fp.read_trailer_lines():
|
||||
if line[0] in ntob(' \t'):
|
||||
@@ -872,7 +872,7 @@ class SizedReader:
|
||||
raise ValueError("Illegal header line.")
|
||||
k = k.strip().title()
|
||||
v = v.strip()
|
||||
|
||||
|
||||
if k in comma_separated_headers:
|
||||
existing = self.trailers.get(envname)
|
||||
if existing:
|
||||
@@ -890,10 +890,10 @@ class SizedReader:
|
||||
|
||||
class RequestBody(Entity):
|
||||
"""The entity of the HTTP request."""
|
||||
|
||||
|
||||
bufsize = 8 * 1024
|
||||
"""The buffer size used when reading the socket."""
|
||||
|
||||
|
||||
# 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
|
||||
default_content_type = ''
|
||||
@@ -905,13 +905,13 @@ class RequestBody(Entity):
|
||||
declares that a part with no Content-Type defaults to "text/plain"
|
||||
(see :class:`Part<cherrypy._cpreqbody.Part>`).
|
||||
"""
|
||||
|
||||
|
||||
maxbytes = None
|
||||
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
|
||||
|
||||
|
||||
def __init__(self, fp, headers, params=None, request_params=None):
|
||||
Entity.__init__(self, fp, headers, params)
|
||||
|
||||
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
|
||||
# When no explicit charset parameter is provided by the
|
||||
# sender, media subtypes of the "text" type are defined
|
||||
@@ -923,14 +923,14 @@ class RequestBody(Entity):
|
||||
break
|
||||
else:
|
||||
self.attempt_charsets.append('ISO-8859-1')
|
||||
|
||||
|
||||
# Temporary fix while deprecating passing .parts as .params.
|
||||
self.processors['multipart'] = _old_process_multipart
|
||||
|
||||
|
||||
if request_params is None:
|
||||
request_params = {}
|
||||
self.request_params = request_params
|
||||
|
||||
|
||||
def process(self):
|
||||
"""Process the request entity based on its Content-Type."""
|
||||
# "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
|
||||
if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
|
||||
self.fp = SizedReader(self.fp, self.length,
|
||||
self.maxbytes, bufsize=self.bufsize,
|
||||
has_trailers='Trailer' in h)
|
||||
super(RequestBody, self).process()
|
||||
|
||||
|
||||
# Body params should also be a part of the request_params
|
||||
# add them in here.
|
||||
request_params = self.request_params
|
||||
@@ -956,7 +956,7 @@ class RequestBody(Entity):
|
||||
if sys.version_info < (3, 0):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('ISO-8859-1')
|
||||
|
||||
|
||||
if key in request_params:
|
||||
if not isinstance(request_params[key], list):
|
||||
request_params[key] = [request_params[key]]
|
||||
|
||||
+146
-146
@@ -14,41 +14,41 @@ from cherrypy.lib import httputil, file_generator
|
||||
|
||||
class Hook(object):
|
||||
"""A callback and its metadata: failsafe, priority, and kwargs."""
|
||||
|
||||
|
||||
callback = None
|
||||
"""
|
||||
The bare callable that this Hook object is wrapping, which will
|
||||
be called when the Hook is called."""
|
||||
|
||||
|
||||
failsafe = False
|
||||
"""
|
||||
If True, the callback is guaranteed to run even if other callbacks
|
||||
from the same call point raise exceptions."""
|
||||
|
||||
|
||||
priority = 50
|
||||
"""
|
||||
Defines the order of execution for a list of Hooks. Priority numbers
|
||||
should be limited to the closed interval [0, 100], but values outside
|
||||
this range are acceptable, as are fractional values."""
|
||||
|
||||
|
||||
kwargs = {}
|
||||
"""
|
||||
A set of keyword arguments that will be passed to the
|
||||
callable on each call."""
|
||||
|
||||
|
||||
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
|
||||
self.callback = callback
|
||||
|
||||
|
||||
if failsafe is None:
|
||||
failsafe = getattr(callback, "failsafe", False)
|
||||
self.failsafe = failsafe
|
||||
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, "priority", 50)
|
||||
self.priority = priority
|
||||
|
||||
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
# Python 3
|
||||
return self.priority < other.priority
|
||||
@@ -56,11 +56,11 @@ class Hook(object):
|
||||
def __cmp__(self, other):
|
||||
# Python 2
|
||||
return cmp(self.priority, other.priority)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Run self.callback(**self.kwargs)."""
|
||||
return self.callback(**self.kwargs)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
|
||||
@@ -72,20 +72,20 @@ class Hook(object):
|
||||
|
||||
class HookMap(dict):
|
||||
"""A map of call points to lists of callbacks (Hook objects)."""
|
||||
|
||||
|
||||
def __new__(cls, points=None):
|
||||
d = dict.__new__(cls)
|
||||
for p in points or []:
|
||||
d[p] = []
|
||||
return d
|
||||
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
|
||||
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
|
||||
"""Append a new Hook made from the supplied arguments."""
|
||||
self[point].append(Hook(callback, failsafe, priority, **kwargs))
|
||||
|
||||
|
||||
def run(self, point):
|
||||
"""Execute all registered Hooks (callbacks) for the given point."""
|
||||
exc = None
|
||||
@@ -110,7 +110,7 @@ class HookMap(dict):
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if exc:
|
||||
raise exc
|
||||
|
||||
|
||||
def __copy__(self):
|
||||
newmap = self.__class__()
|
||||
# We can't just use 'update' because we want copies of the
|
||||
@@ -119,7 +119,7 @@ class HookMap(dict):
|
||||
newmap[k] = v[:]
|
||||
return newmap
|
||||
copy = __copy__
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
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):
|
||||
"""An HTTP request.
|
||||
|
||||
|
||||
This object represents the metadata of an HTTP request message;
|
||||
that is, it contains attributes which describe the environment
|
||||
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
|
||||
the given URL, and the execution plan for generating a response.
|
||||
"""
|
||||
|
||||
|
||||
prev = None
|
||||
"""
|
||||
The previous Request object (if any). This should be None
|
||||
unless we are processing an InternalRedirect."""
|
||||
|
||||
|
||||
# Conversation/connection attributes
|
||||
local = httputil.Host("127.0.0.1", 80)
|
||||
"An httputil.Host(ip, port, hostname) object for the server socket."
|
||||
|
||||
|
||||
remote = httputil.Host("127.0.0.1", 1111)
|
||||
"An httputil.Host(ip, port, hostname) object for the client socket."
|
||||
|
||||
|
||||
scheme = "http"
|
||||
"""
|
||||
The protocol used between client and server. In most cases,
|
||||
this will be either 'http' or 'https'."""
|
||||
|
||||
|
||||
server_protocol = "HTTP/1.1"
|
||||
"""
|
||||
The HTTP version for which the HTTP server is at least
|
||||
conditionally compliant."""
|
||||
|
||||
|
||||
base = ""
|
||||
"""The (scheme://host) portion of the requested URL.
|
||||
In some cases (e.g. when proxying via mod_rewrite), this may contain
|
||||
path segments which cherrypy.url uses when constructing url's, but
|
||||
which otherwise are ignored by CherryPy. Regardless, this value
|
||||
MUST NOT end in a slash."""
|
||||
|
||||
|
||||
# Request-Line attributes
|
||||
request_line = ""
|
||||
"""
|
||||
The complete Request-Line received from the client. This is a
|
||||
single string consisting of the request method, URI, and protocol
|
||||
version (joined by spaces). Any final CRLF is removed."""
|
||||
|
||||
|
||||
method = "GET"
|
||||
"""
|
||||
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
|
||||
servers and gateways may restrict the set of allowable methods.
|
||||
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
|
||||
|
||||
|
||||
query_string = ""
|
||||
"""
|
||||
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
|
||||
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
|
||||
'a=3&b=4'."""
|
||||
|
||||
|
||||
query_string_encoding = 'utf8'
|
||||
"""
|
||||
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
|
||||
encode back to bytes and re-decode to whatever encoding you like later.
|
||||
"""
|
||||
|
||||
|
||||
protocol = (1, 1)
|
||||
"""The HTTP protocol version corresponding to the set
|
||||
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).
|
||||
If either is 1.0, this attribute will be the tuple (1, 0).
|
||||
Lower HTTP protocol versions are not explicitly supported."""
|
||||
|
||||
|
||||
params = {}
|
||||
"""
|
||||
A dict which combines query string (GET) and request entity (POST)
|
||||
variables. This is populated in two stages: GET params are added
|
||||
before the 'on_start_resource' hook, and POST params are added
|
||||
between the 'before_request_body' and 'before_handler' hooks."""
|
||||
|
||||
|
||||
# Message attributes
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP request headers as (name, value) tuples.
|
||||
In general, you should use request.headers (a dict) instead."""
|
||||
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
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
|
||||
values (decoded according to :rfc:`2047` if necessary). See also:
|
||||
httputil.HeaderMap, httputil.HeaderElement."""
|
||||
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
|
||||
rfile = None
|
||||
"""
|
||||
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
|
||||
'before_handler' hook, and the resulting string is placed into
|
||||
either request.params or the request.body attribute.
|
||||
|
||||
|
||||
You may disable the automatic consumption of the rfile by setting
|
||||
request.process_request_body to False, either in config for the desired
|
||||
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
|
||||
rfile stream after CherryPy's automatic mechanism has read it. If you
|
||||
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
|
||||
or in corruption of the next (pipelined) request.
|
||||
"""
|
||||
|
||||
|
||||
process_request_body = True
|
||||
"""
|
||||
If True, the rfile (if any) is automatically read and parsed,
|
||||
and the result placed into request.params or request.body."""
|
||||
|
||||
|
||||
methods_with_bodies = ("POST", "PUT")
|
||||
"""
|
||||
A sequence of HTTP methods for which CherryPy will automatically
|
||||
attempt to read a body from the rfile."""
|
||||
|
||||
|
||||
body = None
|
||||
"""
|
||||
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
|
||||
can .read()); this value is set between the 'before_request_body' and
|
||||
'before_handler' hooks (assuming that process_request_body is True)."""
|
||||
|
||||
|
||||
# Dispatch attributes
|
||||
dispatch = cherrypy.dispatch.Dispatcher()
|
||||
"""
|
||||
@@ -322,19 +322,19 @@ class Request(object):
|
||||
request attributes, and the application architecture. The core
|
||||
calls the dispatcher as early as possible, passing it a 'path_info'
|
||||
argument.
|
||||
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
See help(cherrypy.dispatch) for more information."""
|
||||
|
||||
|
||||
script_name = ""
|
||||
"""
|
||||
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
|
||||
the root of the URI, it MUST be an empty string (not "/").
|
||||
"""
|
||||
|
||||
|
||||
path_info = "/"
|
||||
"""
|
||||
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
|
||||
set to 'False' if it failed and to the 'username' value if it succeeded.
|
||||
The default 'None' implies that no authentication happened."""
|
||||
|
||||
|
||||
# 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.
|
||||
app = None
|
||||
"""The cherrypy.Application object which is handling this request."""
|
||||
|
||||
|
||||
handler = None
|
||||
"""
|
||||
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
|
||||
starting at request.app.root, and is then passed all HTTP params
|
||||
(from the query string and POST body) as keyword arguments."""
|
||||
|
||||
|
||||
toolmaps = {}
|
||||
"""
|
||||
A nested dict of all Toolboxes and Tools in effect for this request,
|
||||
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
|
||||
|
||||
|
||||
config = None
|
||||
"""
|
||||
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
|
||||
anywhere in the tree between request.app.root and the final handler,
|
||||
and inherits downward)."""
|
||||
|
||||
|
||||
is_index = None
|
||||
"""
|
||||
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
|
||||
user-agent to a 'more canonical' URL which either adds or removes
|
||||
the trailing slash. See cherrypy.tools.trailing_slash."""
|
||||
|
||||
|
||||
hooks = HookMap(hookpoints)
|
||||
"""
|
||||
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
|
||||
from Tools specified in config), but may be extended at any time.
|
||||
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
|
||||
|
||||
|
||||
error_response = cherrypy.HTTPError(500).set_response
|
||||
"""
|
||||
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).
|
||||
By default, error_response uses HTTPError(500) to return a generic
|
||||
error response to the user-agent."""
|
||||
|
||||
|
||||
error_page = {}
|
||||
"""
|
||||
A dict of {error code: response filename or callable} pairs.
|
||||
|
||||
|
||||
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
|
||||
is found for a given numeric code.
|
||||
|
||||
|
||||
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,
|
||||
and %(version)s. The set of format mappings can be extended by
|
||||
overriding HTTPError.set_response.
|
||||
|
||||
|
||||
If a callable is provided, it will be called by default with keyword
|
||||
arguments 'status', 'message', 'traceback', and 'version', as for a
|
||||
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
|
||||
perform any other processing.
|
||||
|
||||
|
||||
If no entry is given for an error code, and no 'default' entry exists,
|
||||
a default template will be used.
|
||||
"""
|
||||
|
||||
|
||||
show_tracebacks = True
|
||||
"""
|
||||
If True, unexpected errors encountered during request processing will
|
||||
@@ -436,23 +436,23 @@ class Request(object):
|
||||
"""
|
||||
If True, mismatched parameters encountered during PageHandler invocation
|
||||
processing will be included in the response body."""
|
||||
|
||||
|
||||
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
|
||||
"""The sequence of exceptions which Request.run does not trap."""
|
||||
|
||||
|
||||
throw_errors = False
|
||||
"""
|
||||
If True, Request.run will not trap any errors (except HTTPRedirect and
|
||||
HTTPError, which are more properly called 'exceptions', not errors)."""
|
||||
|
||||
|
||||
closed = False
|
||||
"""True once the close method has been called, False otherwise."""
|
||||
|
||||
|
||||
stage = None
|
||||
"""
|
||||
A string containing the stage reached in the request-handling process.
|
||||
This is useful when debugging a live server with hung requests."""
|
||||
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet(
|
||||
**{"hooks": hooks_namespace,
|
||||
"request": request_namespace,
|
||||
@@ -460,11 +460,11 @@ class Request(object):
|
||||
"error_page": error_page_namespace,
|
||||
"tools": cherrypy.tools,
|
||||
})
|
||||
|
||||
|
||||
def __init__(self, local_host, remote_host, scheme="http",
|
||||
server_protocol="HTTP/1.1"):
|
||||
"""Populate a new Request object.
|
||||
|
||||
|
||||
local_host should be an httputil.Host object with the server info.
|
||||
remote_host should be an httputil.Host object with the client info.
|
||||
scheme should be a string, either "http" or "https".
|
||||
@@ -473,17 +473,17 @@ class Request(object):
|
||||
self.remote = remote_host
|
||||
self.scheme = scheme
|
||||
self.server_protocol = server_protocol
|
||||
|
||||
|
||||
self.closed = False
|
||||
|
||||
|
||||
# Put a *copy* of the class error_page into self.
|
||||
self.error_page = self.error_page.copy()
|
||||
|
||||
|
||||
# Put a *copy* of the class namespaces into self.
|
||||
self.namespaces = self.namespaces.copy()
|
||||
|
||||
|
||||
self.stage = None
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Run cleanup code. (Core)"""
|
||||
if not self.closed:
|
||||
@@ -491,49 +491,49 @@ class Request(object):
|
||||
self.stage = 'on_end_request'
|
||||
self.hooks.run('on_end_request')
|
||||
self.stage = 'close'
|
||||
|
||||
|
||||
def run(self, method, path, query_string, req_protocol, headers, rfile):
|
||||
r"""Process the Request. (Core)
|
||||
|
||||
|
||||
method, path, query_string, and req_protocol should be pulled directly
|
||||
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
|
||||
|
||||
|
||||
path
|
||||
This should be %XX-unquoted, but query_string should not be.
|
||||
|
||||
|
||||
When using Python 2, they both MUST be byte strings,
|
||||
not unicode strings.
|
||||
|
||||
|
||||
When using Python 3, they both MUST be unicode strings,
|
||||
not byte strings, and preferably not bytes \x00-\xFF
|
||||
disguised as unicode.
|
||||
|
||||
|
||||
headers
|
||||
A list of (name, value) tuples.
|
||||
|
||||
|
||||
rfile
|
||||
A file-like object containing the HTTP request entity.
|
||||
|
||||
|
||||
When run() is done, the returned object should have 3 attributes:
|
||||
|
||||
|
||||
* status, e.g. "200 OK"
|
||||
* header_list, a list of (name, value) tuples
|
||||
* body, an iterable yielding strings
|
||||
|
||||
|
||||
Consumer code (HTTP servers) should then access these response
|
||||
attributes to build the outbound stream.
|
||||
|
||||
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
self.stage = 'run'
|
||||
try:
|
||||
self.error_response = cherrypy.HTTPError(500).set_response
|
||||
|
||||
|
||||
self.method = method
|
||||
path = path or "/"
|
||||
self.query_string = query_string or ''
|
||||
self.params = {}
|
||||
|
||||
|
||||
# Compare request and server HTTP protocol versions, in case our
|
||||
# server does not support the requested protocol. Limit our 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])
|
||||
self.protocol = min(rp, sp)
|
||||
response.headers.protocol = self.protocol
|
||||
|
||||
|
||||
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
|
||||
url = path
|
||||
if query_string:
|
||||
url += '?' + query_string
|
||||
self.request_line = '%s %s %s' % (method, url, req_protocol)
|
||||
|
||||
|
||||
self.header_list = list(headers)
|
||||
self.headers = httputil.HeaderMap()
|
||||
|
||||
|
||||
self.rfile = rfile
|
||||
self.body = None
|
||||
|
||||
|
||||
self.cookie = SimpleCookie()
|
||||
self.handler = None
|
||||
|
||||
|
||||
# path_info should be the path from the
|
||||
# app root (script_name) to the handler.
|
||||
self.script_name = self.app.script_name
|
||||
self.path_info = pi = path[len(self.script_name):]
|
||||
|
||||
|
||||
self.stage = 'respond'
|
||||
self.respond(pi)
|
||||
|
||||
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
@@ -589,24 +589,24 @@ class Request(object):
|
||||
body = ""
|
||||
r = bare_error(body)
|
||||
response.output_status, response.header_list, response.body = r
|
||||
|
||||
|
||||
if self.method == "HEAD":
|
||||
# HEAD requests MUST NOT return a message-body in the response.
|
||||
response.body = []
|
||||
|
||||
|
||||
try:
|
||||
cherrypy.log.access()
|
||||
except:
|
||||
cherrypy.log.error(traceback=True)
|
||||
|
||||
|
||||
if response.timed_out:
|
||||
raise cherrypy.TimeoutError()
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# Uncomment for stage debugging
|
||||
# stage = property(lambda self: self._stage, lambda self, v: print(v))
|
||||
|
||||
|
||||
def respond(self, path_info):
|
||||
"""Generate a response for the resource at self.path_info. (Core)"""
|
||||
response = cherrypy.serving.response
|
||||
@@ -615,30 +615,30 @@ class Request(object):
|
||||
try:
|
||||
if self.app is None:
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
|
||||
# Get the 'Host' header, so we can HTTPRedirect properly.
|
||||
self.stage = 'process_headers'
|
||||
self.process_headers()
|
||||
|
||||
|
||||
# Make a copy of the class hooks
|
||||
self.hooks = self.__class__.hooks.copy()
|
||||
self.toolmaps = {}
|
||||
|
||||
|
||||
self.stage = 'get_resource'
|
||||
self.get_resource(path_info)
|
||||
|
||||
|
||||
self.body = _cpreqbody.RequestBody(
|
||||
self.rfile, self.headers, request_params=self.params)
|
||||
|
||||
|
||||
self.namespaces(self.config)
|
||||
|
||||
|
||||
self.stage = 'on_start_resource'
|
||||
self.hooks.run('on_start_resource')
|
||||
|
||||
|
||||
# Parse the querystring
|
||||
self.stage = 'process_query_string'
|
||||
self.process_query_string()
|
||||
|
||||
|
||||
# Process the body
|
||||
if self.process_request_body:
|
||||
if self.method not in self.methods_with_bodies:
|
||||
@@ -647,14 +647,14 @@ class Request(object):
|
||||
self.hooks.run('before_request_body')
|
||||
if self.process_request_body:
|
||||
self.body.process()
|
||||
|
||||
|
||||
# Run the handler
|
||||
self.stage = 'before_handler'
|
||||
self.hooks.run('before_handler')
|
||||
if self.handler:
|
||||
self.stage = 'handler'
|
||||
response.body = self.handler()
|
||||
|
||||
|
||||
# Finalize
|
||||
self.stage = 'before_finalize'
|
||||
self.hooks.run('before_finalize')
|
||||
@@ -674,7 +674,7 @@ class Request(object):
|
||||
if self.throw_errors:
|
||||
raise
|
||||
self.handle_error()
|
||||
|
||||
|
||||
def process_query_string(self):
|
||||
"""Parse the query string into Python structures. (Core)"""
|
||||
try:
|
||||
@@ -685,7 +685,7 @@ class Request(object):
|
||||
404, "The given query string could not be processed. Query "
|
||||
"strings for this resource must be encoded with %r." %
|
||||
self.query_string_encoding)
|
||||
|
||||
|
||||
# Python 2 only: keyword arguments must be byte strings (type 'str').
|
||||
if not py3k:
|
||||
for key, value in p.items():
|
||||
@@ -693,7 +693,7 @@ class Request(object):
|
||||
del p[key]
|
||||
p[key.encode(self.query_string_encoding)] = value
|
||||
self.params.update(p)
|
||||
|
||||
|
||||
def process_headers(self):
|
||||
"""Parse HTTP header data into Python structures. (Core)"""
|
||||
# Process the headers into self.headers
|
||||
@@ -703,7 +703,7 @@ class Request(object):
|
||||
# so title doesn't have to be called twice.
|
||||
name = name.title()
|
||||
value = value.strip()
|
||||
|
||||
|
||||
# Warning: if there is more than one header entry for cookies (AFAIK,
|
||||
# only Konqueror does that), only the last one will remain in headers
|
||||
# (but they will be correctly stored in request.cookie).
|
||||
@@ -711,7 +711,7 @@ class Request(object):
|
||||
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
|
||||
else:
|
||||
dict.__setitem__(headers, name, value)
|
||||
|
||||
|
||||
# Handle cookies differently because on Konqueror, multiple
|
||||
# cookies come on different lines with the same key
|
||||
if name == 'Cookie':
|
||||
@@ -720,7 +720,7 @@ class Request(object):
|
||||
except CookieError:
|
||||
msg = "Illegal cookie name %s" % value.split('=')[0]
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
|
||||
|
||||
if not dict.__contains__(headers, 'Host'):
|
||||
# All Internet-based HTTP/1.1 servers MUST respond with a 400
|
||||
# (Bad Request) status code to any HTTP/1.1 request message
|
||||
@@ -732,17 +732,17 @@ class Request(object):
|
||||
if not host:
|
||||
host = self.local.name or self.local.ip
|
||||
self.base = "%s://%s" % (self.scheme, host)
|
||||
|
||||
|
||||
def get_resource(self, path):
|
||||
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
|
||||
# First, see if there is a custom dispatch at this URI. Custom
|
||||
# dispatchers can only be specified in app.config, not in _cp_config
|
||||
# (since custom dispatchers may not even have an app.root).
|
||||
dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
|
||||
|
||||
|
||||
# dispatch() should set self.handler and self.config
|
||||
dispatch(path)
|
||||
|
||||
|
||||
def handle_error(self):
|
||||
"""Handle the last unanticipated exception. (Core)"""
|
||||
try:
|
||||
@@ -755,9 +755,9 @@ class Request(object):
|
||||
inst = sys.exc_info()[1]
|
||||
inst.set_response()
|
||||
cherrypy.serving.response.finalize()
|
||||
|
||||
|
||||
# ------------------------- Properties ------------------------- #
|
||||
|
||||
|
||||
def _get_body_params(self):
|
||||
warnings.warn(
|
||||
"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
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True).
|
||||
|
||||
|
||||
Deprecated in 3.2, will be removed for 3.3 in favor of
|
||||
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
|
||||
|
||||
|
||||
class ResponseBody(object):
|
||||
"""The body of the HTTP response (the response entity)."""
|
||||
|
||||
|
||||
if py3k:
|
||||
unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
|
||||
"if you wish to return unicode.")
|
||||
|
||||
|
||||
def __get__(self, obj, objclass=None):
|
||||
if obj is None:
|
||||
# When calling on the class instead of an instance...
|
||||
return self
|
||||
else:
|
||||
return obj._body
|
||||
|
||||
|
||||
def __set__(self, obj, value):
|
||||
# Convert the given value to an iterable object.
|
||||
if py3k and isinstance(value, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
|
||||
|
||||
if isinstance(value, basestring):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# 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 [].
|
||||
value = []
|
||||
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):
|
||||
if isinstance(item, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
@@ -823,17 +823,17 @@ class ResponseBody(object):
|
||||
|
||||
class Response(object):
|
||||
"""An HTTP Response, including status, headers, and body."""
|
||||
|
||||
|
||||
status = ""
|
||||
"""The HTTP Status-Code and Reason-Phrase."""
|
||||
|
||||
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP response headers as (name, value) tuples.
|
||||
In general, you should use response.headers (a dict) instead. This
|
||||
attribute is generated from response.headers and is not valid until
|
||||
after the finalize phase."""
|
||||
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
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
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to :rfc:`2047` if necessary).
|
||||
|
||||
|
||||
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
|
||||
"""
|
||||
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
|
||||
body = ResponseBody()
|
||||
"""The body (entity) of the HTTP response."""
|
||||
|
||||
|
||||
time = None
|
||||
"""The value of time.time() when created. Use in HTTP dates."""
|
||||
|
||||
|
||||
timeout = 300
|
||||
"""Seconds after which the response will be aborted."""
|
||||
|
||||
|
||||
timed_out = False
|
||||
"""
|
||||
Flag to indicate the response should be aborted, because it has
|
||||
exceeded its timeout."""
|
||||
|
||||
|
||||
stream = False
|
||||
"""If False, buffer the response body."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.header_list = None
|
||||
self._body = []
|
||||
self.time = time.time()
|
||||
|
||||
|
||||
self.headers = httputil.HeaderMap()
|
||||
# Since we know all our keys are titled strings, we can
|
||||
# bypass HeaderMap.update and get a big speed boost.
|
||||
@@ -880,34 +880,34 @@ class Response(object):
|
||||
"Date": httputil.HTTPDate(self.time),
|
||||
})
|
||||
self.cookie = SimpleCookie()
|
||||
|
||||
|
||||
def collapse_body(self):
|
||||
"""Collapse self.body to a single string; replace it and return it."""
|
||||
if isinstance(self.body, basestring):
|
||||
return self.body
|
||||
|
||||
|
||||
newbody = []
|
||||
for chunk in self.body:
|
||||
if py3k and not isinstance(chunk, bytes):
|
||||
raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk))
|
||||
newbody.append(chunk)
|
||||
newbody = ntob('').join(newbody)
|
||||
|
||||
|
||||
self.body = newbody
|
||||
return newbody
|
||||
|
||||
|
||||
def finalize(self):
|
||||
"""Transform headers (and cookies) into self.header_list. (Core)"""
|
||||
try:
|
||||
code, reason, _ = httputil.valid_status(self.status)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
|
||||
|
||||
|
||||
headers = self.headers
|
||||
|
||||
|
||||
self.status = "%s %s" % (code, reason)
|
||||
self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
|
||||
|
||||
|
||||
if self.stream:
|
||||
# The upshot: wsgiserver will chunk the response if
|
||||
# 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:
|
||||
content = self.collapse_body()
|
||||
dict.__setitem__(headers, 'Content-Length', len(content))
|
||||
|
||||
|
||||
# Transform our header dict into a list of tuples.
|
||||
self.header_list = h = headers.output()
|
||||
|
||||
|
||||
cookie = self.cookie.output()
|
||||
if cookie:
|
||||
for line in cookie.split("\n"):
|
||||
@@ -942,10 +942,10 @@ class Response(object):
|
||||
if isinstance(value, unicodestr):
|
||||
value = headers.encode(value)
|
||||
h.append((name, value))
|
||||
|
||||
|
||||
def check_timeout(self):
|
||||
"""If now > self.time + self.timeout, set self.timed_out.
|
||||
|
||||
|
||||
This purposefully sets a flag, rather than raising an error,
|
||||
so that a monitor thread can interrupt the Response thread.
|
||||
"""
|
||||
|
||||
+33
-33
@@ -13,18 +13,18 @@ from cherrypy.process.servers import *
|
||||
|
||||
class Server(ServerAdapter):
|
||||
"""An adapter for an HTTP server.
|
||||
|
||||
|
||||
You can set attributes (like socket_host and socket_port)
|
||||
on *this* object (which is probably cherrypy.server), and call
|
||||
quickstart. For example::
|
||||
|
||||
|
||||
cherrypy.server.socket_port = 80
|
||||
cherrypy.quickstart()
|
||||
"""
|
||||
|
||||
|
||||
socket_port = 8080
|
||||
"""The TCP port on which to listen for connections."""
|
||||
|
||||
|
||||
_socket_host = '127.0.0.1'
|
||||
def _get_socket_host(self):
|
||||
return self._socket_host
|
||||
@@ -36,68 +36,68 @@ class Server(ServerAdapter):
|
||||
self._socket_host = value
|
||||
socket_host = property(_get_socket_host, _set_socket_host,
|
||||
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.
|
||||
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
|
||||
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
|
||||
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
|
||||
not allowed.""")
|
||||
|
||||
|
||||
socket_file = None
|
||||
"""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
|
||||
are ignored."""
|
||||
|
||||
|
||||
socket_queue_size = 5
|
||||
"""The 'backlog' argument to socket.listen(); specifies the maximum number
|
||||
of queued connections (default 5)."""
|
||||
|
||||
|
||||
socket_timeout = 10
|
||||
"""The timeout in seconds for accepted connections (default 10)."""
|
||||
|
||||
|
||||
shutdown_timeout = 5
|
||||
"""The time to wait for HTTP worker threads to clean up."""
|
||||
|
||||
|
||||
protocol_version = 'HTTP/1.1'
|
||||
"""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,
|
||||
this should also limit the supported features used in the response."""
|
||||
|
||||
|
||||
thread_pool = 10
|
||||
"""The number of worker threads to start up in the pool."""
|
||||
|
||||
|
||||
thread_pool_max = -1
|
||||
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
|
||||
|
||||
|
||||
max_request_header_size = 500 * 1024
|
||||
"""The maximum number of bytes allowable in the request headers. If exceeded,
|
||||
the HTTP server should return "413 Request Entity Too Large"."""
|
||||
|
||||
|
||||
max_request_body_size = 100 * 1024 * 1024
|
||||
"""The maximum number of bytes allowable in the request body. If exceeded,
|
||||
the HTTP server should return "413 Request Entity Too Large"."""
|
||||
|
||||
|
||||
instance = None
|
||||
"""If not None, this should be an HTTP server instance (such as
|
||||
CPWSGIServer) which cherrypy.server will control. Use this when you need
|
||||
more control over object instantiation than is available in the various
|
||||
configuration options."""
|
||||
|
||||
|
||||
ssl_context = None
|
||||
"""When using PyOpenSSL, an instance of SSL.Context."""
|
||||
|
||||
|
||||
ssl_certificate = None
|
||||
"""The filename of the SSL certificate to use."""
|
||||
|
||||
|
||||
ssl_certificate_chain = None
|
||||
"""When using PyOpenSSL, the certificate chain to pass to
|
||||
Context.load_verify_locations."""
|
||||
|
||||
|
||||
ssl_private_key = None
|
||||
"""The filename of the private key to use with SSL."""
|
||||
|
||||
|
||||
if py3k:
|
||||
ssl_module = '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
|
||||
project, which you must install separately). You may also register your
|
||||
own classes in the wsgiserver.ssl_adapters dict."""
|
||||
|
||||
|
||||
statistics = False
|
||||
"""Turns statistics-gathering on or off for aware HTTP servers."""
|
||||
|
||||
|
||||
nodelay = True
|
||||
"""If True (the default since 3.1), sets the TCP_NODELAY socket option."""
|
||||
|
||||
|
||||
wsgi_version = (1, 0)
|
||||
"""The WSGI version tuple to use with the builtin WSGI server.
|
||||
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.
|
||||
You may create and register your own experimental versions of the WSGI
|
||||
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.bus = cherrypy.engine
|
||||
self.httpserver = None
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
|
||||
def httpserver_from_self(self, httpserver=None):
|
||||
"""Return a (httpserver, bind_addr) pair based on self attributes."""
|
||||
if httpserver is None:
|
||||
@@ -143,14 +143,14 @@ class Server(ServerAdapter):
|
||||
# Is anyone using this? Can I add an arg?
|
||||
httpserver = attributes(httpserver)(self)
|
||||
return httpserver, self.bind_addr
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if not self.httpserver:
|
||||
self.httpserver, self.bind_addr = self.httpserver_from_self()
|
||||
ServerAdapter.start(self)
|
||||
start.priority = 75
|
||||
|
||||
|
||||
def _get_bind_addr(self):
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
@@ -176,12 +176,12 @@ class Server(ServerAdapter):
|
||||
"domain sockets), not %r" % value)
|
||||
bind_addr = property(_get_bind_addr, _set_bind_addr,
|
||||
doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
|
||||
|
||||
|
||||
def base(self):
|
||||
"""Return the base (scheme://host[:port] or sock file) for this server."""
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
|
||||
|
||||
host = self.socket_host
|
||||
if host in ('0.0.0.0', '::'):
|
||||
# 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.
|
||||
import socket
|
||||
host = socket.gethostname()
|
||||
|
||||
|
||||
port = self.socket_port
|
||||
|
||||
|
||||
if self.ssl_certificate:
|
||||
scheme = "https"
|
||||
if port != 443:
|
||||
@@ -200,6 +200,6 @@ class Server(ServerAdapter):
|
||||
scheme = "http"
|
||||
if port != 80:
|
||||
host += ":%s" % port
|
||||
|
||||
|
||||
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
|
||||
may only offer one if they choose):
|
||||
|
||||
|
||||
Library calls
|
||||
All tools are callables that can be used wherever needed.
|
||||
The arguments are straightforward and should be detailed within the
|
||||
docstring.
|
||||
|
||||
|
||||
Function decorators
|
||||
All tools, when called, may be used as decorators which configure
|
||||
individual CherryPy page handlers (methods on the CherryPy tree).
|
||||
That is, "@tools.anytool()" should "turn on" the tool via the
|
||||
decorated function's _cp_config attribute.
|
||||
|
||||
|
||||
CherryPy config
|
||||
If a tool exposes a "_setup" callable, it will be called
|
||||
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):
|
||||
"""A registered function for use with CherryPy request-processing hooks.
|
||||
|
||||
|
||||
help(tool.callable) should give you more information about this Tool.
|
||||
"""
|
||||
|
||||
|
||||
namespace = "tools"
|
||||
|
||||
|
||||
def __init__(self, point, callable, name=None, priority=50):
|
||||
self._point = point
|
||||
self.callable = callable
|
||||
@@ -61,13 +61,13 @@ class Tool(object):
|
||||
self._priority = priority
|
||||
self.__doc__ = self.callable.__doc__
|
||||
self._setargs()
|
||||
|
||||
|
||||
def _get_on(self):
|
||||
raise AttributeError(_attr_error)
|
||||
def _set_on(self, value):
|
||||
raise AttributeError(_attr_error)
|
||||
on = property(_get_on, _set_on)
|
||||
|
||||
|
||||
def _setargs(self):
|
||||
"""Copy func parameter names to obj attributes."""
|
||||
try:
|
||||
@@ -86,28 +86,28 @@ class Tool(object):
|
||||
# but if we trap it here it doesn't prevent CP from working.
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
def _merged_args(self, d=None):
|
||||
"""Return a dict of configuration entries for this Tool."""
|
||||
if d:
|
||||
conf = d.copy()
|
||||
else:
|
||||
conf = {}
|
||||
|
||||
|
||||
tm = cherrypy.serving.request.toolmaps[self.namespace]
|
||||
if self._name in tm:
|
||||
conf.update(tm[self._name])
|
||||
|
||||
|
||||
if "on" in conf:
|
||||
del conf["on"]
|
||||
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Compile-time decorator (turn on the tool in config).
|
||||
|
||||
|
||||
For example::
|
||||
|
||||
|
||||
@tools.proxy()
|
||||
def whats_my_base(self):
|
||||
return cherrypy.request.base
|
||||
@@ -126,10 +126,10 @@ class Tool(object):
|
||||
f._cp_config[subspace + k] = v
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
@@ -143,7 +143,7 @@ class Tool(object):
|
||||
|
||||
class HandlerTool(Tool):
|
||||
"""Tool which is called 'before main', that may skip normal handlers.
|
||||
|
||||
|
||||
If the tool successfully handles the request (by setting response.body),
|
||||
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
|
||||
@@ -151,15 +151,15 @@ class HandlerTool(Tool):
|
||||
tool is declared AS a page handler (see the 'handler' method), returning
|
||||
False will raise NotFound.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, 'before_handler', callable, name)
|
||||
|
||||
|
||||
def handler(self, *args, **kwargs):
|
||||
"""Use this tool as a CherryPy page handler.
|
||||
|
||||
|
||||
For example::
|
||||
|
||||
|
||||
class Root:
|
||||
nav = tools.staticdir.handler(section="/nav", dir="nav",
|
||||
root=absDir)
|
||||
@@ -171,14 +171,14 @@ class HandlerTool(Tool):
|
||||
return cherrypy.serving.response.body
|
||||
handle_func.exposed = True
|
||||
return handle_func
|
||||
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
if self.callable(**kwargs):
|
||||
cherrypy.serving.request.handler = None
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
@@ -192,15 +192,15 @@ class HandlerTool(Tool):
|
||||
|
||||
class HandlerWrapperTool(Tool):
|
||||
"""Tool which wraps request.handler in a provided wrapper function.
|
||||
|
||||
|
||||
The 'newhandler' arg must be a handler wrapper function that takes a
|
||||
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
|
||||
page handler
|
||||
functions, it must return an iterable for use as cherrypy.response.body.
|
||||
|
||||
|
||||
For example, to allow your 'inner' page handlers to return dicts
|
||||
which then get interpolated into a template::
|
||||
|
||||
|
||||
def interpolator(next_handler, *args, **kwargs):
|
||||
filename = cherrypy.request.config.get('template')
|
||||
cherrypy.response.template = env.get_template(filename)
|
||||
@@ -208,13 +208,13 @@ class HandlerWrapperTool(Tool):
|
||||
return cherrypy.response.template.render(**response_dict)
|
||||
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None, priority=50):
|
||||
self.newhandler = newhandler
|
||||
self._point = point
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
|
||||
|
||||
def callable(self, debug=False):
|
||||
innerfunc = cherrypy.serving.request.handler
|
||||
def wrap(*args, **kwargs):
|
||||
@@ -224,16 +224,16 @@ class HandlerWrapperTool(Tool):
|
||||
|
||||
class ErrorTool(Tool):
|
||||
"""Tool which is used to replace the default request.error_response."""
|
||||
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, None, callable, name)
|
||||
|
||||
|
||||
def _wrapper(self):
|
||||
self.callable(**self._merged_args())
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
@@ -250,44 +250,44 @@ from cherrypy.lib import auth_basic, auth_digest
|
||||
|
||||
class SessionTool(Tool):
|
||||
"""Session Tool for CherryPy.
|
||||
|
||||
|
||||
sessions.locking
|
||||
When 'implicit' (the default), the session will be locked for you,
|
||||
just before running the page handler.
|
||||
|
||||
|
||||
When 'early', the session will be locked before reading the request
|
||||
body. This is off by default for safety reasons; for example,
|
||||
a large upload would block the session, denying an AJAX
|
||||
progress meter (see http://www.cherrypy.org/ticket/630).
|
||||
|
||||
|
||||
When 'explicit' (or any other value), you need to call
|
||||
cherrypy.session.acquire_lock() yourself before using
|
||||
session data.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
# _sessions.init must be bound after headers are read
|
||||
Tool.__init__(self, 'before_request_body', _sessions.init)
|
||||
|
||||
|
||||
def _lock_session(self):
|
||||
cherrypy.serving.session.acquire_lock()
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
hooks = cherrypy.serving.request.hooks
|
||||
|
||||
|
||||
conf = self._merged_args()
|
||||
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, "priority", self._priority)
|
||||
|
||||
|
||||
hooks.attach(self._point, self.callable, priority=p, **conf)
|
||||
|
||||
|
||||
locking = conf.pop('locking', 'implicit')
|
||||
if locking == 'implicit':
|
||||
hooks.attach('before_handler', self._lock_session)
|
||||
@@ -298,15 +298,15 @@ class SessionTool(Tool):
|
||||
else:
|
||||
# Don't lock
|
||||
pass
|
||||
|
||||
|
||||
hooks.attach('before_finalize', _sessions.save)
|
||||
hooks.attach('on_end_request', _sessions.close)
|
||||
|
||||
|
||||
def regenerate(self):
|
||||
"""Drop the current session and make a new one (with a new id)."""
|
||||
sess = cherrypy.serving.session
|
||||
sess.regenerate()
|
||||
|
||||
|
||||
# Grab cookie-relevant tool args
|
||||
conf = dict([(k, v) for k, v in self._merged_args().items()
|
||||
if k in ('path', 'path_header', 'name', 'timeout',
|
||||
@@ -318,15 +318,15 @@ class SessionTool(Tool):
|
||||
|
||||
class XMLRPCController(object):
|
||||
"""A Controller (page handler collection) for XML-RPC.
|
||||
|
||||
|
||||
To use it, have your controllers subclass this base class (it will
|
||||
turn on the tool for you).
|
||||
|
||||
|
||||
You can also supply the following optional config entries::
|
||||
|
||||
|
||||
tools.xmlrpc.encoding: 'utf-8'
|
||||
tools.xmlrpc.allow_none: 0
|
||||
|
||||
|
||||
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
|
||||
appropriate handler must first be performed according to the URL, and
|
||||
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
|
||||
requires a 200 OK "Fault" response instead of 404 when the desired
|
||||
method is not found.
|
||||
|
||||
|
||||
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
|
||||
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
|
||||
does its own second dispatch step based on that method. It also reads
|
||||
body params, and returns a Fault on error.
|
||||
|
||||
|
||||
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
|
||||
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
|
||||
Otherwise, you need to use declare it in config::
|
||||
|
||||
|
||||
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
|
||||
"""
|
||||
|
||||
|
||||
# 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
|
||||
# would be if someone actually disabled the default_toolbox. Meh.
|
||||
_cp_config = {'tools.xmlrpc.on': True}
|
||||
|
||||
|
||||
def default(self, *vpath, **params):
|
||||
rpcparams, rpcmethod = _xmlrpc.process_body()
|
||||
|
||||
|
||||
subhandler = self
|
||||
for attr in str(rpcmethod).split('.'):
|
||||
subhandler = getattr(subhandler, attr, None)
|
||||
|
||||
|
||||
if subhandler and getattr(subhandler, "exposed", False):
|
||||
body = subhandler(*(vpath + rpcparams), **params)
|
||||
|
||||
|
||||
else:
|
||||
# http://www.cherrypy.org/ticket/533
|
||||
# if a method is not found, an xmlrpclib.Fault should be returned
|
||||
# raising an exception here will do that; see
|
||||
# cherrypy.lib.xmlrpcutil.on_error
|
||||
raise Exception('method "%s" is not supported' % attr)
|
||||
|
||||
|
||||
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
|
||||
_xmlrpc.respond(body,
|
||||
conf.get('encoding', 'utf-8'),
|
||||
@@ -379,7 +379,7 @@ class XMLRPCController(object):
|
||||
|
||||
|
||||
class SessionAuthTool(HandlerTool):
|
||||
|
||||
|
||||
def _setargs(self):
|
||||
for name in dir(cptools.SessionAuth):
|
||||
if not name.startswith("__"):
|
||||
@@ -388,7 +388,7 @@ class SessionAuthTool(HandlerTool):
|
||||
|
||||
class CachingTool(Tool):
|
||||
"""Caching Tool for CherryPy."""
|
||||
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
request = cherrypy.serving.request
|
||||
if _caching.get(**kwargs):
|
||||
@@ -399,11 +399,11 @@ class CachingTool(Tool):
|
||||
request.hooks.attach('before_finalize', _caching.tee_output,
|
||||
priority = 90)
|
||||
_wrapper.priority = 20
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook caching into cherrypy.request."""
|
||||
conf = self._merged_args()
|
||||
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
|
||||
priority=p, **conf)
|
||||
@@ -412,14 +412,14 @@ class CachingTool(Tool):
|
||||
|
||||
class Toolbox(object):
|
||||
"""A collection of Tools.
|
||||
|
||||
|
||||
This object also functions as a config namespace handler for itself.
|
||||
Custom toolboxes should be added to each Application's toolboxes dict.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# If the Tool._name is None, supply it from the attribute name.
|
||||
if isinstance(value, Tool):
|
||||
@@ -427,7 +427,7 @@ class Toolbox(object):
|
||||
value._name = name
|
||||
value.namespace = self.namespace
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""Populate request.toolmaps from tools specified in config."""
|
||||
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
|
||||
@@ -436,7 +436,7 @@ class Toolbox(object):
|
||||
bucket = map.setdefault(toolname, {})
|
||||
bucket[arg] = v
|
||||
return populate
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Run tool._setup() for each tool in our toolmap."""
|
||||
map = cherrypy.serving.request.toolmaps.get(self.namespace)
|
||||
@@ -448,21 +448,21 @@ class Toolbox(object):
|
||||
|
||||
|
||||
class DeprecatedTool(Tool):
|
||||
|
||||
|
||||
_name = None
|
||||
warnmsg = "This Tool is deprecated."
|
||||
|
||||
|
||||
def __init__(self, point, warnmsg=None):
|
||||
self.point = point
|
||||
if warnmsg is not None:
|
||||
self.warnmsg = warnmsg
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
warnings.warn(self.warnmsg)
|
||||
def tool_decorator(f):
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
|
||||
def _setup(self):
|
||||
warnings.warn(self.warnmsg)
|
||||
|
||||
|
||||
+55
-55
@@ -11,69 +11,69 @@ from cherrypy.lib import httputil
|
||||
|
||||
class Application(object):
|
||||
"""A CherryPy Application.
|
||||
|
||||
|
||||
Servers and gateways should not instantiate Request objects directly.
|
||||
Instead, they should ask an Application object for a request object.
|
||||
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object) for itself.
|
||||
"""
|
||||
|
||||
|
||||
root = None
|
||||
"""The top-most container of page handlers for this app. Handlers should
|
||||
be arranged in a hierarchy of attributes, matching the expected URI
|
||||
hierarchy; the default dispatcher then searches this hierarchy for a
|
||||
matching handler. When using a dispatcher other than the default,
|
||||
this value may be None."""
|
||||
|
||||
|
||||
config = {}
|
||||
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
|
||||
of {key: value} pairs."""
|
||||
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet()
|
||||
toolboxes = {'tools': cherrypy.tools}
|
||||
|
||||
|
||||
log = None
|
||||
"""A LogManager instance. See _cplogging."""
|
||||
|
||||
|
||||
wsgiapp = None
|
||||
"""A CPWSGIApp instance. See _cpwsgi."""
|
||||
|
||||
|
||||
request_class = _cprequest.Request
|
||||
response_class = _cprequest.Response
|
||||
|
||||
|
||||
relative_urls = False
|
||||
|
||||
|
||||
def __init__(self, root, script_name="", config=None):
|
||||
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
|
||||
self.root = root
|
||||
self.script_name = script_name
|
||||
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
|
||||
|
||||
|
||||
self.namespaces = self.namespaces.copy()
|
||||
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
|
||||
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
|
||||
|
||||
|
||||
self.config = self.__class__.config.copy()
|
||||
if config:
|
||||
self.merge(config)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
|
||||
self.root, self.script_name)
|
||||
|
||||
|
||||
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
|
||||
application; it does not include scheme, host, or proxy ("virtual host")
|
||||
portions of the URI.
|
||||
|
||||
|
||||
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
|
||||
"page1" method on the root object.
|
||||
|
||||
|
||||
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 "/").
|
||||
|
||||
|
||||
If script_name is explicitly set to None, then the script_name will be
|
||||
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
|
||||
"""
|
||||
@@ -88,23 +88,23 @@ class Application(object):
|
||||
self._script_name = value
|
||||
script_name = property(fget=_get_script_name, fset=_set_script_name,
|
||||
doc=script_name_doc)
|
||||
|
||||
|
||||
def merge(self, config):
|
||||
"""Merge the given config into self.config."""
|
||||
_cpconfig.merge(self.config, config)
|
||||
|
||||
|
||||
# Handle namespaces specified in config.
|
||||
self.namespaces(self.config.get("/", {}))
|
||||
|
||||
|
||||
def find_config(self, path, key, default=None):
|
||||
"""Return the most-specific value for key along path, or default."""
|
||||
trail = path or "/"
|
||||
while trail:
|
||||
nodeconf = self.config.get(trail, {})
|
||||
|
||||
|
||||
if key in nodeconf:
|
||||
return nodeconf[key]
|
||||
|
||||
|
||||
lastslash = trail.rfind("/")
|
||||
if lastslash == -1:
|
||||
break
|
||||
@@ -112,78 +112,78 @@ class Application(object):
|
||||
trail = "/"
|
||||
else:
|
||||
trail = trail[:lastslash]
|
||||
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def get_serving(self, local, remote, scheme, sproto):
|
||||
"""Create and return a Request and Response object."""
|
||||
req = self.request_class(local, remote, scheme, sproto)
|
||||
req.app = self
|
||||
|
||||
|
||||
for name, toolbox in self.toolboxes.items():
|
||||
req.namespaces[name] = toolbox
|
||||
|
||||
|
||||
resp = self.response_class()
|
||||
cherrypy.serving.load(req, resp)
|
||||
cherrypy.engine.publish('acquire_thread')
|
||||
cherrypy.engine.publish('before_request')
|
||||
|
||||
|
||||
return req, resp
|
||||
|
||||
|
||||
def release_serving(self):
|
||||
"""Release the current serving (request and response)."""
|
||||
req = cherrypy.serving.request
|
||||
|
||||
|
||||
cherrypy.engine.publish('after_request')
|
||||
|
||||
|
||||
try:
|
||||
req.close()
|
||||
except:
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
|
||||
|
||||
cherrypy.serving.clear()
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgiapp(environ, start_response)
|
||||
|
||||
|
||||
class Tree(object):
|
||||
"""A registry of CherryPy applications, mounted at diverse points.
|
||||
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object), in which case it dispatches to all
|
||||
mounted apps.
|
||||
"""
|
||||
|
||||
|
||||
apps = {}
|
||||
"""
|
||||
A dict of the form {script name: application}, where "script name"
|
||||
is a string declaring the URI mount point (no trailing slash), and
|
||||
"application" is an instance of cherrypy.Application (or an arbitrary
|
||||
WSGI callable if you happen to be using a WSGI server)."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
|
||||
def mount(self, root, script_name="", config=None):
|
||||
"""Mount a new app from a root object, script_name, and config.
|
||||
|
||||
|
||||
root
|
||||
An instance of a "controller class" (a collection of page
|
||||
handler methods) which represents the root of the application.
|
||||
This may also be an Application instance, or None if using
|
||||
a dispatcher other than the default.
|
||||
|
||||
|
||||
script_name
|
||||
A string containing the "mount point" of the application.
|
||||
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()
|
||||
will handle requests to "http://www.example.com:8080/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
|
||||
root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
|
||||
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 "
|
||||
"request). You cannot mount such Applications on this Tree; "
|
||||
"you must pass them to a WSGI server interface directly.")
|
||||
|
||||
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
|
||||
|
||||
if isinstance(root, Application):
|
||||
app = root
|
||||
if script_name != "" and script_name != app.script_name:
|
||||
@@ -206,30 +206,30 @@ class Tree(object):
|
||||
script_name = app.script_name
|
||||
else:
|
||||
app = Application(root, script_name)
|
||||
|
||||
|
||||
# If mounted at "", add favicon.ico
|
||||
if (script_name == "" and root is not None
|
||||
and not hasattr(root, "favicon_ico")):
|
||||
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
|
||||
"favicon.ico")
|
||||
root.favicon_ico = tools.staticfile.handler(favicon)
|
||||
|
||||
|
||||
if config:
|
||||
app.merge(config)
|
||||
|
||||
|
||||
self.apps[script_name] = app
|
||||
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def graft(self, wsgi_callable, script_name=""):
|
||||
"""Mount a wsgi callable at the given script_name."""
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
self.apps[script_name] = wsgi_callable
|
||||
|
||||
|
||||
def script_name(self, path=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:
|
||||
@@ -239,17 +239,17 @@ class Tree(object):
|
||||
request.path_info)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
|
||||
while True:
|
||||
if path in self.apps:
|
||||
return path
|
||||
|
||||
|
||||
if path == "":
|
||||
return None
|
||||
|
||||
|
||||
# Move one node up the tree and try again.
|
||||
path = path[:path.rfind("/")]
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# If you're calling this, then you're probably setting SCRIPT_NAME
|
||||
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
||||
@@ -263,9 +263,9 @@ class Tree(object):
|
||||
if sn is None:
|
||||
start_response('404 Not Found', [])
|
||||
return []
|
||||
|
||||
|
||||
app = self.apps[sn]
|
||||
|
||||
|
||||
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
||||
environ = environ.copy()
|
||||
if not py3k:
|
||||
|
||||
+49
-49
@@ -18,7 +18,7 @@ from cherrypy.lib import httputil
|
||||
def downgrade_wsgi_ux_to_1x(environ):
|
||||
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
|
||||
env1x = {}
|
||||
|
||||
|
||||
url_encoding = environ[ntou('wsgi.url_encoding')]
|
||||
for k, v in list(environ.items()):
|
||||
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):
|
||||
v = v.encode('ISO-8859-1')
|
||||
env1x[k.encode('ISO-8859-1')] = v
|
||||
|
||||
|
||||
return env1x
|
||||
|
||||
|
||||
class VirtualHost(object):
|
||||
"""Select a different WSGI application based on the Host header.
|
||||
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different applications. For example::
|
||||
|
||||
|
||||
root = Root()
|
||||
RootApp = cherrypy.Application(root)
|
||||
Domain2App = cherrypy.Application(root)
|
||||
SecureApp = cherrypy.Application(Secure())
|
||||
|
||||
|
||||
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
|
||||
domains={'www.domain2.example': Domain2App,
|
||||
'www.domain2.example:443': SecureApp,
|
||||
})
|
||||
|
||||
|
||||
cherrypy.tree.graft(vhost)
|
||||
"""
|
||||
default = None
|
||||
"""Required. The default WSGI application."""
|
||||
|
||||
|
||||
use_x_forwarded_host = True
|
||||
"""If True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying."""
|
||||
|
||||
|
||||
domains = {}
|
||||
"""A dict of {host header value: application} pairs.
|
||||
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".
|
||||
In addition, "Host" headers may contain the port number.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, default, domains=None, use_x_forwarded_host=True):
|
||||
self.default = default
|
||||
self.domains = domains or {}
|
||||
self.use_x_forwarded_host = use_x_forwarded_host
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
domain = environ.get('HTTP_HOST', '')
|
||||
if self.use_x_forwarded_host:
|
||||
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
|
||||
|
||||
|
||||
nextapp = self.domains.get(domain)
|
||||
if nextapp is None:
|
||||
nextapp = self.default
|
||||
@@ -83,11 +83,11 @@ class VirtualHost(object):
|
||||
|
||||
class InternalRedirector(object):
|
||||
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
|
||||
|
||||
|
||||
def __init__(self, nextapp, recursive=False):
|
||||
self.nextapp = nextapp
|
||||
self.recursive = recursive
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
redirections = []
|
||||
while True:
|
||||
@@ -99,13 +99,13 @@ class InternalRedirector(object):
|
||||
sn = environ.get('SCRIPT_NAME', '')
|
||||
path = environ.get('PATH_INFO', '')
|
||||
qs = environ.get('QUERY_STRING', '')
|
||||
|
||||
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
old_uri = sn + path
|
||||
if qs:
|
||||
old_uri += "?" + qs
|
||||
redirections.append(old_uri)
|
||||
|
||||
|
||||
if not self.recursive:
|
||||
# Check to see if the new URI has been redirected to already
|
||||
new_uri = sn + ir.path
|
||||
@@ -115,7 +115,7 @@ class InternalRedirector(object):
|
||||
ir.request.close()
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
"same URL twice: %r" % new_uri)
|
||||
|
||||
|
||||
# Munge the environment and try again.
|
||||
environ['REQUEST_METHOD'] = "GET"
|
||||
environ['PATH_INFO'] = ir.path
|
||||
@@ -127,19 +127,19 @@ class InternalRedirector(object):
|
||||
|
||||
class ExceptionTrapper(object):
|
||||
"""WSGI middleware that traps exceptions."""
|
||||
|
||||
|
||||
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
|
||||
self.nextapp = nextapp
|
||||
self.throws = throws
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
|
||||
|
||||
|
||||
class _TrappedResponse(object):
|
||||
|
||||
|
||||
response = iter([])
|
||||
|
||||
|
||||
def __init__(self, nextapp, environ, start_response, throws):
|
||||
self.nextapp = nextapp
|
||||
self.environ = environ
|
||||
@@ -148,22 +148,22 @@ class _TrappedResponse(object):
|
||||
self.started_response = False
|
||||
self.response = self.trap(self.nextapp, self.environ, self.start_response)
|
||||
self.iter_response = iter(self.response)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
self.started_response = True
|
||||
return self
|
||||
|
||||
|
||||
if py3k:
|
||||
def __next__(self):
|
||||
return self.trap(next, self.iter_response)
|
||||
else:
|
||||
def next(self):
|
||||
return self.trap(self.iter_response.next)
|
||||
|
||||
|
||||
def close(self):
|
||||
if hasattr(self.response, 'close'):
|
||||
self.response.close()
|
||||
|
||||
|
||||
def trap(self, func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
@@ -188,7 +188,7 @@ class _TrappedResponse(object):
|
||||
self.iter_response = iter([])
|
||||
else:
|
||||
self.iter_response = iter(b)
|
||||
|
||||
|
||||
try:
|
||||
self.start_response(s, h, _sys.exc_info())
|
||||
except:
|
||||
@@ -199,7 +199,7 @@ class _TrappedResponse(object):
|
||||
# But we still log and call close() to clean up ourselves.
|
||||
_cherrypy.log(traceback=True, severity=40)
|
||||
raise
|
||||
|
||||
|
||||
if self.started_response:
|
||||
return ntob("").join(b)
|
||||
else:
|
||||
@@ -211,7 +211,7 @@ class _TrappedResponse(object):
|
||||
|
||||
class AppResponse(object):
|
||||
"""WSGI response iterable for CherryPy applications."""
|
||||
|
||||
|
||||
def __init__(self, environ, start_response, cpapp):
|
||||
self.cpapp = cpapp
|
||||
try:
|
||||
@@ -226,7 +226,7 @@ class AppResponse(object):
|
||||
outstatus = r.output_status
|
||||
if not isinstance(outstatus, bytestr):
|
||||
raise TypeError("response.output_status is not a byte string.")
|
||||
|
||||
|
||||
outheaders = []
|
||||
for k, v in r.header_list:
|
||||
if not isinstance(k, bytestr):
|
||||
@@ -234,7 +234,7 @@ class AppResponse(object):
|
||||
if not isinstance(v, bytestr):
|
||||
raise TypeError("response.header_list value %r is not a byte string." % v)
|
||||
outheaders.append((k, v))
|
||||
|
||||
|
||||
if py3k:
|
||||
# According to PEP 3333, when using Python 3, the response status
|
||||
# and headers must be bytes masquerading as unicode; that is, they
|
||||
@@ -249,25 +249,25 @@ class AppResponse(object):
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
if py3k:
|
||||
def __next__(self):
|
||||
return next(self.iter_response)
|
||||
else:
|
||||
def next(self):
|
||||
return self.iter_response.next()
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Close and de-reference the current request and response. (Core)"""
|
||||
self.cpapp.release_serving()
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Create a Request object using environ."""
|
||||
env = self.environ.get
|
||||
|
||||
|
||||
local = httputil.Host('', int(env('SERVER_PORT', 80)),
|
||||
env('SERVER_NAME', ''))
|
||||
remote = httputil.Host(env('REMOTE_ADDR', ''),
|
||||
@@ -276,7 +276,7 @@ class AppResponse(object):
|
||||
scheme = env('wsgi.url_scheme')
|
||||
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
|
||||
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
|
||||
|
||||
|
||||
# LOGON_USER is served by IIS, and is the name of the
|
||||
# user after having been mapped to a local account.
|
||||
# Both IIS and Apache set REMOTE_USER, when possible.
|
||||
@@ -285,9 +285,9 @@ class AppResponse(object):
|
||||
request.multiprocess = self.environ['wsgi.multiprocess']
|
||||
request.wsgi_environ = self.environ
|
||||
request.prev = env('cherrypy.previous_request', None)
|
||||
|
||||
|
||||
meth = self.environ['REQUEST_METHOD']
|
||||
|
||||
|
||||
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
|
||||
self.environ.get('PATH_INFO', ''))
|
||||
qs = self.environ.get('QUERY_STRING', '')
|
||||
@@ -313,19 +313,19 @@ class AppResponse(object):
|
||||
# Only set transcoded values if they both succeed.
|
||||
path = u_path
|
||||
qs = u_qs
|
||||
|
||||
|
||||
rproto = self.environ.get('SERVER_PROTOCOL')
|
||||
headers = self.translate_headers(self.environ)
|
||||
rfile = self.environ['wsgi.input']
|
||||
request.run(meth, path, qs, rproto, headers, rfile)
|
||||
|
||||
|
||||
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
|
||||
'CONTENT_LENGTH': 'Content-Length',
|
||||
'CONTENT_TYPE': 'Content-Type',
|
||||
'REMOTE_HOST': 'Remote-Host',
|
||||
'REMOTE_ADDR': 'Remote-Addr',
|
||||
}
|
||||
|
||||
|
||||
def translate_headers(self, environ):
|
||||
"""Translate CGI-environ header names to HTTP header names."""
|
||||
for cgiName in environ:
|
||||
@@ -340,7 +340,7 @@ class AppResponse(object):
|
||||
|
||||
class CPWSGIApp(object):
|
||||
"""A WSGI application object for a CherryPy Application."""
|
||||
|
||||
|
||||
pipeline = [('ExceptionTrapper', ExceptionTrapper),
|
||||
('InternalRedirector', InternalRedirector),
|
||||
]
|
||||
@@ -349,35 +349,35 @@ class CPWSGIApp(object):
|
||||
plus optional keyword arguments, and returns a WSGI application
|
||||
(that takes environ and start_response arguments). The 'name' can
|
||||
be any you choose, and will correspond to keys in self.config."""
|
||||
|
||||
|
||||
head = None
|
||||
"""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
|
||||
this to None again if you change self.pipeline after calling self."""
|
||||
|
||||
|
||||
config = {}
|
||||
"""A dict whose keys match names listed in the pipeline. Each
|
||||
value is a further dict which will be passed to the corresponding
|
||||
named WSGI callable (from the pipeline) as keyword arguments."""
|
||||
|
||||
|
||||
response_class = AppResponse
|
||||
"""The class to instantiate and return as the next app in the WSGI chain."""
|
||||
|
||||
|
||||
def __init__(self, cpapp, pipeline=None):
|
||||
self.cpapp = cpapp
|
||||
self.pipeline = self.pipeline[:]
|
||||
if pipeline:
|
||||
self.pipeline.extend(pipeline)
|
||||
self.config = self.config.copy()
|
||||
|
||||
|
||||
def tail(self, environ, start_response):
|
||||
"""WSGI application callable for the actual CherryPy application.
|
||||
|
||||
|
||||
You probably shouldn't call this; call self.__call__ instead,
|
||||
so that any WSGI middleware in self.pipeline can run first.
|
||||
"""
|
||||
return self.response_class(environ, start_response, self.cpapp)
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
head = self.head
|
||||
if head is None:
|
||||
@@ -389,7 +389,7 @@ class CPWSGIApp(object):
|
||||
head = callable(head, **conf)
|
||||
self.head = head
|
||||
return head(environ, start_response)
|
||||
|
||||
|
||||
def namespace_handler(self, k, v):
|
||||
"""Config handler for the 'wsgi' namespace."""
|
||||
if k == "pipeline":
|
||||
|
||||
@@ -9,22 +9,22 @@ from cherrypy import wsgiserver
|
||||
|
||||
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
|
||||
"""Wrapper for wsgiserver.CherryPyWSGIServer.
|
||||
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
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
|
||||
and apply some attributes from config -> cherrypy.server -> wsgiserver.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
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
|
||||
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
|
||||
self.wsgi_version = self.server_adapter.wsgi_version
|
||||
s = wsgiserver.CherryPyWSGIServer
|
||||
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_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
||||
|
||||
|
||||
self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', 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):
|
||||
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
|
||||
|
||||
|
||||
def __init__(self, input, chunkSize=65536):
|
||||
self.input = input
|
||||
self.chunkSize = chunkSize
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __next__(self):
|
||||
chunk = self.input.read(self.chunkSize)
|
||||
if chunk:
|
||||
|
||||
+16
-16
@@ -10,18 +10,18 @@ def check_auth(users, encrypt=None, realm=None):
|
||||
ah = httpauth.parseAuthorization(request.headers['authorization'])
|
||||
if ah is None:
|
||||
raise cherrypy.HTTPError(400, 'Bad Request')
|
||||
|
||||
|
||||
if not encrypt:
|
||||
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
|
||||
|
||||
|
||||
if hasattr(users, '__call__'):
|
||||
try:
|
||||
# backward compatibility
|
||||
users = users() # expect it to return a dictionary
|
||||
|
||||
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError("Authentication users must be a dictionary")
|
||||
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah["username"], None)
|
||||
except TypeError:
|
||||
@@ -30,47 +30,47 @@ def check_auth(users, encrypt=None, realm=None):
|
||||
else:
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError("Authentication users must be a dictionary")
|
||||
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah["username"], None)
|
||||
|
||||
|
||||
# validate the authorization by re-computing it here
|
||||
# and compare it with what the user-agent provided
|
||||
if httpauth.checkResponse(ah, password, method=request.method,
|
||||
encrypt=encrypt, realm=realm):
|
||||
request.login = ah["username"]
|
||||
return True
|
||||
|
||||
|
||||
request.login = False
|
||||
return False
|
||||
|
||||
def basic_auth(realm, users, encrypt=None, debug=False):
|
||||
"""If auth fails, raise 401 with a basic authentication header.
|
||||
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
|
||||
|
||||
users
|
||||
A dict of the form: {username: password} or a callable returning a dict.
|
||||
|
||||
|
||||
encrypt
|
||||
callable used to encrypt the password returned from the user-agent.
|
||||
if None it defaults to a md5 encryption.
|
||||
|
||||
|
||||
"""
|
||||
if check_auth(users, encrypt):
|
||||
if debug:
|
||||
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
|
||||
return
|
||||
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
|
||||
|
||||
|
||||
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
|
||||
|
||||
def digest_auth(realm, users, debug=False):
|
||||
"""If auth fails, raise 401 with a digest authentication header.
|
||||
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
users
|
||||
@@ -80,8 +80,8 @@ def digest_auth(realm, users, debug=False):
|
||||
if debug:
|
||||
cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
|
||||
return
|
||||
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
|
||||
|
||||
|
||||
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
|
||||
'authorization' header. If authentication succeeds, checkpassword
|
||||
returns True, else it returns False.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
if '"' in realm:
|
||||
raise ValueError('Realm cannot contain the " (quote) character.')
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
auth_header = request.headers.get('authorization')
|
||||
if auth_header is not None:
|
||||
try:
|
||||
@@ -80,7 +80,7 @@ def basic_auth(realm, checkpassword, debug=False):
|
||||
return # successful authentication
|
||||
except (ValueError, binascii.Error): # split() error, base64.decodestring() error
|
||||
raise cherrypy.HTTPError(400, 'Bad Request')
|
||||
|
||||
|
||||
# Respond with 401 status and a WWW-Authenticate header
|
||||
cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
|
||||
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
|
||||
|
||||
@@ -107,10 +107,10 @@ def synthesize_nonce(s, key, timestamp=None):
|
||||
|
||||
key
|
||||
A secret string known only to the server.
|
||||
|
||||
|
||||
timestamp
|
||||
An integer seconds-since-the-epoch timestamp
|
||||
|
||||
|
||||
"""
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
@@ -190,10 +190,10 @@ class HttpDigestAuthorization (object):
|
||||
|
||||
s
|
||||
A string related to the resource, such as the hostname of the server.
|
||||
|
||||
|
||||
key
|
||||
A secret string known only to the server.
|
||||
|
||||
|
||||
Both s and key must be the same values which were used to synthesize the nonce
|
||||
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
|
||||
has the Authorization header. Typically GET requests don't have an entity,
|
||||
and POST requests do.
|
||||
|
||||
|
||||
"""
|
||||
ha2 = self.HA2(entity_body)
|
||||
# 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):
|
||||
"""A CherryPy tool which hooks at before_handler to perform
|
||||
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
|
||||
|
||||
|
||||
If the request has an 'authorization' header with a 'Digest' scheme, this
|
||||
tool authenticates the credentials supplied in that header. If
|
||||
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
|
||||
a 'WWW-Authenticate' Digest header.
|
||||
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
|
||||
|
||||
get_ha1
|
||||
A callable which looks up a username in a credentials store
|
||||
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.
|
||||
If username is not found in the credentials store, get_ha1() returns
|
||||
None.
|
||||
|
||||
|
||||
key
|
||||
A secret string known only to the server, used in the synthesis of nonces.
|
||||
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
auth_header = request.headers.get('authorization')
|
||||
nonce_is_stale = False
|
||||
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)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
|
||||
|
||||
|
||||
if debug:
|
||||
TRACE(str(auth))
|
||||
|
||||
|
||||
if auth.validate_nonce(realm, key):
|
||||
ha1 = get_ha1(realm, auth.username)
|
||||
if ha1 is not None:
|
||||
@@ -355,7 +355,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
|
||||
if debug:
|
||||
TRACE("authentication of %s successful" % auth.username)
|
||||
return
|
||||
|
||||
|
||||
# Respond with 401 status and a WWW-Authenticate header
|
||||
header = www_authenticate(realm, key, stale=nonce_is_stale)
|
||||
if debug:
|
||||
|
||||
+59
-59
@@ -44,19 +44,19 @@ from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
|
||||
|
||||
class Cache(object):
|
||||
"""Base class for Cache implementations."""
|
||||
|
||||
|
||||
def get(self):
|
||||
"""Return the current variant if in the cache, else None."""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def put(self, obj, size):
|
||||
"""Store the current variant in the cache."""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def delete(self):
|
||||
"""Remove ALL cached variants of the current resource."""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Reset the cache to its initial, empty state."""
|
||||
raise NotImplemented
|
||||
@@ -68,16 +68,16 @@ class Cache(object):
|
||||
|
||||
class AntiStampedeCache(dict):
|
||||
"""A storage system for cached items which reduces stampede collisions."""
|
||||
|
||||
|
||||
def wait(self, key, timeout=5, debug=False):
|
||||
"""Return the cached value for the given key, or None.
|
||||
|
||||
|
||||
If timeout is not None, and the value is already
|
||||
being calculated by another thread, wait until the given timeout has
|
||||
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
|
||||
to signal other threads to wait.
|
||||
|
||||
|
||||
If timeout is None, no waiting is performed nor sentinels used.
|
||||
"""
|
||||
value = self.get(key)
|
||||
@@ -87,7 +87,7 @@ class AntiStampedeCache(dict):
|
||||
if debug:
|
||||
cherrypy.log('No timeout', 'TOOLS.CACHING')
|
||||
return None
|
||||
|
||||
|
||||
# Wait until it's done or times out.
|
||||
if debug:
|
||||
cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
|
||||
@@ -104,7 +104,7 @@ class AntiStampedeCache(dict):
|
||||
e = threading.Event()
|
||||
e.result = None
|
||||
dict.__setitem__(self, key, e)
|
||||
|
||||
|
||||
return None
|
||||
elif value is None:
|
||||
# Stick an Event in the slot so other threads wait
|
||||
@@ -115,7 +115,7 @@ class AntiStampedeCache(dict):
|
||||
e.result = None
|
||||
dict.__setitem__(self, key, e)
|
||||
return value
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set the cached value for the given key."""
|
||||
existing = self.get(key)
|
||||
@@ -129,48 +129,48 @@ class AntiStampedeCache(dict):
|
||||
|
||||
class MemoryCache(Cache):
|
||||
"""An in-memory cache for varying response content.
|
||||
|
||||
|
||||
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
|
||||
"selecting request headers"; that is, those named in the Vary
|
||||
response header. We assume the list of header names to be constant
|
||||
for each URI throughout the lifetime of the application, and store
|
||||
that list in ``self.store[uri].selecting_headers``.
|
||||
|
||||
|
||||
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
|
||||
selecting_headers), and values which are the actual responses.
|
||||
"""
|
||||
|
||||
|
||||
maxobjects = 1000
|
||||
"""The maximum number of cached objects; defaults to 1000."""
|
||||
|
||||
|
||||
maxobj_size = 100000
|
||||
"""The maximum size of each cached object in bytes; defaults to 100 KB."""
|
||||
|
||||
|
||||
maxsize = 10000000
|
||||
"""The maximum size of the entire cache in bytes; defaults to 10 MB."""
|
||||
|
||||
|
||||
delay = 600
|
||||
"""Seconds until the cached content expires; defaults to 600 (10 minutes)."""
|
||||
|
||||
|
||||
antistampede_timeout = 5
|
||||
"""Seconds to wait for other threads to release a cache lock."""
|
||||
|
||||
|
||||
expire_freq = 0.1
|
||||
"""Seconds to sleep between cache expiration sweeps."""
|
||||
|
||||
|
||||
debug = False
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
|
||||
# Run self.expire_cache in a separate daemon thread.
|
||||
t = threading.Thread(target=self.expire_cache, name='expire_cache')
|
||||
self.expiration_thread = t
|
||||
set_daemon(t, True)
|
||||
t.start()
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Reset the cache to its initial, empty state."""
|
||||
self.store = {}
|
||||
@@ -181,10 +181,10 @@ class MemoryCache(Cache):
|
||||
self.tot_expires = 0
|
||||
self.tot_non_modified = 0
|
||||
self.cursize = 0
|
||||
|
||||
|
||||
def expire_cache(self):
|
||||
"""Continuously examine cached objects, expiring stale ones.
|
||||
|
||||
|
||||
This function is designed to be run in its own daemon thread,
|
||||
referenced at ``self.expiration_thread``.
|
||||
"""
|
||||
@@ -207,17 +207,17 @@ class MemoryCache(Cache):
|
||||
pass
|
||||
del self.expirations[expiration_time]
|
||||
time.sleep(self.expire_freq)
|
||||
|
||||
|
||||
def get(self):
|
||||
"""Return the current variant if in the cache, else None."""
|
||||
request = cherrypy.serving.request
|
||||
self.tot_gets += 1
|
||||
|
||||
|
||||
uri = cherrypy.url(qs=request.query_string)
|
||||
uricache = self.store.get(uri)
|
||||
if uricache is None:
|
||||
return None
|
||||
|
||||
|
||||
header_values = [request.headers.get(h, '')
|
||||
for h in uricache.selecting_headers]
|
||||
variant = uricache.wait(key=tuple(sorted(header_values)),
|
||||
@@ -226,12 +226,12 @@ class MemoryCache(Cache):
|
||||
if variant is not None:
|
||||
self.tot_hist += 1
|
||||
return variant
|
||||
|
||||
|
||||
def put(self, variant, size):
|
||||
"""Store the current variant in the cache."""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
uri = cherrypy.url(qs=request.query_string)
|
||||
uricache = self.store.get(uri)
|
||||
if uricache is None:
|
||||
@@ -239,24 +239,24 @@ class MemoryCache(Cache):
|
||||
uricache.selecting_headers = [
|
||||
e.value for e in response.headers.elements('Vary')]
|
||||
self.store[uri] = uricache
|
||||
|
||||
|
||||
if len(self.store) < self.maxobjects:
|
||||
total_size = self.cursize + size
|
||||
|
||||
|
||||
# checks if there's space for the object
|
||||
if (size < self.maxobj_size and total_size < self.maxsize):
|
||||
# add to the expirations list
|
||||
expiration_time = response.time + self.delay
|
||||
bucket = self.expirations.setdefault(expiration_time, [])
|
||||
bucket.append((size, uri, uricache.selecting_headers))
|
||||
|
||||
|
||||
# add to the cache
|
||||
header_values = [request.headers.get(h, '')
|
||||
for h in uricache.selecting_headers]
|
||||
uricache[tuple(sorted(header_values))] = variant
|
||||
self.tot_puts += 1
|
||||
self.cursize = total_size
|
||||
|
||||
|
||||
def delete(self):
|
||||
"""Remove ALL cached variants of the current resource."""
|
||||
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):
|
||||
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
|
||||
|
||||
|
||||
If POST, PUT, or DELETE:
|
||||
* invalidates (deletes) any cached response for this resource
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = False
|
||||
|
||||
|
||||
else if a cached copy exists:
|
||||
* sets request.cached = True
|
||||
* sets request.cacheable = False
|
||||
@@ -280,7 +280,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
||||
if necessary.
|
||||
* sets response.status and response.body to the cached values
|
||||
* returns True
|
||||
|
||||
|
||||
otherwise:
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = True
|
||||
@@ -288,16 +288,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
if not hasattr(cherrypy, "_cache"):
|
||||
# Make a process-wide Cache object.
|
||||
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
|
||||
|
||||
|
||||
# Take all remaining kwargs and set them on the Cache object.
|
||||
for k, v in kwargs.items():
|
||||
setattr(cherrypy._cache, k, v)
|
||||
cherrypy._cache.debug = debug
|
||||
|
||||
|
||||
# POST, PUT, DELETE should invalidate (delete) the cached copy.
|
||||
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
|
||||
if request.method in invalid_methods:
|
||||
@@ -308,12 +308,12 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
||||
request.cached = False
|
||||
request.cacheable = False
|
||||
return False
|
||||
|
||||
|
||||
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
|
||||
cache_data = cherrypy._cache.get()
|
||||
request.cached = bool(cache_data)
|
||||
request.cacheable = not request.cached
|
||||
@@ -335,7 +335,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
|
||||
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.cacheable = True
|
||||
return False
|
||||
|
||||
|
||||
# Copy the response headers. See http://www.cherrypy.org/ticket/721.
|
||||
response.headers = rh = httputil.HeaderMap()
|
||||
for k in h:
|
||||
dict.__setitem__(rh, k, dict.__getitem__(h, k))
|
||||
|
||||
|
||||
# Add the required Age header
|
||||
response.headers["Age"] = str(age)
|
||||
|
||||
|
||||
try:
|
||||
# Note that validate_since depends on a Last-Modified header;
|
||||
# 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:
|
||||
cherrypy._cache.tot_non_modified += 1
|
||||
raise
|
||||
|
||||
|
||||
# serve it & get out from the request
|
||||
response.status = s
|
||||
response.body = b
|
||||
@@ -379,11 +379,11 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
|
||||
def tee_output():
|
||||
"""Tee response output to cache storage. Internal."""
|
||||
# Used by CachingTool by attaching to request.hooks
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
if 'no-store' in request.headers.values('Cache-Control'):
|
||||
return
|
||||
|
||||
|
||||
def tee(body):
|
||||
"""Tee response.body into a list."""
|
||||
if ('no-cache' in response.headers.values('Pragma') or
|
||||
@@ -391,17 +391,17 @@ def tee_output():
|
||||
for chunk in body:
|
||||
yield chunk
|
||||
return
|
||||
|
||||
|
||||
output = []
|
||||
for chunk in body:
|
||||
output.append(chunk)
|
||||
yield chunk
|
||||
|
||||
|
||||
# save the cache data
|
||||
body = ntob('').join(output)
|
||||
cherrypy._cache.put((response.status, response.headers or {},
|
||||
body, response.time), len(body))
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
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.
|
||||
If secs is zero, the 'Expires' header is set one year in the past, and
|
||||
the following "cache prevention" headers are also set:
|
||||
|
||||
|
||||
* Pragma: no-cache
|
||||
* Cache-Control': no-cache, must-revalidate
|
||||
|
||||
force
|
||||
If False, the following headers are checked:
|
||||
|
||||
|
||||
* Etag
|
||||
* Last-Modified
|
||||
* Age
|
||||
* Expires
|
||||
|
||||
|
||||
If any are already present, none of the above response headers are set.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
headers = response.headers
|
||||
|
||||
|
||||
cacheable = False
|
||||
if not force:
|
||||
# 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:
|
||||
cacheable = True
|
||||
break
|
||||
|
||||
|
||||
if not cacheable and not force:
|
||||
if debug:
|
||||
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')
|
||||
if isinstance(secs, datetime.timedelta):
|
||||
secs = (86400 * secs.days) + secs.seconds
|
||||
|
||||
|
||||
if secs == 0:
|
||||
if force or ("Pragma" not in headers):
|
||||
headers["Pragma"] = "no-cache"
|
||||
|
||||
+25
-25
@@ -1,7 +1,7 @@
|
||||
"""Code-coverage tools for CherryPy.
|
||||
|
||||
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/>`_
|
||||
or Ned Batchelder's `enhanced version:
|
||||
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
|
||||
@@ -37,10 +37,10 @@ except ImportError:
|
||||
# Setting the_coverage to None will raise errors
|
||||
# that need to be trapped downstream.
|
||||
the_coverage = None
|
||||
|
||||
|
||||
import warnings
|
||||
warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
|
||||
|
||||
|
||||
def start():
|
||||
pass
|
||||
start.priority = 20
|
||||
@@ -69,7 +69,7 @@ TEMPLATE_MENU = """<html>
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
input { border: 1px solid #ccc; padding: 2px; }
|
||||
.directory {
|
||||
@@ -126,7 +126,7 @@ TEMPLATE_FORM = """
|
||||
|
||||
<input type='submit' value='Change view' id="submit"/>
|
||||
</form>
|
||||
</div>"""
|
||||
</div>"""
|
||||
|
||||
TEMPLATE_FRAMESET = """<html>
|
||||
<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="",
|
||||
coverage=the_coverage):
|
||||
|
||||
|
||||
# Show the directory name and any of our children
|
||||
dirs = [k for k, v in root.items() if v]
|
||||
dirs.sort()
|
||||
for name in dirs:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
|
||||
if newpath.lower().startswith(base):
|
||||
relpath = newpath[len(base):]
|
||||
yield "| " * relpath.count(os.sep)
|
||||
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
|
||||
(newpath, quote_plus(exclude), name)
|
||||
|
||||
|
||||
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
|
||||
yield chunk
|
||||
|
||||
|
||||
# Now list the files
|
||||
if path.lower().startswith(base):
|
||||
relpath = path[len(base):]
|
||||
@@ -207,7 +207,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
|
||||
files.sort()
|
||||
for name in files:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
|
||||
pc_str = ""
|
||||
if showpct:
|
||||
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
|
||||
else:
|
||||
pc_str = "<span class='pass'>%s</span>" % pc_str
|
||||
|
||||
|
||||
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
|
||||
pc_str, newpath, name)
|
||||
|
||||
@@ -232,7 +232,7 @@ def _skip_file(path, exclude):
|
||||
|
||||
def _graft(path, tree):
|
||||
d = tree
|
||||
|
||||
|
||||
p = path
|
||||
atoms = []
|
||||
while True:
|
||||
@@ -243,7 +243,7 @@ def _graft(path, tree):
|
||||
atoms.append(p)
|
||||
if p != "/":
|
||||
atoms.append("/")
|
||||
|
||||
|
||||
atoms.reverse()
|
||||
for node in atoms:
|
||||
if node:
|
||||
@@ -259,7 +259,7 @@ def get_tree(base, exclude, coverage=the_coverage):
|
||||
return tree
|
||||
|
||||
class CoverStats(object):
|
||||
|
||||
|
||||
def __init__(self, coverage, root=None):
|
||||
self.coverage = coverage
|
||||
if root is None:
|
||||
@@ -268,20 +268,20 @@ class CoverStats(object):
|
||||
import cherrypy
|
||||
root = os.path.dirname(cherrypy.__file__)
|
||||
self.root = root
|
||||
|
||||
|
||||
def index(self):
|
||||
return TEMPLATE_FRAMESET % self.root.lower()
|
||||
index.exposed = True
|
||||
|
||||
|
||||
def menu(self, base="/", pct="50", showpct="",
|
||||
exclude=r'python\d\.\d|test|tut\d|tutorial'):
|
||||
|
||||
|
||||
# The coverage module uses all-lower-case names.
|
||||
base = base.lower().rstrip(os.sep)
|
||||
|
||||
|
||||
yield TEMPLATE_MENU
|
||||
yield TEMPLATE_FORM % locals()
|
||||
|
||||
|
||||
# Start by showing links for parent paths
|
||||
yield "<div id='crumbs'>"
|
||||
path = ""
|
||||
@@ -292,9 +292,9 @@ class CoverStats(object):
|
||||
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
|
||||
% (path, quote_plus(exclude), atom, os.sep))
|
||||
yield "</div>"
|
||||
|
||||
|
||||
yield "<div id='tree'>"
|
||||
|
||||
|
||||
# Then display the tree
|
||||
tree = get_tree(base, exclude, self.coverage)
|
||||
if not tree:
|
||||
@@ -303,11 +303,11 @@ class CoverStats(object):
|
||||
for chunk in _show_branch(tree, base, "/", pct,
|
||||
showpct=='checked', exclude, coverage=self.coverage):
|
||||
yield chunk
|
||||
|
||||
|
||||
yield "</div>"
|
||||
yield "</body></html>"
|
||||
menu.exposed = True
|
||||
|
||||
|
||||
def annotated_file(self, filename, statements, excluded, missing):
|
||||
source = open(filename, 'r')
|
||||
buffer = []
|
||||
@@ -329,7 +329,7 @@ class CoverStats(object):
|
||||
yield template % (lno, cgi.escape(pastline))
|
||||
buffer = []
|
||||
yield template % (lineno, cgi.escape(line))
|
||||
|
||||
|
||||
def report(self, name):
|
||||
filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
|
||||
pc = _percent(statements, missing)
|
||||
@@ -352,7 +352,7 @@ def serve(path=localFile, port=8080, root=None):
|
||||
from coverage import coverage
|
||||
cov = coverage(data_file = path)
|
||||
cov.load()
|
||||
|
||||
|
||||
import cherrypy
|
||||
cherrypy.config.update({'server.socket_port': int(port),
|
||||
'server.thread_pool': 10,
|
||||
|
||||
+35
-35
@@ -235,21 +235,21 @@ proc_time = lambda s: time.time() - s['Start Time']
|
||||
|
||||
class ByteCountWrapper(object):
|
||||
"""Wraps a file-like object, counting the number of bytes read."""
|
||||
|
||||
|
||||
def __init__(self, rfile):
|
||||
self.rfile = rfile
|
||||
self.bytes_read = 0
|
||||
|
||||
|
||||
def read(self, size=-1):
|
||||
data = self.rfile.read(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
|
||||
def readline(self, size=-1):
|
||||
data = self.rfile.readline(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
|
||||
def readlines(self, sizehint=0):
|
||||
# Shamelessly stolen from StringIO
|
||||
total = 0
|
||||
@@ -262,13 +262,13 @@ class ByteCountWrapper(object):
|
||||
break
|
||||
line = self.readline()
|
||||
return lines
|
||||
|
||||
|
||||
def close(self):
|
||||
self.rfile.close()
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def next(self):
|
||||
data = self.rfile.next()
|
||||
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):
|
||||
"""Record various information about the current request."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
if appstats.get('Enabled', False):
|
||||
cherrypy.Tool._setup(self)
|
||||
self.record_start()
|
||||
|
||||
|
||||
def record_start(self):
|
||||
"""Record the beginning of a request."""
|
||||
request = cherrypy.serving.request
|
||||
if not hasattr(request.rfile, 'bytes_read'):
|
||||
request.rfile = ByteCountWrapper(request.rfile)
|
||||
request.body.fp = request.rfile
|
||||
|
||||
|
||||
r = request.remote
|
||||
|
||||
|
||||
appstats['Current Requests'] += 1
|
||||
appstats['Total Requests'] += 1
|
||||
appstats['Requests'][threading._get_ident()] = {
|
||||
@@ -322,30 +322,30 @@ class StatsTool(cherrypy.Tool):
|
||||
"""Record the end of a request."""
|
||||
resp = cherrypy.serving.response
|
||||
w = appstats['Requests'][threading._get_ident()]
|
||||
|
||||
|
||||
r = cherrypy.request.rfile.bytes_read
|
||||
w['Bytes Read'] = r
|
||||
appstats['Total Bytes Read'] += r
|
||||
|
||||
|
||||
if resp.stream:
|
||||
w['Bytes Written'] = 'chunked'
|
||||
else:
|
||||
cl = int(resp.headers.get('Content-Length', 0))
|
||||
w['Bytes Written'] = cl
|
||||
appstats['Total Bytes Written'] += cl
|
||||
|
||||
|
||||
w['Response Status'] = getattr(resp, 'output_status', None) or resp.status
|
||||
|
||||
|
||||
w['End Time'] = time.time()
|
||||
p = w['End Time'] - w['Start Time']
|
||||
w['Processing Time'] = p
|
||||
appstats['Total Time'] += p
|
||||
|
||||
|
||||
appstats['Current Requests'] -= 1
|
||||
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
|
||||
|
||||
|
||||
if uriset:
|
||||
rs = appstats.setdefault('URI Set Tracking', {})
|
||||
r = rs.setdefault(uriset, {
|
||||
@@ -357,7 +357,7 @@ class StatsTool(cherrypy.Tool):
|
||||
r['Max'] = p
|
||||
r['Count'] += 1
|
||||
r['Sum'] += p
|
||||
|
||||
|
||||
if slow_queries and p > slow_queries:
|
||||
sq = appstats.setdefault('Slow Queries', [])
|
||||
sq.append(w.copy())
|
||||
@@ -410,7 +410,7 @@ def pause_resume(ns):
|
||||
|
||||
|
||||
class StatsPage(object):
|
||||
|
||||
|
||||
formatting = {
|
||||
'CherryPy Applications': {
|
||||
'Enabled': pause_resume('CherryPy Applications'),
|
||||
@@ -448,8 +448,8 @@ class StatsPage(object):
|
||||
'Start time': iso_format,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def index(self):
|
||||
# Transform the raw data into pretty output for HTML
|
||||
yield """
|
||||
@@ -506,7 +506,7 @@ table.stats2 th {
|
||||
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
|
||||
if colnum == 2: yield """
|
||||
</tr>"""
|
||||
|
||||
|
||||
if colnum == 0: yield """
|
||||
<th></th><td></td>
|
||||
<th></th><td></td>
|
||||
@@ -547,7 +547,7 @@ table.stats2 th {
|
||||
</html>
|
||||
"""
|
||||
index.exposed = True
|
||||
|
||||
|
||||
def get_namespaces(self):
|
||||
"""Yield (title, scalars, collections) for each namespace."""
|
||||
s = extrapolate_statistics(logging.statistics)
|
||||
@@ -574,7 +574,7 @@ table.stats2 th {
|
||||
v = format % v
|
||||
scalars.append((k, v))
|
||||
yield title, scalars, collections
|
||||
|
||||
|
||||
def get_dict_collection(self, v, formatting):
|
||||
"""Return ([headers], [rows]) for the given collection."""
|
||||
# E.g., the 'Requests' dict.
|
||||
@@ -588,7 +588,7 @@ table.stats2 th {
|
||||
if k3 not in headers:
|
||||
headers.append(k3)
|
||||
headers.sort()
|
||||
|
||||
|
||||
subrows = []
|
||||
for k2, record in sorted(v.items()):
|
||||
subrow = [k2]
|
||||
@@ -604,9 +604,9 @@ table.stats2 th {
|
||||
v3 = format % v3
|
||||
subrow.append(v3)
|
||||
subrows.append(subrow)
|
||||
|
||||
|
||||
return headers, subrows
|
||||
|
||||
|
||||
def get_list_collection(self, v, formatting):
|
||||
"""Return ([headers], [subrows]) for the given collection."""
|
||||
# E.g., the 'Slow Queries' list.
|
||||
@@ -620,7 +620,7 @@ table.stats2 th {
|
||||
if k3 not in headers:
|
||||
headers.append(k3)
|
||||
headers.sort()
|
||||
|
||||
|
||||
subrows = []
|
||||
for record in v:
|
||||
subrow = []
|
||||
@@ -636,23 +636,23 @@ table.stats2 th {
|
||||
v3 = format % v3
|
||||
subrow.append(v3)
|
||||
subrows.append(subrow)
|
||||
|
||||
|
||||
return headers, subrows
|
||||
|
||||
|
||||
if json is not None:
|
||||
def data(self):
|
||||
s = extrapolate_statistics(logging.statistics)
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||
return json.dumps(s, sort_keys=True, indent=4)
|
||||
data.exposed = True
|
||||
|
||||
|
||||
def pause(self, namespace):
|
||||
logging.statistics.get(namespace, {})['Enabled'] = False
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
pause.exposed = True
|
||||
pause.cp_config = {'tools.allow.on': True,
|
||||
'tools.allow.methods': ['POST']}
|
||||
|
||||
|
||||
def resume(self, namespace):
|
||||
logging.statistics.get(namespace, {})['Enabled'] = True
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
|
||||
+64
-64
@@ -12,12 +12,12 @@ from cherrypy.lib import httputil as _httputil
|
||||
|
||||
def validate_etags(autotags=False, debug=False):
|
||||
"""Validate the current ETag against If-Match, If-None-Match headers.
|
||||
|
||||
|
||||
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
|
||||
already provided an ETag header). If False (the default), the ETag
|
||||
will not be automatic.
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# Guard against being run twice.
|
||||
if hasattr(response, "ETag"):
|
||||
return
|
||||
|
||||
|
||||
status, reason, msg = _httputil.valid_status(response.status)
|
||||
|
||||
|
||||
etag = response.headers.get('ETag')
|
||||
|
||||
|
||||
# Automatic ETag generation. See warning in docstring.
|
||||
if etag:
|
||||
if debug:
|
||||
@@ -52,9 +52,9 @@ def validate_etags(autotags=False, debug=False):
|
||||
if debug:
|
||||
cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
|
||||
response.headers['ETag'] = etag
|
||||
|
||||
|
||||
response.ETag = etag
|
||||
|
||||
|
||||
# "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
|
||||
# MUST be ignored."
|
||||
@@ -62,7 +62,7 @@ def validate_etags(autotags=False, debug=False):
|
||||
cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
|
||||
if status >= 200 and status <= 299:
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
conditions = request.headers.elements('If-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if debug:
|
||||
@@ -71,7 +71,7 @@ def validate_etags(autotags=False, debug=False):
|
||||
if conditions and not (conditions == ["*"] or etag in conditions):
|
||||
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
|
||||
"not match %r" % (etag, conditions))
|
||||
|
||||
|
||||
conditions = request.headers.elements('If-None-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if debug:
|
||||
@@ -88,7 +88,7 @@ def validate_etags(autotags=False, debug=False):
|
||||
|
||||
def validate_since():
|
||||
"""Validate the current Last-Modified against If-Modified-Since headers.
|
||||
|
||||
|
||||
If no code has set the Last-Modified response header, then no validation
|
||||
will be performed.
|
||||
"""
|
||||
@@ -96,14 +96,14 @@ def validate_since():
|
||||
lastmod = response.headers.get('Last-Modified')
|
||||
if lastmod:
|
||||
status, reason, msg = _httputil.valid_status(response.status)
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
since = request.headers.get('If-Unmodified-Since')
|
||||
if since and since != lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 412:
|
||||
raise cherrypy.HTTPError(412)
|
||||
|
||||
|
||||
since = request.headers.get('If-Modified-Since')
|
||||
if since and since == lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 304:
|
||||
@@ -117,11 +117,11 @@ def validate_since():
|
||||
|
||||
def allow(methods=None, debug=False):
|
||||
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
|
||||
|
||||
|
||||
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 more than one, supply a list of strings.
|
||||
|
||||
|
||||
Regardless of whether the current method is allowed or not, this
|
||||
also emits an 'Allow' response header, containing the given methods.
|
||||
"""
|
||||
@@ -132,7 +132,7 @@ def allow(methods=None, debug=False):
|
||||
methods = ['GET', 'HEAD']
|
||||
elif 'GET' in methods and 'HEAD' not in methods:
|
||||
methods.append('HEAD')
|
||||
|
||||
|
||||
cherrypy.response.headers['Allow'] = ', '.join(methods)
|
||||
if cherrypy.request.method not in methods:
|
||||
if debug:
|
||||
@@ -148,27 +148,27 @@ def allow(methods=None, debug=False):
|
||||
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
scheme='X-Forwarded-Proto', debug=False):
|
||||
"""Change the base URL (scheme://host[:port][/path]).
|
||||
|
||||
|
||||
For running a CP server behind Apache, lighttpd, or other HTTP server.
|
||||
|
||||
|
||||
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
|
||||
tools.proxy.local = 'Origin'.
|
||||
|
||||
|
||||
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'
|
||||
to '', so that the X-Forwarded-Host request header (which never includes
|
||||
path info) does not override it. Regardless, the value for 'base' MUST
|
||||
NOT end in a slash.
|
||||
|
||||
|
||||
cherrypy.request.remote.ip (the IP address of the client) will be
|
||||
rewritten if the header specified by the 'remote' arg is valid.
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
if scheme:
|
||||
s = request.headers.get(scheme, None)
|
||||
if debug:
|
||||
@@ -181,7 +181,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
scheme = s
|
||||
if not scheme:
|
||||
scheme = request.base[:request.base.find("://")]
|
||||
|
||||
|
||||
if local:
|
||||
lbase = request.headers.get(local, None)
|
||||
if debug:
|
||||
@@ -194,13 +194,13 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
base = '127.0.0.1'
|
||||
else:
|
||||
base = '127.0.0.1:%s' % port
|
||||
|
||||
|
||||
if base.find("://") == -1:
|
||||
# add http:// or https:// if needed
|
||||
base = scheme + "://" + base
|
||||
|
||||
|
||||
request.base = base
|
||||
|
||||
|
||||
if remote:
|
||||
xff = request.headers.get(remote)
|
||||
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):
|
||||
"""Delete request headers whose field names are included in 'headers'.
|
||||
|
||||
|
||||
This is a useful tool for working behind certain HTTP servers;
|
||||
for example, Apache duplicates the work that CP does for 'Range'
|
||||
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,
|
||||
message='Forbidden Referer header.', debug=False):
|
||||
"""Raise HTTPError if Referer header does/does not match the given pattern.
|
||||
|
||||
|
||||
pattern
|
||||
A regular expression pattern to test against the Referer.
|
||||
|
||||
|
||||
accept
|
||||
If True, the Referer must match the pattern; if False,
|
||||
the Referer must NOT match the pattern.
|
||||
@@ -254,10 +254,10 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
|
||||
|
||||
error
|
||||
The HTTP error code to return to the client on failure.
|
||||
|
||||
|
||||
message
|
||||
A string to include in the response body on failure.
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
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')
|
||||
if accept_missing:
|
||||
return
|
||||
|
||||
|
||||
raise cherrypy.HTTPError(error, message)
|
||||
|
||||
|
||||
class SessionAuth(object):
|
||||
"""Assert that the user is logged in."""
|
||||
|
||||
|
||||
session_key = "username"
|
||||
debug = False
|
||||
|
||||
|
||||
def check_username_and_password(self, username, password):
|
||||
pass
|
||||
|
||||
|
||||
def anonymous(self):
|
||||
"""Provide a temporary user name for anonymous users."""
|
||||
pass
|
||||
|
||||
|
||||
def on_login(self, username):
|
||||
pass
|
||||
|
||||
|
||||
def on_logout(self, username):
|
||||
pass
|
||||
|
||||
|
||||
def on_check(self, username):
|
||||
pass
|
||||
|
||||
|
||||
def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
|
||||
return ntob("""<html><body>
|
||||
Message: %(error_msg)s
|
||||
@@ -309,7 +309,7 @@ Message: %(error_msg)s
|
||||
</form>
|
||||
</body></html>""" % {'from_page': from_page, 'username': username,
|
||||
'error_msg': error_msg}, "utf-8")
|
||||
|
||||
|
||||
def do_login(self, username, password, from_page='..', **kwargs):
|
||||
"""Login. May raise redirect, or return True if request handled."""
|
||||
response = cherrypy.serving.response
|
||||
@@ -326,7 +326,7 @@ Message: %(error_msg)s
|
||||
cherrypy.session[self.session_key] = username
|
||||
self.on_login(username)
|
||||
raise cherrypy.HTTPRedirect(from_page or "/")
|
||||
|
||||
|
||||
def do_logout(self, from_page='..', **kwargs):
|
||||
"""Logout. May raise redirect, or return True if request handled."""
|
||||
sess = cherrypy.session
|
||||
@@ -336,13 +336,13 @@ Message: %(error_msg)s
|
||||
cherrypy.serving.request.login = None
|
||||
self.on_logout(username)
|
||||
raise cherrypy.HTTPRedirect(from_page)
|
||||
|
||||
|
||||
def do_check(self):
|
||||
"""Assert username. May raise redirect, or return True if request handled."""
|
||||
sess = cherrypy.session
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
username = sess.get(self.session_key)
|
||||
if not username:
|
||||
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')
|
||||
request.login = username
|
||||
self.on_check(username)
|
||||
|
||||
|
||||
def run(self):
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
path = request.path_info
|
||||
if path.endswith('login_screen'):
|
||||
if self.debug:
|
||||
@@ -420,7 +420,7 @@ def log_request_headers(debug=False):
|
||||
def log_hooks(debug=False):
|
||||
"""Write request.hooks to the cherrypy error log."""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
msg = []
|
||||
# Sort by the standard points if possible.
|
||||
from cherrypy import _cprequest
|
||||
@@ -428,7 +428,7 @@ def log_hooks(debug=False):
|
||||
for k in request.hooks.keys():
|
||||
if k not in points:
|
||||
points.append(k)
|
||||
|
||||
|
||||
for k in points:
|
||||
msg.append(" %s:" % 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."""
|
||||
request = cherrypy.serving.request
|
||||
pi = request.path_info
|
||||
|
||||
|
||||
if debug:
|
||||
cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
|
||||
(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):
|
||||
"""Wrap response.body in a generator that recursively iterates over body.
|
||||
|
||||
|
||||
This allows cherrypy.response.body to consist of 'nested generators';
|
||||
that is, a set of generators that yield generators.
|
||||
"""
|
||||
@@ -495,9 +495,9 @@ def flatten(debug=False):
|
||||
|
||||
def accept(media=None, debug=False):
|
||||
"""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 provided, it should be the Content-Type value (as a string)
|
||||
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
|
||||
@@ -505,16 +505,16 @@ def accept(media=None, debug=False):
|
||||
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
|
||||
if 'media' is None).
|
||||
|
||||
|
||||
If no match is found, then HTTPError 406 (Not Acceptable) is raised.
|
||||
Note that most web browsers send */* as a (low-quality) acceptable
|
||||
media range, which should match any Content-Type. In addition, "...if
|
||||
no Accept header field is present, then it is assumed that the client
|
||||
accepts all media types."
|
||||
|
||||
|
||||
Matching types are checked in order of client preference first,
|
||||
and then in the order of the given 'media' values.
|
||||
|
||||
|
||||
Note that this function does not honor accept-params (other than "q").
|
||||
"""
|
||||
if not media:
|
||||
@@ -522,7 +522,7 @@ def accept(media=None, debug=False):
|
||||
if isinstance(media, basestring):
|
||||
media = [media]
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
# Parse the Accept request header, and try to match one
|
||||
# of the requested media-ranges (in order of preference).
|
||||
ranges = request.headers.elements('Accept')
|
||||
@@ -556,7 +556,7 @@ def accept(media=None, debug=False):
|
||||
cherrypy.log('Match due to %s' % element.value,
|
||||
'TOOLS.ACCEPT')
|
||||
return element.value
|
||||
|
||||
|
||||
# No suitable media-range found.
|
||||
ah = request.headers.get('Accept')
|
||||
if ah is None:
|
||||
@@ -569,22 +569,22 @@ def accept(media=None, debug=False):
|
||||
|
||||
|
||||
class MonitoredHeaderMap(_httputil.HeaderMap):
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.accessed_headers = set()
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.__getitem__(self, key)
|
||||
|
||||
|
||||
def __contains__(self, key):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.__contains__(self, key)
|
||||
|
||||
|
||||
def get(self, key, default=None):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.get(self, key, default=default)
|
||||
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
# Python 2
|
||||
def has_key(self, key):
|
||||
@@ -595,13 +595,13 @@ class MonitoredHeaderMap(_httputil.HeaderMap):
|
||||
def autovary(ignore=None, debug=False):
|
||||
"""Auto-populate the Vary response header based on request.header access."""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
req_h = request.headers
|
||||
request.headers = MonitoredHeaderMap()
|
||||
request.headers.update(req_h)
|
||||
if ignore is None:
|
||||
ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
|
||||
|
||||
|
||||
def set_response_header():
|
||||
resp_h = cherrypy.serving.response.headers
|
||||
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'):
|
||||
"""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.
|
||||
|
||||
|
||||
encoding
|
||||
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
|
||||
the Content-Type request header).
|
||||
|
||||
|
||||
default_encoding
|
||||
Only in effect if the 'encoding' argument is not given.
|
||||
If given, the set of charsets attempted while decoding a request entity is
|
||||
*extended* with the given value(s).
|
||||
|
||||
|
||||
"""
|
||||
body = cherrypy.request.body
|
||||
if encoding is not None:
|
||||
@@ -35,7 +35,7 @@ def decode(encoding=None, default_encoding='utf-8'):
|
||||
|
||||
|
||||
class ResponseEncoder:
|
||||
|
||||
|
||||
default_encoding = 'utf-8'
|
||||
failmsg = "Response body could not be encoded with %r."
|
||||
encoding = None
|
||||
@@ -43,11 +43,11 @@ class ResponseEncoder:
|
||||
text_only = True
|
||||
add_charset = True
|
||||
debug = False
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
self.attempted_charsets = set()
|
||||
request = cherrypy.serving.request
|
||||
if request.handler is not None:
|
||||
@@ -56,17 +56,17 @@ class ResponseEncoder:
|
||||
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
|
||||
self.oldhandler = request.handler
|
||||
request.handler = self
|
||||
|
||||
|
||||
def encode_stream(self, encoding):
|
||||
"""Encode a streaming response body.
|
||||
|
||||
|
||||
Use a generator wrapper, and just pray it works as the stream is
|
||||
being written out.
|
||||
"""
|
||||
if encoding in self.attempted_charsets:
|
||||
return False
|
||||
self.attempted_charsets.add(encoding)
|
||||
|
||||
|
||||
def encoder(body):
|
||||
for chunk in body:
|
||||
if isinstance(chunk, unicodestr):
|
||||
@@ -74,13 +74,13 @@ class ResponseEncoder:
|
||||
yield chunk
|
||||
self.body = encoder(self.body)
|
||||
return True
|
||||
|
||||
|
||||
def encode_string(self, encoding):
|
||||
"""Encode a buffered response body."""
|
||||
if encoding in self.attempted_charsets:
|
||||
return False
|
||||
self.attempted_charsets.add(encoding)
|
||||
|
||||
|
||||
try:
|
||||
body = []
|
||||
for chunk in self.body:
|
||||
@@ -92,11 +92,11 @@ class ResponseEncoder:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def find_acceptable_charset(self):
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
if self.debug:
|
||||
cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
|
||||
if response.stream:
|
||||
@@ -115,14 +115,14 @@ class ResponseEncoder:
|
||||
# >>> len(t.encode("utf7"))
|
||||
# 8
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
|
||||
# Parse the Accept-Charset request header, and try to provide one
|
||||
# of the requested charsets (in order of user preference).
|
||||
encs = request.headers.elements('Accept-Charset')
|
||||
charsets = [enc.value.lower() for enc in encs]
|
||||
if self.debug:
|
||||
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
|
||||
|
||||
|
||||
if self.encoding is not None:
|
||||
# If specified, force this encoding to be used, or fail.
|
||||
encoding = self.encoding.lower()
|
||||
@@ -160,7 +160,7 @@ class ResponseEncoder:
|
||||
'0)' % element, 'TOOLS.ENCODE')
|
||||
if encoder(encoding):
|
||||
return encoding
|
||||
|
||||
|
||||
if "*" not in charsets:
|
||||
# If no "*" is present in an Accept-Charset field, then all
|
||||
# character sets not explicitly mentioned get a quality
|
||||
@@ -173,7 +173,7 @@ class ResponseEncoder:
|
||||
'TOOLS.ENCODE')
|
||||
if encoder(iso):
|
||||
return iso
|
||||
|
||||
|
||||
# No suitable encoding found.
|
||||
ac = request.headers.get('Accept-Charset')
|
||||
if ac is None:
|
||||
@@ -182,11 +182,11 @@ class ResponseEncoder:
|
||||
msg = "Your client sent this Accept-Charset header: %s." % ac
|
||||
msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
response = cherrypy.serving.response
|
||||
self.body = self.oldhandler(*args, **kwargs)
|
||||
|
||||
|
||||
if isinstance(self.body, basestring):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# item list is much faster than iterating over every character
|
||||
@@ -200,7 +200,7 @@ class ResponseEncoder:
|
||||
self.body = file_generator(self.body)
|
||||
elif self.body is None:
|
||||
self.body = []
|
||||
|
||||
|
||||
ct = response.headers.elements("Content-Type")
|
||||
if self.debug:
|
||||
cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
|
||||
@@ -222,7 +222,7 @@ class ResponseEncoder:
|
||||
if self.debug:
|
||||
cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
|
||||
do_find = True
|
||||
|
||||
|
||||
if do_find:
|
||||
# Set "charset=..." param on response Content-Type header
|
||||
ct.params['charset'] = self.find_acceptable_charset()
|
||||
@@ -231,7 +231,7 @@ class ResponseEncoder:
|
||||
cherrypy.log('Setting Content-Type %s' % ct,
|
||||
'TOOLS.ENCODE')
|
||||
response.headers["Content-Type"] = str(ct)
|
||||
|
||||
|
||||
return self.body
|
||||
|
||||
# GZIP
|
||||
@@ -239,7 +239,7 @@ class ResponseEncoder:
|
||||
def compress(body, compress_level):
|
||||
"""Compress 'body' at the given compress_level."""
|
||||
import zlib
|
||||
|
||||
|
||||
# See http://www.gzip.org/zlib/rfc-gzip.html
|
||||
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
|
||||
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 ntob('\x02') # XFL: max compression, slowest algo
|
||||
yield ntob('\xff') # OS: unknown
|
||||
|
||||
|
||||
crc = zlib.crc32(ntob(""))
|
||||
size = 0
|
||||
zobj = zlib.compressobj(compress_level,
|
||||
@@ -259,7 +259,7 @@ def compress(body, compress_level):
|
||||
crc = zlib.crc32(line, crc)
|
||||
yield zobj.compress(line)
|
||||
yield zobj.flush()
|
||||
|
||||
|
||||
# CRC32: 4 bytes
|
||||
yield struct.pack("<L", crc & int('FFFFFFFF', 16))
|
||||
# ISIZE: 4 bytes
|
||||
@@ -267,7 +267,7 @@ def compress(body, compress_level):
|
||||
|
||||
def decompress(body):
|
||||
import gzip
|
||||
|
||||
|
||||
zbuf = BytesIO()
|
||||
zbuf.write(body)
|
||||
zbuf.seek(0)
|
||||
@@ -279,7 +279,7 @@ def decompress(body):
|
||||
|
||||
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.
|
||||
|
||||
|
||||
cherrypy.response.headers['Content-Type'] must be set to one of the
|
||||
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/*
|
||||
* type/*+subtype
|
||||
|
||||
|
||||
No compression is performed if any of the following hold:
|
||||
* 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' with a qvalue > 0 is present
|
||||
* The 'identity' value is given with a qvalue > 0.
|
||||
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
set_vary_header(response, "Accept-Encoding")
|
||||
|
||||
|
||||
if not response.body:
|
||||
# Response body is empty (might be a 304 for instance)
|
||||
if debug:
|
||||
cherrypy.log('No response body', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
|
||||
# If returning cached content (which should already have been gzipped),
|
||||
# don't re-zip.
|
||||
if getattr(request, "cached", False):
|
||||
if debug:
|
||||
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
|
||||
acceptable = request.headers.elements('Accept-Encoding')
|
||||
if not acceptable:
|
||||
# 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:
|
||||
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
|
||||
ct = response.headers.get('Content-Type', '').split(';')[0]
|
||||
for coding in acceptable:
|
||||
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,
|
||||
context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
|
||||
if ct not in mime_types:
|
||||
# If the list of provided mime-types contains tokens
|
||||
# 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' %
|
||||
(ct, mime_types), context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Gzipping', context='TOOLS.GZIP')
|
||||
# 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:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
if debug:
|
||||
cherrypy.log('No acceptable encoding found.', context='GZIP')
|
||||
cherrypy.HTTPError(406, "identity, gzip").set_response()
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
try:
|
||||
import objgraph
|
||||
except ImportError:
|
||||
@@ -103,13 +103,13 @@ def get_instances(cls):
|
||||
|
||||
|
||||
class RequestCounter(SimplePlugin):
|
||||
|
||||
|
||||
def start(self):
|
||||
self.count = 0
|
||||
|
||||
|
||||
def before_request(self):
|
||||
self.count += 1
|
||||
|
||||
|
||||
def after_request(self):
|
||||
self.count -=1
|
||||
request_counter = RequestCounter(cherrypy.engine)
|
||||
@@ -145,14 +145,14 @@ class GCRoot(object):
|
||||
|
||||
def stats(self):
|
||||
output = ["Statistics:"]
|
||||
|
||||
|
||||
for trial in range(10):
|
||||
if request_counter.count > 0:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
output.append("\nNot all requests closed properly.")
|
||||
|
||||
|
||||
# gc_collect isn't perfectly synchronous, because it may
|
||||
# break reference cycles that then take time to fully
|
||||
# finalize. Call it thrice and hope for the best.
|
||||
@@ -208,7 +208,7 @@ class GCRoot(object):
|
||||
t = ReferrerTree(ignore=[objs], maxdepth=3)
|
||||
tree = t.ascend(obj)
|
||||
output.extend(t.format(tree))
|
||||
|
||||
|
||||
return "\n".join(output)
|
||||
stats.exposed = True
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage:
|
||||
First use 'doAuth' to request the client authentication for a
|
||||
certain resource. You should send an httplib.UNAUTHORIZED response to the
|
||||
client so he knows he has to authenticate itself.
|
||||
|
||||
|
||||
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
|
||||
'checkResponse'.
|
||||
|
||||
@@ -29,27 +29,27 @@ __license__ = """
|
||||
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
|
||||
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:
|
||||
|
||||
* 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.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of Sylvain Hellegouarch nor the names of his contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
* Neither the name of Sylvain Hellegouarch nor the names of his contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
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
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
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
|
||||
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
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
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
|
||||
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
|
||||
Digest and fallback to Basic authentication when the browser doesn't
|
||||
support the first one.
|
||||
|
||||
|
||||
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
|
||||
|
||||
return digestAuth (realm) + " " + basicAuth (realm)
|
||||
@@ -187,7 +187,7 @@ def parseAuthorization (credentials):
|
||||
#
|
||||
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
|
||||
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):
|
||||
"""'checkResponse' compares the auth_map with the password and optionally
|
||||
other arguments that each implementation might need.
|
||||
|
||||
|
||||
If the response is of type 'Basic' then the function has the following
|
||||
signature::
|
||||
|
||||
|
||||
checkBasicResponse (auth_map, password) -> bool
|
||||
|
||||
|
||||
If the response is of type 'Digest' then the function has the following
|
||||
signature::
|
||||
|
||||
|
||||
checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
|
||||
|
||||
|
||||
The 'A1' argument is only used in MD5_SESS algorithm based responses.
|
||||
Check md5SessionKey() for more info.
|
||||
"""
|
||||
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
|
||||
return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import urllib
|
||||
|
||||
def urljoin(*atoms):
|
||||
"""Return the given path \*atoms, joined into a single URL.
|
||||
|
||||
|
||||
This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
||||
original URL, even if either atom is blank.
|
||||
"""
|
||||
@@ -40,7 +40,7 @@ def urljoin(*atoms):
|
||||
|
||||
def urljoin_bytes(*atoms):
|
||||
"""Return the given path *atoms, joined into a single URL.
|
||||
|
||||
|
||||
This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
||||
original URL, even if either atom is blank.
|
||||
"""
|
||||
@@ -56,18 +56,18 @@ def protocol_from_http(protocol_str):
|
||||
|
||||
def get_ranges(headervalue, content_length):
|
||||
"""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
|
||||
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
|
||||
function will return the list [(3, 7)].
|
||||
|
||||
|
||||
If this function returns an empty list, you should return HTTP 416.
|
||||
"""
|
||||
|
||||
|
||||
if not headervalue:
|
||||
return None
|
||||
|
||||
|
||||
result = []
|
||||
bytesunit, byteranges = headervalue.split("=", 1)
|
||||
for brange in byteranges.split(","):
|
||||
@@ -101,35 +101,35 @@ def get_ranges(headervalue, content_length):
|
||||
return None
|
||||
# Negative subscript (last N bytes)
|
||||
result.append((content_length - int(stop), content_length))
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class HeaderElement(object):
|
||||
"""An element (with parameters) from an HTTP header's element list."""
|
||||
|
||||
|
||||
def __init__(self, value, params=None):
|
||||
self.value = value
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.value, other.value)
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.value < other.value
|
||||
|
||||
|
||||
def __str__(self):
|
||||
p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
|
||||
return "%s%s" % (self.value, "".join(p))
|
||||
|
||||
|
||||
def __bytes__(self):
|
||||
return ntob(self.__str__())
|
||||
|
||||
def __unicode__(self):
|
||||
return ntou(self.__str__())
|
||||
|
||||
|
||||
def parse(elementstr):
|
||||
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
|
||||
# Split the element into a value and parameters. The 'value' may
|
||||
@@ -150,7 +150,7 @@ class HeaderElement(object):
|
||||
params[key] = val
|
||||
return initial_value, params
|
||||
parse = staticmethod(parse)
|
||||
|
||||
|
||||
def from_str(cls, elementstr):
|
||||
"""Construct an instance from a string of the form 'token;key=val'."""
|
||||
ival, params = cls.parse(elementstr)
|
||||
@@ -162,14 +162,14 @@ q_separator = re.compile(r'; *q *=')
|
||||
|
||||
class AcceptElement(HeaderElement):
|
||||
"""An element (with parameters) from an Accept* header's element list.
|
||||
|
||||
|
||||
AcceptElement objects are comparable; the more-preferred object will be
|
||||
"less than" the less-preferred object. They are also therefore sortable;
|
||||
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
|
||||
have been the other way around, but it's too late to fix now.
|
||||
"""
|
||||
|
||||
|
||||
def from_str(cls, elementstr):
|
||||
qvalue = None
|
||||
# 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
|
||||
# headers cannot, but it's easier to parse them as if they did.
|
||||
qvalue = HeaderElement.from_str(atoms[0].strip())
|
||||
|
||||
|
||||
media_type, params = cls.parse(media_range)
|
||||
if qvalue is not None:
|
||||
params["q"] = qvalue
|
||||
return cls(media_type, params)
|
||||
from_str = classmethod(from_str)
|
||||
|
||||
|
||||
def qvalue(self):
|
||||
val = self.params.get("q", "1")
|
||||
if isinstance(val, HeaderElement):
|
||||
val = val.value
|
||||
return float(val)
|
||||
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
|
||||
|
||||
|
||||
def __cmp__(self, other):
|
||||
diff = cmp(self.qvalue, other.qvalue)
|
||||
if diff == 0:
|
||||
diff = cmp(str(self), str(other))
|
||||
return diff
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.qvalue == other.qvalue:
|
||||
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."""
|
||||
if not fieldvalue:
|
||||
return []
|
||||
|
||||
|
||||
result = []
|
||||
for element in fieldvalue.split(","):
|
||||
if fieldname.startswith("Accept") or fieldname == 'TE':
|
||||
@@ -219,7 +219,7 @@ def header_elements(fieldname, fieldvalue):
|
||||
else:
|
||||
hv = HeaderElement.from_str(element)
|
||||
result.append(hv)
|
||||
|
||||
|
||||
return list(reversed(sorted(result)))
|
||||
|
||||
def decode_TEXT(value):
|
||||
@@ -239,16 +239,16 @@ def decode_TEXT(value):
|
||||
|
||||
def valid_status(status):
|
||||
"""Return legal HTTP status Code, Reason-phrase and Message.
|
||||
|
||||
|
||||
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,
|
||||
a default reason-phrase will be provided.
|
||||
"""
|
||||
|
||||
|
||||
if not status:
|
||||
status = 200
|
||||
|
||||
|
||||
status = str(status)
|
||||
parts = status.split(" ", 1)
|
||||
if len(parts) == 1:
|
||||
@@ -258,26 +258,26 @@ def valid_status(status):
|
||||
else:
|
||||
code, reason = parts
|
||||
reason = reason.strip()
|
||||
|
||||
|
||||
try:
|
||||
code = int(code)
|
||||
except ValueError:
|
||||
raise ValueError("Illegal response status from server "
|
||||
"(%s is non-numeric)." % repr(code))
|
||||
|
||||
|
||||
if code < 100 or code > 599:
|
||||
raise ValueError("Illegal response status from server "
|
||||
"(%s is out of range)." % repr(code))
|
||||
|
||||
|
||||
if code not in response_codes:
|
||||
# code is unknown but not illegal
|
||||
default_reason, message = "", ""
|
||||
else:
|
||||
default_reason, message = response_codes[code]
|
||||
|
||||
|
||||
if reason is None:
|
||||
reason = default_reason
|
||||
|
||||
|
||||
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'):
|
||||
"""Parse a query given as a string argument.
|
||||
|
||||
|
||||
Arguments:
|
||||
|
||||
|
||||
qs: URL-encoded query string to be parsed
|
||||
|
||||
|
||||
keep_blank_values: flag indicating whether blank values in
|
||||
URL encoded queries should be treated as blank strings. A
|
||||
true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
|
||||
|
||||
strict_parsing: flag indicating what to do with parsing errors. If
|
||||
false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
|
||||
|
||||
Returns a dict, as G-d intended.
|
||||
"""
|
||||
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'):
|
||||
"""Build a params dictionary from a query_string.
|
||||
|
||||
|
||||
Duplicate key/value pairs in the provided query_string will be
|
||||
returned as {'key': [val1, val2, ...]}. Single key/values will
|
||||
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):
|
||||
"""A case-insensitive dict subclass.
|
||||
|
||||
|
||||
Each key is changed on entry to str(key).title().
|
||||
"""
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, str(key).title())
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, str(key).title(), value)
|
||||
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, str(key).title())
|
||||
|
||||
|
||||
def __contains__(self, key):
|
||||
return dict.__contains__(self, str(key).title())
|
||||
|
||||
|
||||
def get(self, key, default=None):
|
||||
return dict.get(self, str(key).title(), default)
|
||||
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
def has_key(self, key):
|
||||
return dict.has_key(self, str(key).title())
|
||||
|
||||
|
||||
def update(self, E):
|
||||
for k in E.keys():
|
||||
self[str(k).title()] = E[k]
|
||||
|
||||
|
||||
def fromkeys(cls, seq, value=None):
|
||||
newdict = cls()
|
||||
for k in seq:
|
||||
newdict[str(k).title()] = value
|
||||
return newdict
|
||||
fromkeys = classmethod(fromkeys)
|
||||
|
||||
|
||||
def setdefault(self, key, x=None):
|
||||
key = str(key).title()
|
||||
try:
|
||||
@@ -392,7 +392,7 @@ class CaseInsensitiveDict(dict):
|
||||
except KeyError:
|
||||
self[key] = x
|
||||
return x
|
||||
|
||||
|
||||
def pop(self, key, default):
|
||||
return dict.pop(self, str(key).title(), default)
|
||||
|
||||
@@ -412,54 +412,54 @@ else:
|
||||
|
||||
class HeaderMap(CaseInsensitiveDict):
|
||||
"""A dict subclass for HTTP request and response headers.
|
||||
|
||||
|
||||
Each key is changed on entry to str(key).title(). This allows headers
|
||||
to be case-insensitive and avoid duplicates.
|
||||
|
||||
|
||||
Values are header values (decoded according to :rfc:`2047` if necessary).
|
||||
"""
|
||||
|
||||
|
||||
protocol=(1, 1)
|
||||
encodings = ["ISO-8859-1"]
|
||||
|
||||
|
||||
# Someday, when http-bis is done, this will probably get dropped
|
||||
# since few servers, clients, or intermediaries do it. But until then,
|
||||
# we're going to obey the spec as is.
|
||||
# "Words of *TEXT MAY contain characters from character sets other than
|
||||
# ISO-8859-1 only when encoded according to the rules of RFC 2047."
|
||||
use_rfc_2047 = True
|
||||
|
||||
|
||||
def elements(self, key):
|
||||
"""Return a sorted list of HeaderElements for the given header."""
|
||||
key = str(key).title()
|
||||
value = self.get(key)
|
||||
return header_elements(key, value)
|
||||
|
||||
|
||||
def values(self, key):
|
||||
"""Return a sorted list of HeaderElement.value for the given header."""
|
||||
return [e.value for e in self.elements(key)]
|
||||
|
||||
|
||||
def output(self):
|
||||
"""Transform self into a list of (name, value) tuples."""
|
||||
header_list = []
|
||||
for k, v in self.items():
|
||||
if isinstance(k, unicodestr):
|
||||
k = self.encode(k)
|
||||
|
||||
|
||||
if not isinstance(v, basestring):
|
||||
v = str(v)
|
||||
|
||||
|
||||
if isinstance(v, unicodestr):
|
||||
v = self.encode(v)
|
||||
|
||||
|
||||
# See header_translate_* constants above.
|
||||
# Replace only if you really know what you're doing.
|
||||
k = k.translate(header_translate_table, header_translate_deletechars)
|
||||
v = v.translate(header_translate_table, header_translate_deletechars)
|
||||
|
||||
|
||||
header_list.append((k, v))
|
||||
return header_list
|
||||
|
||||
|
||||
def encode(self, v):
|
||||
"""Return the given header name or value, encoded for HTTP output."""
|
||||
for enc in self.encodings:
|
||||
@@ -467,16 +467,16 @@ class HeaderMap(CaseInsensitiveDict):
|
||||
return v.encode(enc)
|
||||
except UnicodeEncodeError:
|
||||
continue
|
||||
|
||||
|
||||
if self.protocol == (1, 1) and self.use_rfc_2047:
|
||||
# Encode RFC-2047 TEXT
|
||||
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
|
||||
# Encode RFC-2047 TEXT
|
||||
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
|
||||
# We do our own here instead of using the email module
|
||||
# because we never want to fold lines--folding has
|
||||
# been deprecated by the HTTP working group.
|
||||
v = b2a_base64(v.encode('utf-8'))
|
||||
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
|
||||
|
||||
|
||||
raise ValueError("Could not encode header part %r using "
|
||||
"any of the encodings %r." %
|
||||
(v, self.encodings))
|
||||
@@ -484,23 +484,23 @@ class HeaderMap(CaseInsensitiveDict):
|
||||
|
||||
class Host(object):
|
||||
"""An internet address.
|
||||
|
||||
|
||||
name
|
||||
Should be the client's host name. If not available (because no DNS
|
||||
lookup is performed), the IP address should be used instead.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
ip = "0.0.0.0"
|
||||
port = 80
|
||||
name = "unknown.tld"
|
||||
|
||||
|
||||
def __init__(self, ip, port, name=None):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
if name is None:
|
||||
name = ip
|
||||
self.name = name
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
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."""
|
||||
if not entity.headers.get(ntou("Content-Length"), ntou("")):
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
|
||||
body = entity.fp.read()
|
||||
try:
|
||||
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
|
||||
stored at cherrypy.request.json. The 'content_type' argument may
|
||||
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
|
||||
content types will not be allowed; "415 Unsupported Media Type" is
|
||||
raised instead.
|
||||
|
||||
|
||||
Supply your own processor to use a custom decoder, or to handle the parsed
|
||||
data differently. The processor can be configured via
|
||||
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
|
||||
other reason the request entity cannot be deserialized from JSON,
|
||||
it will raise "400 Bad Request: Invalid JSON document".
|
||||
|
||||
|
||||
You must be using Python 2.6 or greater, or have the 'simplejson'
|
||||
package importable; otherwise, ValueError is raised during processing.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if isinstance(content_type, basestring):
|
||||
content_type = [content_type]
|
||||
|
||||
|
||||
if force:
|
||||
if debug:
|
||||
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(
|
||||
415, 'Expected an entity of content type %s' %
|
||||
', '.join(content_type))
|
||||
|
||||
|
||||
for ct in content_type:
|
||||
if debug:
|
||||
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):
|
||||
"""Wrap request.handler to serialize its output to JSON. Sets Content-Type.
|
||||
|
||||
|
||||
If the given content_type is None, the Content-Type response header
|
||||
is not set.
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ CherryPy users
|
||||
You can profile any of your pages as follows::
|
||||
|
||||
from cherrypy.lib import profiler
|
||||
|
||||
|
||||
class Root:
|
||||
p = profile.Profiler("/path/to/profile/dir")
|
||||
|
||||
|
||||
def index(self):
|
||||
self.p.run(self._index)
|
||||
index.exposed = True
|
||||
|
||||
|
||||
def _index(self):
|
||||
return "Hello, world!"
|
||||
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
You can also turn on profiling for all requests
|
||||
@@ -58,14 +58,14 @@ from cherrypy._cpcompat import BytesIO
|
||||
_count = 0
|
||||
|
||||
class Profiler(object):
|
||||
|
||||
|
||||
def __init__(self, path=None):
|
||||
if not path:
|
||||
path = os.path.join(os.path.dirname(__file__), "profile")
|
||||
self.path = path
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
|
||||
def run(self, func, *args, **params):
|
||||
"""Dump profile data into self.path."""
|
||||
global _count
|
||||
@@ -75,13 +75,13 @@ class Profiler(object):
|
||||
result = prof.runcall(func, *args, **params)
|
||||
prof.dump_stats(path)
|
||||
return result
|
||||
|
||||
|
||||
def statfiles(self):
|
||||
""":rtype: list of available profiles.
|
||||
"""
|
||||
return [f for f in os.listdir(self.path)
|
||||
if f.startswith("cp_") and f.endswith(".prof")]
|
||||
|
||||
|
||||
def stats(self, filename, sortby='cumulative'):
|
||||
""":rtype stats(index): output of print_stats() for the given profile.
|
||||
"""
|
||||
@@ -106,7 +106,7 @@ class Profiler(object):
|
||||
response = sio.getvalue()
|
||||
sio.close()
|
||||
return response
|
||||
|
||||
|
||||
def index(self):
|
||||
return """<html>
|
||||
<head><title>CherryPy profile data</title></head>
|
||||
@@ -117,7 +117,7 @@ class Profiler(object):
|
||||
</html>
|
||||
"""
|
||||
index.exposed = True
|
||||
|
||||
|
||||
def menu(self):
|
||||
yield "<h2>Profiling runs</h2>"
|
||||
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:
|
||||
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
|
||||
menu.exposed = True
|
||||
|
||||
|
||||
def report(self, filename):
|
||||
import cherrypy
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
@@ -135,13 +135,13 @@ class Profiler(object):
|
||||
|
||||
|
||||
class ProfileAggregator(Profiler):
|
||||
|
||||
|
||||
def __init__(self, path=None):
|
||||
Profiler.__init__(self, path)
|
||||
global _count
|
||||
self.count = _count = _count + 1
|
||||
self.profiler = profile.Profile()
|
||||
|
||||
|
||||
def run(self, func, *args):
|
||||
path = os.path.join(self.path, "cp_%04d.prof" % self.count)
|
||||
result = self.profiler.runcall(func, *args)
|
||||
@@ -152,33 +152,33 @@ class ProfileAggregator(Profiler):
|
||||
class make_app:
|
||||
def __init__(self, nextapp, path=None, aggregate=False):
|
||||
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
|
||||
|
||||
|
||||
nextapp
|
||||
the WSGI application to wrap, usually an instance of
|
||||
cherrypy.Application.
|
||||
|
||||
|
||||
path
|
||||
where to dump the profiling output.
|
||||
|
||||
|
||||
aggregate
|
||||
if True, profile data for all HTTP requests will go in
|
||||
a single file. If False (the default), each HTTP request will
|
||||
dump its profile data into a separate file.
|
||||
|
||||
|
||||
"""
|
||||
if profile is None or pstats is None:
|
||||
msg = ("Your installation of Python does not have a profile module. "
|
||||
"If you're on Debian, try `sudo apt-get install python-profiler`. "
|
||||
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
|
||||
warnings.warn(msg)
|
||||
|
||||
|
||||
self.nextapp = nextapp
|
||||
self.aggregate = aggregate
|
||||
if aggregate:
|
||||
self.profiler = ProfileAggregator(path)
|
||||
else:
|
||||
self.profiler = Profiler(path)
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
def gather():
|
||||
result = []
|
||||
@@ -194,7 +194,7 @@ def serve(path=None, port=8080):
|
||||
"If you're on Debian, try `sudo apt-get install python-profiler`. "
|
||||
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
|
||||
warnings.warn(msg)
|
||||
|
||||
|
||||
import cherrypy
|
||||
cherrypy.config.update({'server.socket_port': int(port),
|
||||
'server.thread_pool': 10,
|
||||
|
||||
@@ -55,25 +55,25 @@ def as_dict(config):
|
||||
|
||||
class NamespaceSet(dict):
|
||||
"""A dict of config namespace names and handlers.
|
||||
|
||||
|
||||
Each config entry should begin with a namespace name; the corresponding
|
||||
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 removed) and the config value.
|
||||
|
||||
|
||||
Namespace handlers may be any Python callable; they may also be
|
||||
Python 2.5-style 'context managers', in which case their __enter__
|
||||
method should return a callable to be used as the handler.
|
||||
See cherrypy.tools (the Toolbox class) for an example.
|
||||
"""
|
||||
|
||||
|
||||
def __call__(self, config):
|
||||
"""Iterate through config and pass it to each namespace handler.
|
||||
|
||||
|
||||
config
|
||||
A flat dict, where keys use dots to separate
|
||||
namespaces, and values are arbitrary.
|
||||
|
||||
|
||||
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}
|
||||
will call the 'tools' namespace handler with the args: ('gzip.on', v)
|
||||
@@ -85,7 +85,7 @@ class NamespaceSet(dict):
|
||||
ns, name = k.split(".", 1)
|
||||
bucket = ns_confs.setdefault(ns, {})
|
||||
bucket[name] = config[k]
|
||||
|
||||
|
||||
# I chose __enter__ and __exit__ so someday this could be
|
||||
# rewritten using Python 2.5's 'with' statement:
|
||||
# for ns, handler in self.iteritems():
|
||||
@@ -116,11 +116,11 @@ class NamespaceSet(dict):
|
||||
else:
|
||||
for k, v in ns_confs.get(ns, {}).items():
|
||||
handler(k, v)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
|
||||
dict.__repr__(self))
|
||||
|
||||
|
||||
def __copy__(self):
|
||||
newobj = self.__class__()
|
||||
newobj.update(self)
|
||||
@@ -130,26 +130,26 @@ class NamespaceSet(dict):
|
||||
|
||||
class Config(dict):
|
||||
"""A dict-like set of configuration data, with defaults and namespaces.
|
||||
|
||||
|
||||
May take a file, filename, or dict.
|
||||
"""
|
||||
|
||||
|
||||
defaults = {}
|
||||
environments = {}
|
||||
namespaces = NamespaceSet()
|
||||
|
||||
|
||||
def __init__(self, file=None, **kwargs):
|
||||
self.reset()
|
||||
if file is not None:
|
||||
self.update(file)
|
||||
if kwargs:
|
||||
self.update(kwargs)
|
||||
|
||||
|
||||
def reset(self):
|
||||
"""Reset self to default values."""
|
||||
self.clear()
|
||||
dict.update(self, self.defaults)
|
||||
|
||||
|
||||
def update(self, config):
|
||||
"""Update self from a dict, file or filename."""
|
||||
if isinstance(config, basestring):
|
||||
@@ -161,7 +161,7 @@ class Config(dict):
|
||||
else:
|
||||
config = config.copy()
|
||||
self._apply(config)
|
||||
|
||||
|
||||
def _apply(self, config):
|
||||
"""Update self from a dict."""
|
||||
which_env = config.get('environment')
|
||||
@@ -170,23 +170,23 @@ class Config(dict):
|
||||
for k in env:
|
||||
if k not in config:
|
||||
config[k] = env[k]
|
||||
|
||||
|
||||
dict.update(self, config)
|
||||
self.namespaces(config)
|
||||
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
dict.__setitem__(self, k, v)
|
||||
self.namespaces({k: v})
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def optionxform(self, optionstr):
|
||||
return optionstr
|
||||
|
||||
|
||||
def read(self, filenames):
|
||||
if isinstance(filenames, basestring):
|
||||
filenames = [filenames]
|
||||
@@ -200,7 +200,7 @@ class Parser(ConfigParser):
|
||||
self._read(fp, filename)
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def as_dict(self, raw=False, vars=None):
|
||||
"""Convert an INI file to a dictionary"""
|
||||
# Load INI file into a dict
|
||||
@@ -220,7 +220,7 @@ class Parser(ConfigParser):
|
||||
raise ValueError(msg, x.__class__.__name__, x.args)
|
||||
result[section][option] = value
|
||||
return result
|
||||
|
||||
|
||||
def dict_from_file(self, file):
|
||||
if hasattr(file, 'read'):
|
||||
self.readfp(file)
|
||||
@@ -233,14 +233,14 @@ class Parser(ConfigParser):
|
||||
|
||||
|
||||
class _Builder2:
|
||||
|
||||
|
||||
def build(self, o):
|
||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||
if m is None:
|
||||
raise TypeError("unrepr does not recognize %s" %
|
||||
repr(o.__class__.__name__))
|
||||
return m(o)
|
||||
|
||||
|
||||
def astnode(self, s):
|
||||
"""Return a Python2 ast Node compiled from a string."""
|
||||
try:
|
||||
@@ -249,16 +249,16 @@ class _Builder2:
|
||||
# Fallback to eval when compiler package is not available,
|
||||
# e.g. IronPython 1.0.
|
||||
return eval(s)
|
||||
|
||||
|
||||
p = compiler.parse("__tempvalue__ = " + s)
|
||||
return p.getChildren()[1].getChildren()[0].getChildren()[1]
|
||||
|
||||
|
||||
def build_Subscript(self, o):
|
||||
expr, flags, subs = o.getChildren()
|
||||
expr = self.build(expr)
|
||||
subs = self.build(subs)
|
||||
return expr[subs]
|
||||
|
||||
|
||||
def build_CallFunc(self, o):
|
||||
children = map(self.build, o.getChildren())
|
||||
callee = children.pop(0)
|
||||
@@ -266,23 +266,23 @@ class _Builder2:
|
||||
starargs = children.pop() or ()
|
||||
args = tuple(children) + tuple(starargs)
|
||||
return callee(*args, **kwargs)
|
||||
|
||||
|
||||
def build_List(self, o):
|
||||
return map(self.build, o.getChildren())
|
||||
|
||||
|
||||
def build_Const(self, o):
|
||||
return o.value
|
||||
|
||||
|
||||
def build_Dict(self, o):
|
||||
d = {}
|
||||
i = iter(map(self.build, o.getChildren()))
|
||||
for el in i:
|
||||
d[el] = i.next()
|
||||
return d
|
||||
|
||||
|
||||
def build_Tuple(self, o):
|
||||
return tuple(self.build_List(o))
|
||||
|
||||
|
||||
def build_Name(self, o):
|
||||
name = o.name
|
||||
if name == 'None':
|
||||
@@ -291,21 +291,21 @@ class _Builder2:
|
||||
return True
|
||||
if name == 'False':
|
||||
return False
|
||||
|
||||
|
||||
# See if the Name is a package or module. If it is, import it.
|
||||
try:
|
||||
return modules(name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# See if the Name is in builtins.
|
||||
try:
|
||||
return getattr(builtins, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
raise TypeError("unrepr could not resolve the name %s" % repr(name))
|
||||
|
||||
|
||||
def build_Add(self, o):
|
||||
left, right = map(self.build, o.getChildren())
|
||||
return left + right
|
||||
@@ -313,30 +313,30 @@ class _Builder2:
|
||||
def build_Mul(self, o):
|
||||
left, right = map(self.build, o.getChildren())
|
||||
return left * right
|
||||
|
||||
|
||||
def build_Getattr(self, o):
|
||||
parent = self.build(o.expr)
|
||||
return getattr(parent, o.attrname)
|
||||
|
||||
|
||||
def build_NoneType(self, o):
|
||||
return None
|
||||
|
||||
|
||||
def build_UnarySub(self, o):
|
||||
return -self.build(o.getChildren()[0])
|
||||
|
||||
|
||||
def build_UnaryAdd(self, o):
|
||||
return self.build(o.getChildren()[0])
|
||||
|
||||
|
||||
class _Builder3:
|
||||
|
||||
|
||||
def build(self, o):
|
||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||
if m is None:
|
||||
raise TypeError("unrepr does not recognize %s" %
|
||||
repr(o.__class__.__name__))
|
||||
return m(o)
|
||||
|
||||
|
||||
def astnode(self, s):
|
||||
"""Return a Python3 ast Node compiled from a string."""
|
||||
try:
|
||||
@@ -351,46 +351,46 @@ class _Builder3:
|
||||
|
||||
def build_Subscript(self, o):
|
||||
return self.build(o.value)[self.build(o.slice)]
|
||||
|
||||
|
||||
def build_Index(self, o):
|
||||
return self.build(o.value)
|
||||
|
||||
|
||||
def build_Call(self, o):
|
||||
callee = self.build(o.func)
|
||||
|
||||
|
||||
if o.args is None:
|
||||
args = ()
|
||||
else:
|
||||
args = tuple([self.build(a) for a in o.args])
|
||||
|
||||
else:
|
||||
args = tuple([self.build(a) for a in o.args])
|
||||
|
||||
if o.starargs is None:
|
||||
starargs = ()
|
||||
else:
|
||||
starargs = self.build(o.starargs)
|
||||
|
||||
|
||||
if o.kwargs is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = self.build(o.kwargs)
|
||||
|
||||
|
||||
return callee(*(args + starargs), **kwargs)
|
||||
|
||||
|
||||
def build_List(self, o):
|
||||
return list(map(self.build, o.elts))
|
||||
|
||||
|
||||
def build_Str(self, o):
|
||||
return o.s
|
||||
|
||||
|
||||
def build_Num(self, o):
|
||||
return o.n
|
||||
|
||||
|
||||
def build_Dict(self, o):
|
||||
return dict([(self.build(k), self.build(v))
|
||||
for k, v in zip(o.keys, o.values)])
|
||||
|
||||
|
||||
def build_Tuple(self, o):
|
||||
return tuple(self.build_List(o))
|
||||
|
||||
|
||||
def build_Name(self, o):
|
||||
name = o.id
|
||||
if name == 'None':
|
||||
@@ -399,28 +399,28 @@ class _Builder3:
|
||||
return True
|
||||
if name == 'False':
|
||||
return False
|
||||
|
||||
|
||||
# See if the Name is a package or module. If it is, import it.
|
||||
try:
|
||||
return modules(name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# See if the Name is in builtins.
|
||||
try:
|
||||
import builtins
|
||||
return getattr(builtins, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
raise TypeError("unrepr could not resolve the name %s" % repr(name))
|
||||
|
||||
|
||||
def build_UnaryOp(self, o):
|
||||
op, operand = map(self.build, [o.op, o.operand])
|
||||
return op(operand)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def build_Add(self, o):
|
||||
@@ -428,7 +428,7 @@ class _Builder3:
|
||||
|
||||
def build_Mult(self, o):
|
||||
return _operator.mul
|
||||
|
||||
|
||||
def build_USub(self, o):
|
||||
return _operator.neg
|
||||
|
||||
@@ -465,12 +465,12 @@ def modules(modulePath):
|
||||
|
||||
def attributes(full_attribute_name):
|
||||
"""Load a module and retrieve an attribute of that module."""
|
||||
|
||||
|
||||
# Parse out the path, module, and attribute
|
||||
last_dot = full_attribute_name.rfind(".")
|
||||
attr_name = full_attribute_name[last_dot + 1:]
|
||||
mod_path = full_attribute_name[:last_dot]
|
||||
|
||||
|
||||
mod = modules(mod_path)
|
||||
# Let an AttributeError propagate outward.
|
||||
try:
|
||||
@@ -478,7 +478,7 @@ def attributes(full_attribute_name):
|
||||
except AttributeError:
|
||||
raise AttributeError("'%s' object has no attribute '%s'"
|
||||
% (mod_path, attr_name))
|
||||
|
||||
|
||||
# Return a reference to the attribute.
|
||||
return attr
|
||||
|
||||
|
||||
+119
-119
@@ -101,12 +101,12 @@ missing = object()
|
||||
|
||||
class Session(object):
|
||||
"""A CherryPy dict-like Session object (one per request)."""
|
||||
|
||||
|
||||
_id = None
|
||||
|
||||
|
||||
id_observers = None
|
||||
"A list of callbacks to which to pass new id's."
|
||||
|
||||
|
||||
def _get_id(self):
|
||||
return self._id
|
||||
def _set_id(self, value):
|
||||
@@ -114,46 +114,46 @@ class Session(object):
|
||||
for o in self.id_observers:
|
||||
o(value)
|
||||
id = property(_get_id, _set_id, doc="The current session ID.")
|
||||
|
||||
|
||||
timeout = 60
|
||||
"Number of minutes after which to delete session data."
|
||||
|
||||
|
||||
locked = False
|
||||
"""
|
||||
If True, this session instance has exclusive read/write access
|
||||
to session data."""
|
||||
|
||||
|
||||
loaded = False
|
||||
"""
|
||||
If True, data has been retrieved from storage. This should happen
|
||||
automatically on the first attempt to access session data."""
|
||||
|
||||
|
||||
clean_thread = None
|
||||
"Class-level Monitor which calls self.clean_up."
|
||||
|
||||
|
||||
clean_freq = 5
|
||||
"The poll rate for expired session cleanup in minutes."
|
||||
|
||||
|
||||
originalid = None
|
||||
"The session id passed by the client. May be missing or unsafe."
|
||||
|
||||
|
||||
missing = False
|
||||
"True if the session requested by the client did not exist."
|
||||
|
||||
|
||||
regenerated = False
|
||||
"""
|
||||
True if the application called session.regenerate(). This is not set by
|
||||
internal calls to regenerate the session id."""
|
||||
|
||||
|
||||
debug=False
|
||||
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
self.id_observers = []
|
||||
self._data = {}
|
||||
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
self.originalid = id
|
||||
self.missing = False
|
||||
if id is None:
|
||||
@@ -184,33 +184,33 @@ class Session(object):
|
||||
"""Replace the current session (with a new id)."""
|
||||
self.regenerated = True
|
||||
self._regenerate()
|
||||
|
||||
|
||||
def _regenerate(self):
|
||||
if self.id is not None:
|
||||
self.delete()
|
||||
|
||||
|
||||
old_session_was_locked = self.locked
|
||||
if old_session_was_locked:
|
||||
self.release_lock()
|
||||
|
||||
|
||||
self.id = None
|
||||
while self.id is None:
|
||||
self.id = self.generate_id()
|
||||
# Assert that the generated id is not already stored.
|
||||
if self._exists():
|
||||
self.id = None
|
||||
|
||||
|
||||
if old_session_was_locked:
|
||||
self.acquire_lock()
|
||||
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
pass
|
||||
|
||||
|
||||
def generate_id(self):
|
||||
"""Return a new session id."""
|
||||
return random20()
|
||||
|
||||
|
||||
def save(self):
|
||||
"""Save session data."""
|
||||
try:
|
||||
@@ -223,12 +223,12 @@ class Session(object):
|
||||
cherrypy.log('Saving with expiry %s' % expiration_time,
|
||||
'TOOLS.SESSIONS')
|
||||
self._save(expiration_time)
|
||||
|
||||
|
||||
finally:
|
||||
if self.locked:
|
||||
# Always release the lock if the user didn't release it
|
||||
self.release_lock()
|
||||
|
||||
|
||||
def load(self):
|
||||
"""Copy stored session data into this session instance."""
|
||||
data = self._load()
|
||||
@@ -240,7 +240,7 @@ class Session(object):
|
||||
else:
|
||||
self._data = data[0]
|
||||
self.loaded = True
|
||||
|
||||
|
||||
# Stick the clean_thread in the class, not the instance.
|
||||
# The instances are created and destroyed per-request.
|
||||
cls = self.__class__
|
||||
@@ -253,23 +253,23 @@ class Session(object):
|
||||
t.subscribe()
|
||||
cls.clean_thread = t
|
||||
t.start()
|
||||
|
||||
|
||||
def delete(self):
|
||||
"""Delete stored session data."""
|
||||
self._delete()
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
return self._data[key]
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if not self.loaded: self.load()
|
||||
self._data[key] = value
|
||||
|
||||
|
||||
def __delitem__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
del self._data[key]
|
||||
|
||||
|
||||
def pop(self, key, default=missing):
|
||||
"""Remove the specified key and return the corresponding value.
|
||||
If key is not found, default is returned if given,
|
||||
@@ -280,47 +280,47 @@ class Session(object):
|
||||
return self._data.pop(key)
|
||||
else:
|
||||
return self._data.pop(key, default)
|
||||
|
||||
|
||||
def __contains__(self, key):
|
||||
if not self.loaded: self.load()
|
||||
return key in self._data
|
||||
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
def has_key(self, key):
|
||||
"""D.has_key(k) -> True if D has a key k, else False."""
|
||||
if not self.loaded: self.load()
|
||||
return key in self._data
|
||||
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.get(key, default)
|
||||
|
||||
|
||||
def update(self, d):
|
||||
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
|
||||
if not self.loaded: self.load()
|
||||
self._data.update(d)
|
||||
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.setdefault(key, default)
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""D.clear() -> None. Remove all items from D."""
|
||||
if not self.loaded: self.load()
|
||||
self._data.clear()
|
||||
|
||||
|
||||
def keys(self):
|
||||
"""D.keys() -> list of D's keys."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.keys()
|
||||
|
||||
|
||||
def items(self):
|
||||
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
|
||||
if not self.loaded: self.load()
|
||||
return self._data.items()
|
||||
|
||||
|
||||
def values(self):
|
||||
"""D.values() -> list of D's values."""
|
||||
if not self.loaded: self.load()
|
||||
@@ -328,11 +328,11 @@ class Session(object):
|
||||
|
||||
|
||||
class RamSession(Session):
|
||||
|
||||
|
||||
# Class-level objects. Don't rebind these!
|
||||
cache = {}
|
||||
locks = {}
|
||||
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
now = self.now()
|
||||
@@ -346,34 +346,34 @@ class RamSession(Session):
|
||||
del self.locks[id]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
# added to remove obsolete lock objects
|
||||
for id in list(self.locks):
|
||||
if id not in self.cache:
|
||||
self.locks.pop(id, None)
|
||||
|
||||
|
||||
def _exists(self):
|
||||
return self.id in self.cache
|
||||
|
||||
|
||||
def _load(self):
|
||||
return self.cache.get(self.id)
|
||||
|
||||
|
||||
def _save(self, expiration_time):
|
||||
self.cache[self.id] = (self._data, expiration_time)
|
||||
|
||||
|
||||
def _delete(self):
|
||||
self.cache.pop(self.id, None)
|
||||
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len(self.cache)
|
||||
@@ -381,35 +381,35 @@ class RamSession(Session):
|
||||
|
||||
class FileSession(Session):
|
||||
"""Implementation of the File backend for sessions
|
||||
|
||||
|
||||
storage_path
|
||||
The folder where session data will be saved. Each session
|
||||
will be saved as pickle.dump(data, expiration_time) in its own file;
|
||||
the filename will be self.SESSION_PREFIX + self.id.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
SESSION_PREFIX = 'session-'
|
||||
LOCK_SUFFIX = '.lock'
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
Session.__init__(self, id=id, **kwargs)
|
||||
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for file-based sessions.
|
||||
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(cls, k, v)
|
||||
|
||||
|
||||
# Warn if any lock files exist at startup.
|
||||
lockfiles = [fname for fname in os.listdir(cls.storage_path)
|
||||
if (fname.startswith(cls.SESSION_PREFIX)
|
||||
@@ -421,17 +421,17 @@ class FileSession(Session):
|
||||
"manually delete the lockfiles found at %r."
|
||||
% (len(lockfiles), plural, cls.storage_path))
|
||||
setup = classmethod(setup)
|
||||
|
||||
|
||||
def _get_file_path(self):
|
||||
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
|
||||
if not os.path.abspath(f).startswith(self.storage_path):
|
||||
raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
|
||||
return f
|
||||
|
||||
|
||||
def _exists(self):
|
||||
path = self._get_file_path()
|
||||
return os.path.exists(path)
|
||||
|
||||
|
||||
def _load(self, path=None):
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
@@ -443,20 +443,20 @@ class FileSession(Session):
|
||||
f.close()
|
||||
except (IOError, EOFError):
|
||||
return None
|
||||
|
||||
|
||||
def _save(self, expiration_time):
|
||||
f = open(self._get_file_path(), "wb")
|
||||
try:
|
||||
pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
def _delete(self):
|
||||
try:
|
||||
os.unlink(self._get_file_path())
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def acquire_lock(self, path=None):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
if path is None:
|
||||
@@ -468,17 +468,17 @@ class FileSession(Session):
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
os.close(lockfd)
|
||||
os.close(lockfd)
|
||||
break
|
||||
self.locked = True
|
||||
|
||||
|
||||
def release_lock(self, path=None):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
os.unlink(path + self.LOCK_SUFFIX)
|
||||
self.locked = False
|
||||
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
now = self.now()
|
||||
@@ -500,7 +500,7 @@ class FileSession(Session):
|
||||
os.unlink(path)
|
||||
finally:
|
||||
self.release_lock(path)
|
||||
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len([fname for fname in os.listdir(self.storage_path)
|
||||
@@ -517,40 +517,40 @@ class PostgresqlSession(Session):
|
||||
data text,
|
||||
expiration_time timestamp
|
||||
)
|
||||
|
||||
|
||||
You must provide your own get_db function.
|
||||
"""
|
||||
|
||||
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
Session.__init__(self, id, **kwargs)
|
||||
self.cursor = self.db.cursor()
|
||||
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for Postgres-based sessions.
|
||||
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
for k, v in kwargs.items():
|
||||
setattr(cls, k, v)
|
||||
|
||||
|
||||
self.db = self.get_db()
|
||||
setup = classmethod(setup)
|
||||
|
||||
|
||||
def __del__(self):
|
||||
if self.cursor:
|
||||
self.cursor.close()
|
||||
self.db.commit()
|
||||
|
||||
|
||||
def _exists(self):
|
||||
# Select session data from table
|
||||
self.cursor.execute('select data, expiration_time from session '
|
||||
'where id=%s', (self.id,))
|
||||
rows = self.cursor.fetchall()
|
||||
return bool(rows)
|
||||
|
||||
|
||||
def _load(self):
|
||||
# Select session data from table
|
||||
self.cursor.execute('select data, expiration_time from session '
|
||||
@@ -558,34 +558,34 @@ class PostgresqlSession(Session):
|
||||
rows = self.cursor.fetchall()
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
|
||||
pickled_data, expiration_time = rows[0]
|
||||
data = pickle.loads(pickled_data)
|
||||
return data, expiration_time
|
||||
|
||||
|
||||
def _save(self, expiration_time):
|
||||
pickled_data = pickle.dumps(self._data, self.pickle_protocol)
|
||||
self.cursor.execute('update session set data = %s, '
|
||||
'expiration_time = %s where id = %s',
|
||||
(pickled_data, expiration_time, self.id))
|
||||
|
||||
|
||||
def _delete(self):
|
||||
self.cursor.execute('delete from session where id=%s', (self.id,))
|
||||
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
# We use the "for update" clause to lock the row
|
||||
self.locked = True
|
||||
self.cursor.execute('select id from session where id=%s for update',
|
||||
(self.id,))
|
||||
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
# We just close the cursor and that will remove the lock
|
||||
# introduced by the "for update" clause
|
||||
self.cursor.close()
|
||||
self.locked = False
|
||||
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
self.cursor.execute('delete from session where expiration_time < %s',
|
||||
@@ -593,29 +593,29 @@ class PostgresqlSession(Session):
|
||||
|
||||
|
||||
class MemcachedSession(Session):
|
||||
|
||||
|
||||
# The most popular memcached client for Python isn't thread-safe.
|
||||
# Wrap all .get and .set operations in a single lock.
|
||||
mc_lock = threading.RLock()
|
||||
|
||||
|
||||
# This is a seperate set of locks per session id.
|
||||
locks = {}
|
||||
|
||||
|
||||
servers = ['127.0.0.1:11211']
|
||||
|
||||
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for memcached-based sessions.
|
||||
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
for k, v in kwargs.items():
|
||||
setattr(cls, k, v)
|
||||
|
||||
|
||||
import memcache
|
||||
cls.cache = memcache.Client(cls.servers)
|
||||
setup = classmethod(setup)
|
||||
|
||||
|
||||
def _get_id(self):
|
||||
return self._id
|
||||
def _set_id(self, value):
|
||||
@@ -628,21 +628,21 @@ class MemcachedSession(Session):
|
||||
for o in self.id_observers:
|
||||
o(value)
|
||||
id = property(_get_id, _set_id, doc="The current session ID.")
|
||||
|
||||
|
||||
def _exists(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return bool(self.cache.get(self.id))
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
|
||||
def _load(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return self.cache.get(self.id)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
|
||||
def _save(self, expiration_time):
|
||||
# Send the expiration time as "Unix time" (seconds since 1/1/1970)
|
||||
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)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
|
||||
def _delete(self):
|
||||
self.cache.delete(self.id)
|
||||
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
raise NotImplementedError
|
||||
@@ -675,17 +675,17 @@ class MemcachedSession(Session):
|
||||
|
||||
def save():
|
||||
"""Save any changed session data."""
|
||||
|
||||
|
||||
if not hasattr(cherrypy.serving, "session"):
|
||||
return
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(request, "_sessionsaved"):
|
||||
return
|
||||
request._sessionsaved = True
|
||||
|
||||
|
||||
if response.stream:
|
||||
# If the body is being streamed, we have to save the data
|
||||
# *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,
|
||||
persistent=True, httponly=False, debug=False, **kwargs):
|
||||
"""Initialize session object (using cookies).
|
||||
|
||||
|
||||
storage_type
|
||||
One of 'ram', 'file', 'postgresql', 'memcached'. This will be
|
||||
used to look up the corresponding class in cherrypy.lib.sessions
|
||||
globals. For example, 'file' will use the FileSession class.
|
||||
|
||||
|
||||
path
|
||||
The 'path' value to stick in the response cookie metadata.
|
||||
|
||||
|
||||
path_header
|
||||
If 'path' is None (the default), then the response
|
||||
cookie 'path' will be pulled from request.headers[path_header].
|
||||
|
||||
|
||||
name
|
||||
The name of the cookie.
|
||||
|
||||
|
||||
timeout
|
||||
The expiration timeout (in minutes) for the stored session data.
|
||||
If 'persistent' is True (the default), this is also the timeout
|
||||
for the cookie.
|
||||
|
||||
|
||||
domain
|
||||
The cookie domain.
|
||||
|
||||
|
||||
secure
|
||||
If False (the default) the cookie 'secure' value will not
|
||||
be set. If True, the cookie 'secure' value will be set (to 1).
|
||||
|
||||
|
||||
clean_freq (minutes)
|
||||
The poll rate for expired session cleanup.
|
||||
|
||||
|
||||
persistent
|
||||
If True (the default), the 'timeout' argument will be used
|
||||
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
|
||||
browser is closed.
|
||||
|
||||
|
||||
httponly
|
||||
If False (the default) the cookie 'httponly' value will not be set.
|
||||
If True, the cookie 'httponly' value will be set (to 1).
|
||||
|
||||
|
||||
Any additional kwargs will be bound to the new Session instance,
|
||||
and may be specific to the storage type. See the subclass of Session
|
||||
you're using for more information.
|
||||
"""
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(request, "_session_init_flag"):
|
||||
return
|
||||
request._session_init_flag = True
|
||||
|
||||
|
||||
# Check if request came with a session ID
|
||||
id = None
|
||||
if name in request.cookie:
|
||||
@@ -772,14 +772,14 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
|
||||
if debug:
|
||||
cherrypy.log('ID obtained from request.cookie: %r' % id,
|
||||
'TOOLS.SESSIONS')
|
||||
|
||||
|
||||
# Find the storage class and call setup (first time only).
|
||||
storage_class = storage_type.title() + 'Session'
|
||||
storage_class = globals()[storage_class]
|
||||
if not hasattr(cherrypy, "session"):
|
||||
if hasattr(storage_class, "setup"):
|
||||
storage_class.setup(**kwargs)
|
||||
|
||||
|
||||
# Create and attach a new Session instance to cherrypy.serving.
|
||||
# It will possess a reference to (and lock, and lazily load)
|
||||
# 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."""
|
||||
cherrypy.serving.response.cookie[name] = id
|
||||
sess.id_observers.append(update_cookie)
|
||||
|
||||
|
||||
# Create cherrypy.session which will proxy to cherrypy.serving.session
|
||||
if not hasattr(cherrypy, "session"):
|
||||
cherrypy.session = cherrypy._ThreadLocalProxy('session')
|
||||
|
||||
|
||||
if persistent:
|
||||
cookie_timeout = timeout
|
||||
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',
|
||||
timeout=60, domain=None, secure=False, httponly=False):
|
||||
"""Set a response cookie for the client.
|
||||
|
||||
|
||||
path
|
||||
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]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
|
||||
or '/')
|
||||
|
||||
|
||||
# We'd like to use the "max-age" param as indicated in
|
||||
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
|
||||
# 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):
|
||||
"""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.
|
||||
If not provided, the Content-Type will be guessed by the file extension
|
||||
of the 'path' argument.
|
||||
|
||||
|
||||
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 the basename of path. If disposition is None, no Content-Disposition
|
||||
header will be written.
|
||||
"""
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# If path is relative, users should fix it by making path absolute.
|
||||
# That is, CherryPy should not guess where the application root is.
|
||||
# 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:
|
||||
cherrypy.log(msg, 'TOOLS.STATICFILE')
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
if debug:
|
||||
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
|
||||
# Check if path is a directory.
|
||||
if stat.S_ISDIR(st.st_mode):
|
||||
# Let the caller deal with it as they like.
|
||||
if debug:
|
||||
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
|
||||
# Set the Last-Modified response header, so that
|
||||
# modified-since validation code can work.
|
||||
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
|
||||
cptools.validate_since()
|
||||
|
||||
|
||||
if content_type is None:
|
||||
# Set content-type based on filename extension
|
||||
ext = ""
|
||||
@@ -76,7 +76,7 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
|
||||
response.headers['Content-Type'] = content_type
|
||||
if debug:
|
||||
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
|
||||
|
||||
|
||||
cd = None
|
||||
if disposition is not 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
|
||||
if debug:
|
||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||
|
||||
|
||||
# Set Content-Length and use an iterable (file object)
|
||||
# this way CP won't load the whole file in memory
|
||||
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,
|
||||
debug=False):
|
||||
"""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.
|
||||
|
||||
|
||||
If disposition is not None, the Content-Disposition header will be set
|
||||
to "<disposition>; filename=<name>". If name is None, 'filename' 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
|
||||
position.
|
||||
"""
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
try:
|
||||
st = os.fstat(fileobj.fileno())
|
||||
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)
|
||||
cptools.validate_since()
|
||||
content_length = st.st_size
|
||||
|
||||
|
||||
if content_type is not None:
|
||||
response.headers['Content-Type'] = content_type
|
||||
if debug:
|
||||
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
|
||||
|
||||
|
||||
cd = None
|
||||
if disposition is not 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
|
||||
if debug:
|
||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||
|
||||
|
||||
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
|
||||
|
||||
def _serve_fileobj(fileobj, content_type, content_length, debug=False):
|
||||
"""Internal. Set response.body to the given file object, perhaps ranged."""
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
|
||||
request = cherrypy.serving.request
|
||||
if request.protocol >= (1, 1):
|
||||
@@ -160,7 +160,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
|
||||
if debug:
|
||||
cherrypy.log(message, 'TOOLS.STATIC')
|
||||
raise cherrypy.HTTPError(416, message)
|
||||
|
||||
|
||||
if r:
|
||||
if len(r) == 1:
|
||||
# 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:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers["Content-Length"]
|
||||
|
||||
|
||||
def file_ranges():
|
||||
# Apache compatibility:
|
||||
yield ntob("\r\n")
|
||||
|
||||
|
||||
for start, stop in r:
|
||||
if debug:
|
||||
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")
|
||||
# Final boundary
|
||||
yield ntob("--" + boundary + "--", 'ascii')
|
||||
|
||||
|
||||
# Apache compatibility:
|
||||
yield ntob("\r\n")
|
||||
response.body = file_ranges()
|
||||
@@ -219,7 +219,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
|
||||
else:
|
||||
if debug:
|
||||
cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
|
||||
|
||||
|
||||
# Set Content-Length and use an iterable (file object)
|
||||
# this way CP won't load the whole file in memory
|
||||
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="",
|
||||
debug=False):
|
||||
"""Serve a static resource from the given (root +) dir.
|
||||
|
||||
|
||||
match
|
||||
If given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
|
||||
content_types
|
||||
If given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
|
||||
|
||||
index
|
||||
If provided, it should be the (relative) name of a file to
|
||||
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:
|
||||
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
|
||||
return False
|
||||
|
||||
|
||||
if match and not re.search(match, request.path_info):
|
||||
if debug:
|
||||
cherrypy.log('request.path_info %r does not match pattern %r' %
|
||||
(request.path_info, match), 'TOOLS.STATICDIR')
|
||||
return False
|
||||
|
||||
|
||||
# Allow the use of '~' to refer to a user's home directory.
|
||||
dir = os.path.expanduser(dir)
|
||||
|
||||
@@ -295,7 +295,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
|
||||
cherrypy.log(msg, 'TOOLS.STATICDIR')
|
||||
raise ValueError(msg)
|
||||
dir = os.path.join(root, dir)
|
||||
|
||||
|
||||
# Determine where we are in the object tree relative to 'section'
|
||||
# (where the static tool was defined).
|
||||
if section == 'global':
|
||||
@@ -303,19 +303,19 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
|
||||
section = section.rstrip(r"\/")
|
||||
branch = request.path_info[len(section) + 1:]
|
||||
branch = unquote(branch.lstrip(r"\/"))
|
||||
|
||||
|
||||
# If branch is "", filename will end in a slash
|
||||
filename = os.path.join(dir, branch)
|
||||
if debug:
|
||||
cherrypy.log('Checking file %r to fulfill %r' %
|
||||
(filename, request.path_info), 'TOOLS.STATICDIR')
|
||||
|
||||
|
||||
# There's a chance that the branch pulled from the URL might
|
||||
# have ".." or similar uplevel attacks in it. Check that the final
|
||||
# filename is a child of dir.
|
||||
if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
|
||||
raise cherrypy.HTTPError(403) # Forbidden
|
||||
|
||||
|
||||
handled = _attempt(filename, content_types)
|
||||
if not handled:
|
||||
# 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):
|
||||
"""Serve a static resource from the given (root +) filename.
|
||||
|
||||
|
||||
match
|
||||
If given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
|
||||
content_types
|
||||
If given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if request.method not in ('GET', 'HEAD'):
|
||||
if debug:
|
||||
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
|
||||
return False
|
||||
|
||||
|
||||
if match and not re.search(match, request.path_info):
|
||||
if debug:
|
||||
cherrypy.log('request.path_info %r does not match pattern %r' %
|
||||
(request.path_info, match), 'TOOLS.STATICFILE')
|
||||
return False
|
||||
|
||||
|
||||
# If filename is relative, make absolute using "root".
|
||||
if not os.path.isabs(filename):
|
||||
if not root:
|
||||
@@ -359,5 +359,5 @@ def staticfile(filename, root=None, match="", content_types=None, debug=False):
|
||||
cherrypy.log(msg, 'TOOLS.STATICFILE')
|
||||
raise ValueError(msg)
|
||||
filename = os.path.join(root, filename)
|
||||
|
||||
|
||||
return _attempt(filename, content_types, debug=debug)
|
||||
|
||||
@@ -30,13 +30,13 @@ _module__file__base = os.getcwd()
|
||||
|
||||
class SimplePlugin(object):
|
||||
"""Plugin base class which auto-subscribes methods for known channels."""
|
||||
|
||||
|
||||
bus = None
|
||||
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
|
||||
def subscribe(self):
|
||||
"""Register this object as a (multi-channel) listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
@@ -44,7 +44,7 @@ class SimplePlugin(object):
|
||||
method = getattr(self, channel, None)
|
||||
if method is not None:
|
||||
self.bus.subscribe(channel, method)
|
||||
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unregister this object as a listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
@@ -57,39 +57,39 @@ class SimplePlugin(object):
|
||||
|
||||
class SignalHandler(object):
|
||||
"""Register bus channels (and listeners) for system signals.
|
||||
|
||||
|
||||
You can modify what signals your application listens for, and what it does
|
||||
when it receives signals, by modifying :attr:`SignalHandler.handlers`,
|
||||
a dict of {signal name: callback} pairs. The default set is::
|
||||
|
||||
|
||||
handlers = {'SIGTERM': self.bus.exit,
|
||||
'SIGHUP': self.handle_SIGHUP,
|
||||
'SIGUSR1': self.bus.graceful,
|
||||
}
|
||||
|
||||
|
||||
The :func:`SignalHandler.handle_SIGHUP`` method calls
|
||||
:func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
|
||||
if the process is daemonized, but
|
||||
:func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
|
||||
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.
|
||||
|
||||
|
||||
Feel free to add signals which are not available on every platform. The
|
||||
:class:`SignalHandler` will ignore errors raised from attempting to register
|
||||
handlers for unknown signals.
|
||||
"""
|
||||
|
||||
|
||||
handlers = {}
|
||||
"""A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
|
||||
|
||||
|
||||
signals = {}
|
||||
"""A map from signal numbers to names."""
|
||||
|
||||
|
||||
for k, v in vars(_signal).items():
|
||||
if k.startswith('SIG') and not k.startswith('SIG_'):
|
||||
signals[v] = k
|
||||
del k, v
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
# Set default handlers
|
||||
@@ -106,12 +106,12 @@ class SignalHandler(object):
|
||||
self.handlers['SIGINT'] = self._jython_SIGINT_handler
|
||||
|
||||
self._previous_handlers = {}
|
||||
|
||||
|
||||
def _jython_SIGINT_handler(self, signum=None, frame=None):
|
||||
# See http://bugs.jython.org/issue1313
|
||||
self.bus.log('Keyboard Interrupt: shutting down bus')
|
||||
self.bus.exit()
|
||||
|
||||
|
||||
def subscribe(self):
|
||||
"""Subscribe self.handlers to signals."""
|
||||
for sig, func in self.handlers.items():
|
||||
@@ -119,18 +119,18 @@ class SignalHandler(object):
|
||||
self.set_handler(sig, func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unsubscribe self.handlers from signals."""
|
||||
for signum, handler in self._previous_handlers.items():
|
||||
signame = self.signals[signum]
|
||||
|
||||
|
||||
if handler is None:
|
||||
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
|
||||
handler = _signal.SIG_DFL
|
||||
else:
|
||||
self.bus.log("Restoring %s handler %r." % (signame, handler))
|
||||
|
||||
|
||||
try:
|
||||
our_handler = _signal.signal(signum, handler)
|
||||
if our_handler is None:
|
||||
@@ -140,13 +140,13 @@ class SignalHandler(object):
|
||||
except ValueError:
|
||||
self.bus.log("Unable to restore %s handler %r." %
|
||||
(signame, handler), level=40, traceback=True)
|
||||
|
||||
|
||||
def set_handler(self, signal, listener=None):
|
||||
"""Subscribe a handler for the given signal (number or name).
|
||||
|
||||
|
||||
If the optional 'listener' argument is provided, it will be
|
||||
subscribed as a listener for the given signal's channel.
|
||||
|
||||
|
||||
If the given signal name or number is not available on the current
|
||||
platform, ValueError is raised.
|
||||
"""
|
||||
@@ -161,20 +161,20 @@ class SignalHandler(object):
|
||||
except KeyError:
|
||||
raise ValueError("No such signal: %r" % signal)
|
||||
signum = signal
|
||||
|
||||
|
||||
prev = _signal.signal(signum, self._handle_signal)
|
||||
self._previous_handlers[signum] = prev
|
||||
|
||||
|
||||
if listener is not None:
|
||||
self.bus.log("Listening for %s." % signame)
|
||||
self.bus.subscribe(signame, listener)
|
||||
|
||||
|
||||
def _handle_signal(self, signum=None, frame=None):
|
||||
"""Python signal handler (self.set_handler subscribes it for you)."""
|
||||
signame = self.signals[signum]
|
||||
self.bus.log("Caught signal %s." % signame)
|
||||
self.bus.publish(signame)
|
||||
|
||||
|
||||
def handle_SIGHUP(self):
|
||||
"""Restart if daemonized, else exit."""
|
||||
if os.isatty(sys.stdin.fileno()):
|
||||
@@ -194,17 +194,17 @@ except ImportError:
|
||||
|
||||
class DropPrivileges(SimplePlugin):
|
||||
"""Drop privileges. uid/gid arguments not available on Windows.
|
||||
|
||||
|
||||
Special thanks to Gavin Baker: http://antonym.org/node/100.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, bus, umask=None, uid=None, gid=None):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.finalized = False
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.umask = umask
|
||||
|
||||
|
||||
def _get_uid(self):
|
||||
return self._uid
|
||||
def _set_uid(self, val):
|
||||
@@ -218,7 +218,7 @@ class DropPrivileges(SimplePlugin):
|
||||
self._uid = val
|
||||
uid = property(_get_uid, _set_uid,
|
||||
doc="The uid under which to run. Availability: Unix.")
|
||||
|
||||
|
||||
def _get_gid(self):
|
||||
return self._gid
|
||||
def _set_gid(self, val):
|
||||
@@ -232,7 +232,7 @@ class DropPrivileges(SimplePlugin):
|
||||
self._gid = val
|
||||
gid = property(_get_gid, _set_gid,
|
||||
doc="The gid under which to run. Availability: Unix.")
|
||||
|
||||
|
||||
def _get_umask(self):
|
||||
return self._umask
|
||||
def _set_umask(self, val):
|
||||
@@ -246,11 +246,11 @@ class DropPrivileges(SimplePlugin):
|
||||
self._umask = val
|
||||
umask = property(_get_umask, _set_umask,
|
||||
doc="""The default permission mode for newly created files and directories.
|
||||
|
||||
|
||||
Usually expressed in octal format, for example, ``0644``.
|
||||
Availability: Unix, Windows.
|
||||
""")
|
||||
|
||||
|
||||
def start(self):
|
||||
# uid/gid
|
||||
def current_ids():
|
||||
@@ -261,7 +261,7 @@ class DropPrivileges(SimplePlugin):
|
||||
if grp:
|
||||
group = grp.getgrgid(os.getgid())[0]
|
||||
return name, group
|
||||
|
||||
|
||||
if self.finalized:
|
||||
if not (self.uid is None and self.gid is None):
|
||||
self.bus.log('Already running as uid: %r gid: %r' %
|
||||
@@ -278,7 +278,7 @@ class DropPrivileges(SimplePlugin):
|
||||
if self.uid is not None:
|
||||
os.setuid(self.uid)
|
||||
self.bus.log('Running as uid: %r gid: %r' % current_ids())
|
||||
|
||||
|
||||
# umask
|
||||
if self.finalized:
|
||||
if self.umask is not None:
|
||||
@@ -290,7 +290,7 @@ class DropPrivileges(SimplePlugin):
|
||||
old_umask = os.umask(self.umask)
|
||||
self.bus.log('umask old: %03o, new: %03o' %
|
||||
(old_umask, self.umask))
|
||||
|
||||
|
||||
self.finalized = True
|
||||
# This is slightly higher than the priority for server.start
|
||||
# in order to facilitate the most common use: starting on a low
|
||||
@@ -300,11 +300,11 @@ class DropPrivileges(SimplePlugin):
|
||||
|
||||
class Daemonizer(SimplePlugin):
|
||||
"""Daemonize the running script.
|
||||
|
||||
|
||||
Use this with a Web Site Process Bus via::
|
||||
|
||||
|
||||
Daemonizer(bus).subscribe()
|
||||
|
||||
|
||||
When this component finishes, the process is completely decoupled from
|
||||
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
|
||||
@@ -314,7 +314,7 @@ class Daemonizer(SimplePlugin):
|
||||
of whether the process fully started. In fact, that return code only
|
||||
indicates if the process succesfully finished the first fork.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
|
||||
stderr='/dev/null'):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
@@ -322,11 +322,11 @@ class Daemonizer(SimplePlugin):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.finalized = False
|
||||
|
||||
|
||||
def start(self):
|
||||
if self.finalized:
|
||||
self.bus.log('Already deamonized.')
|
||||
|
||||
|
||||
# forking has issues with threads:
|
||||
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
|
||||
# "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. '
|
||||
'Daemonizing now may cause strange failures.' %
|
||||
threading.enumerate(), level=30)
|
||||
|
||||
|
||||
# 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)
|
||||
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
|
||||
|
||||
|
||||
# Finish up with the current stdout/stderr
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
# Do first fork.
|
||||
try:
|
||||
pid = os.fork()
|
||||
@@ -360,9 +360,9 @@ class Daemonizer(SimplePlugin):
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit("%s: fork #1 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
|
||||
os.setsid()
|
||||
|
||||
|
||||
# Do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
@@ -373,10 +373,10 @@ class Daemonizer(SimplePlugin):
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit("%s: fork #2 failed: (%d) %s\n"
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
|
||||
os.chdir("/")
|
||||
os.umask(0)
|
||||
|
||||
|
||||
si = open(self.stdin, "r")
|
||||
so = open(self.stdout, "a+")
|
||||
se = open(self.stderr, "a+")
|
||||
@@ -387,7 +387,7 @@ class Daemonizer(SimplePlugin):
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
|
||||
self.bus.log('Daemonized to PID: %s' % os.getpid())
|
||||
self.finalized = True
|
||||
start.priority = 65
|
||||
@@ -395,12 +395,12 @@ class Daemonizer(SimplePlugin):
|
||||
|
||||
class PIDFile(SimplePlugin):
|
||||
"""Maintain a PID file via a WSPBus."""
|
||||
|
||||
|
||||
def __init__(self, bus, pidfile):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.pidfile = pidfile
|
||||
self.finalized = False
|
||||
|
||||
|
||||
def start(self):
|
||||
pid = os.getpid()
|
||||
if self.finalized:
|
||||
@@ -410,7 +410,7 @@ class PIDFile(SimplePlugin):
|
||||
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
|
||||
self.finalized = True
|
||||
start.priority = 70
|
||||
|
||||
|
||||
def exit(self):
|
||||
try:
|
||||
os.remove(self.pidfile)
|
||||
@@ -423,12 +423,12 @@ class PIDFile(SimplePlugin):
|
||||
|
||||
class PerpetualTimer(threading._Timer):
|
||||
"""A responsive subclass of threading._Timer whose run() method repeats.
|
||||
|
||||
|
||||
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
|
||||
results in pretty high CPU usage
|
||||
results in pretty high CPU usage
|
||||
"""
|
||||
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.finished.wait(self.interval)
|
||||
@@ -445,14 +445,14 @@ class PerpetualTimer(threading._Timer):
|
||||
|
||||
class BackgroundTask(threading.Thread):
|
||||
"""A subclass of threading.Thread whose run() method repeats.
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
it won't delay stopping the whole process.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
|
||||
threading.Thread.__init__(self)
|
||||
self.interval = interval
|
||||
@@ -461,10 +461,10 @@ class BackgroundTask(threading.Thread):
|
||||
self.kwargs = kwargs
|
||||
self.running = False
|
||||
self.bus = bus
|
||||
|
||||
|
||||
def cancel(self):
|
||||
self.running = False
|
||||
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
while self.running:
|
||||
@@ -479,30 +479,30 @@ class BackgroundTask(threading.Thread):
|
||||
% self.function, level=40, traceback=True)
|
||||
# Quit on first error to avoid massive logs.
|
||||
raise
|
||||
|
||||
|
||||
def _set_daemon(self):
|
||||
return True
|
||||
|
||||
|
||||
class Monitor(SimplePlugin):
|
||||
"""WSPBus listener to periodically run a callback in its own thread."""
|
||||
|
||||
|
||||
callback = None
|
||||
"""The function to call at intervals."""
|
||||
|
||||
|
||||
frequency = 60
|
||||
"""The time in seconds between callback runs."""
|
||||
|
||||
|
||||
thread = None
|
||||
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
|
||||
|
||||
|
||||
def __init__(self, bus, callback, frequency=60, name=None):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.callback = callback
|
||||
self.frequency = frequency
|
||||
self.thread = None
|
||||
self.name = name
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start our callback in its own background thread."""
|
||||
if self.frequency > 0:
|
||||
@@ -516,7 +516,7 @@ class Monitor(SimplePlugin):
|
||||
else:
|
||||
self.bus.log("Monitor thread %r already started." % threadname)
|
||||
start.priority = 70
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop our callback's background task thread."""
|
||||
if self.thread is None:
|
||||
@@ -530,7 +530,7 @@ class Monitor(SimplePlugin):
|
||||
self.thread.join()
|
||||
self.bus.log("Stopped thread %r." % name)
|
||||
self.thread = None
|
||||
|
||||
|
||||
def graceful(self):
|
||||
"""Stop the callback's background task thread and restart it."""
|
||||
self.stop()
|
||||
@@ -539,47 +539,47 @@ class Monitor(SimplePlugin):
|
||||
|
||||
class Autoreloader(Monitor):
|
||||
"""Monitor which re-executes the process when files change.
|
||||
|
||||
|
||||
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
|
||||
autoreloader monitors all imported modules; you can add to the
|
||||
set by adding to ``autoreload.files``::
|
||||
|
||||
|
||||
cherrypy.engine.autoreload.files.add(myFile)
|
||||
|
||||
|
||||
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
|
||||
cherrypy itself::
|
||||
|
||||
|
||||
cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
|
||||
|
||||
|
||||
Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
|
||||
the autoreload plugin takes a ``frequency`` argument. The default is
|
||||
1 second; that is, the autoreloader will examine files once each second.
|
||||
"""
|
||||
|
||||
|
||||
files = None
|
||||
"""The set of files to poll for modifications."""
|
||||
|
||||
|
||||
frequency = 1
|
||||
"""The interval in seconds at which to poll for modified files."""
|
||||
|
||||
|
||||
match = '.*'
|
||||
"""A regular expression by which to match filenames."""
|
||||
|
||||
|
||||
def __init__(self, bus, frequency=1, match='.*'):
|
||||
self.mtimes = {}
|
||||
self.files = set()
|
||||
self.match = match
|
||||
Monitor.__init__(self, bus, self.run, frequency)
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start our own background task thread for self.run."""
|
||||
if self.thread is None:
|
||||
self.mtimes = {}
|
||||
Monitor.start(self)
|
||||
start.priority = 70
|
||||
|
||||
start.priority = 70
|
||||
|
||||
def sysfiles(self):
|
||||
"""Return a Set of sys.modules filenames to monitor."""
|
||||
files = set()
|
||||
@@ -594,25 +594,25 @@ class Autoreloader(Monitor):
|
||||
f = os.path.normpath(os.path.join(_module__file__base, f))
|
||||
files.add(f)
|
||||
return files
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Reload the process if registered files have been modified."""
|
||||
for filename in self.sysfiles() | self.files:
|
||||
if filename:
|
||||
if filename.endswith('.pyc'):
|
||||
filename = filename[:-1]
|
||||
|
||||
|
||||
oldtime = self.mtimes.get(filename, 0)
|
||||
if oldtime is None:
|
||||
# Module with no .py file. Skip it.
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
# Either a module with no .py file, or it's been deleted.
|
||||
mtime = None
|
||||
|
||||
|
||||
if filename not in self.mtimes:
|
||||
# If a module has no .py file, this will be None.
|
||||
self.mtimes[filename] = mtime
|
||||
@@ -628,12 +628,12 @@ class Autoreloader(Monitor):
|
||||
|
||||
class ThreadManager(SimplePlugin):
|
||||
"""Manager for HTTP request threads.
|
||||
|
||||
|
||||
If you have control over thread creation and destruction, publish to
|
||||
the 'acquire_thread' and 'release_thread' channels (for each thread).
|
||||
This will register/unregister the current thread and publish to
|
||||
'start_thread' and 'stop_thread' listeners in the bus as needed.
|
||||
|
||||
|
||||
If threads are created and destroyed by code you do not control
|
||||
(e.g., Apache), then, at the beginning of every HTTP request,
|
||||
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
|
||||
'stop_thread' listeners for you when it stops.
|
||||
"""
|
||||
|
||||
|
||||
threads = None
|
||||
"""A map of {thread ident: index number} pairs."""
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.threads = {}
|
||||
SimplePlugin.__init__(self, bus)
|
||||
@@ -655,7 +655,7 @@ class ThreadManager(SimplePlugin):
|
||||
|
||||
def acquire_thread(self):
|
||||
"""Run 'start_thread' listeners for the current thread.
|
||||
|
||||
|
||||
If the current thread has already been seen, any 'start_thread'
|
||||
listeners will not be run again.
|
||||
"""
|
||||
@@ -666,14 +666,14 @@ class ThreadManager(SimplePlugin):
|
||||
i = len(self.threads) + 1
|
||||
self.threads[thread_ident] = i
|
||||
self.bus.publish('start_thread', i)
|
||||
|
||||
|
||||
def release_thread(self):
|
||||
"""Release the current thread and run 'stop_thread' listeners."""
|
||||
thread_ident = get_thread_ident()
|
||||
i = self.threads.pop(thread_ident, None)
|
||||
if i is not None:
|
||||
self.bus.publish('stop_thread', i)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Release all threads and run all 'stop_thread' listeners."""
|
||||
for thread_ident, i in self.threads.items():
|
||||
|
||||
@@ -54,13 +54,13 @@ hello.py::
|
||||
|
||||
#!/usr/bin/python
|
||||
import cherrypy
|
||||
|
||||
|
||||
class HelloWorld:
|
||||
\"""Sample request handler class.\"""
|
||||
def index(self):
|
||||
return "Hello world!"
|
||||
index.exposed = True
|
||||
|
||||
|
||||
cherrypy.tree.mount(HelloWorld())
|
||||
# CherryPy autoreload must be disabled for the flup server to work
|
||||
cherrypy.config.update({'engine.autoreload_on':False})
|
||||
@@ -107,7 +107,7 @@ directive, configure your fastcgi script like the following::
|
||||
} # end of $HTTP["url"] =~ "^/"
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -117,33 +117,33 @@ import time
|
||||
|
||||
class ServerAdapter(object):
|
||||
"""Adapter for an HTTP server.
|
||||
|
||||
|
||||
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
|
||||
start them all with bus.start:
|
||||
|
||||
|
||||
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
|
||||
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
|
||||
s1.subscribe()
|
||||
s2.subscribe()
|
||||
bus.start()
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, bus, httpserver=None, bind_addr=None):
|
||||
self.bus = bus
|
||||
self.httpserver = httpserver
|
||||
self.bind_addr = bind_addr
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
|
||||
def subscribe(self):
|
||||
self.bus.subscribe('start', self.start)
|
||||
self.bus.subscribe('stop', self.stop)
|
||||
|
||||
|
||||
def unsubscribe(self):
|
||||
self.bus.unsubscribe('start', self.start)
|
||||
self.bus.unsubscribe('stop', self.stop)
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if self.bind_addr is None:
|
||||
@@ -153,29 +153,29 @@ class ServerAdapter(object):
|
||||
on_what = "%s:%s" % (host, port)
|
||||
else:
|
||||
on_what = "socket file: %s" % self.bind_addr
|
||||
|
||||
|
||||
if self.running:
|
||||
self.bus.log("Already serving on %s" % on_what)
|
||||
return
|
||||
|
||||
|
||||
self.interrupt = None
|
||||
if not self.httpserver:
|
||||
raise ValueError("No HTTP server has been created.")
|
||||
|
||||
|
||||
# Start the httpserver in a new thread.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
|
||||
|
||||
import threading
|
||||
t = threading.Thread(target=self._start_http_thread)
|
||||
t.setName("HTTPServer " + t.getName())
|
||||
t.start()
|
||||
|
||||
|
||||
self.wait()
|
||||
self.running = True
|
||||
self.bus.log("Serving on %s" % on_what)
|
||||
start.priority = 75
|
||||
|
||||
|
||||
def _start_http_thread(self):
|
||||
"""HTTP servers MUST be running in new threads, so that the
|
||||
main thread persists to receive KeyboardInterrupt's. If an
|
||||
@@ -200,19 +200,19 @@ class ServerAdapter(object):
|
||||
traceback=True, level=40)
|
||||
self.bus.exit()
|
||||
raise
|
||||
|
||||
|
||||
def wait(self):
|
||||
"""Wait until the HTTP server is ready to receive requests."""
|
||||
while not getattr(self.httpserver, "ready", False):
|
||||
if self.interrupt:
|
||||
raise self.interrupt
|
||||
time.sleep(.1)
|
||||
|
||||
|
||||
# Wait for port to be occupied
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
host, port = self.bind_addr
|
||||
wait_for_occupied_port(host, port)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
if self.running:
|
||||
@@ -226,7 +226,7 @@ class ServerAdapter(object):
|
||||
else:
|
||||
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
|
||||
stop.priority = 25
|
||||
|
||||
|
||||
def restart(self):
|
||||
"""Restart the HTTP server."""
|
||||
self.stop()
|
||||
@@ -235,22 +235,22 @@ class ServerAdapter(object):
|
||||
|
||||
class FlupCGIServer(object):
|
||||
"""Adapter for a flup.server.cgi.WSGIServer."""
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the CGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.cgi import WSGIServer
|
||||
|
||||
|
||||
self.cgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
self.ready = True
|
||||
self.cgiserver.run()
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
self.ready = False
|
||||
@@ -258,7 +258,7 @@ class FlupCGIServer(object):
|
||||
|
||||
class FlupFCGIServer(object):
|
||||
"""Adapter for a flup.server.fcgi.WSGIServer."""
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get('bindAddress', None) is None:
|
||||
import socket
|
||||
@@ -270,7 +270,7 @@ class FlupFCGIServer(object):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the FCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
@@ -290,7 +290,7 @@ class FlupFCGIServer(object):
|
||||
self.fcgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.fcgiserver.run()
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
# Forcibly stop the fcgi server main event loop.
|
||||
@@ -302,12 +302,12 @@ class FlupFCGIServer(object):
|
||||
|
||||
class FlupSCGIServer(object):
|
||||
"""Adapter for a flup.server.scgi.WSGIServer."""
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the SCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
@@ -327,7 +327,7 @@ class FlupSCGIServer(object):
|
||||
self.scgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.scgiserver.run()
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
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.")
|
||||
host = client_host(host)
|
||||
port = int(port)
|
||||
|
||||
|
||||
import socket
|
||||
|
||||
|
||||
# AF_INET or AF_INET6 socket
|
||||
# Get the correct address family for our host (allows IPv6 addresses)
|
||||
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))]
|
||||
else:
|
||||
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
|
||||
|
||||
|
||||
for res in info:
|
||||
af, socktype, proto, canonname, sa = res
|
||||
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.")
|
||||
if timeout is None:
|
||||
timeout = free_port_timeout
|
||||
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
# 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)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
raise IOError("Port %r not free on %r" % (port, host))
|
||||
|
||||
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.")
|
||||
if timeout is None:
|
||||
timeout = occupied_port_timeout
|
||||
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
check_port(host, port, timeout=timeout)
|
||||
@@ -423,5 +423,5 @@ def wait_for_occupied_port(host, port, timeout=None):
|
||||
return
|
||||
else:
|
||||
time.sleep(timeout)
|
||||
|
||||
|
||||
raise IOError("Port %r not bound on %r" % (port, host))
|
||||
|
||||
@@ -12,16 +12,16 @@ from cherrypy.process import wspbus, plugins
|
||||
|
||||
class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.is_set = False
|
||||
plugins.SimplePlugin.__init__(self, bus)
|
||||
|
||||
|
||||
def start(self):
|
||||
if self.is_set:
|
||||
self.bus.log('Handler for console events already set.', level=40)
|
||||
return
|
||||
|
||||
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
|
||||
if result == 0:
|
||||
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
|
||||
@@ -29,38 +29,38 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
else:
|
||||
self.bus.log('Set handler for console events.', level=40)
|
||||
self.is_set = True
|
||||
|
||||
|
||||
def stop(self):
|
||||
if not self.is_set:
|
||||
self.bus.log('Handler for console events already off.', level=40)
|
||||
return
|
||||
|
||||
|
||||
try:
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
|
||||
except ValueError:
|
||||
# "ValueError: The object has not been registered"
|
||||
result = 1
|
||||
|
||||
|
||||
if result == 0:
|
||||
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
|
||||
win32api.GetLastError(), level=40)
|
||||
else:
|
||||
self.bus.log('Removed handler for console events.', level=40)
|
||||
self.is_set = False
|
||||
|
||||
|
||||
def handle(self, event):
|
||||
"""Handle console control events (like Ctrl-C)."""
|
||||
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
|
||||
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
|
||||
win32con.CTRL_CLOSE_EVENT):
|
||||
self.bus.log('Console event %s: shutting down bus' % event)
|
||||
|
||||
|
||||
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
|
||||
try:
|
||||
self.stop()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
self.bus.exit()
|
||||
# 'First to return True stops the calls'
|
||||
return 1
|
||||
@@ -69,14 +69,14 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
|
||||
class Win32Bus(wspbus.Bus):
|
||||
"""A Web Site Process Bus implementation for Win32.
|
||||
|
||||
|
||||
Instead of time.sleep, this bus blocks using native win32event objects.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.events = {}
|
||||
wspbus.Bus.__init__(self)
|
||||
|
||||
|
||||
def _get_state_event(self, state):
|
||||
"""Return a win32event for the given state (creating it if needed)."""
|
||||
try:
|
||||
@@ -87,7 +87,7 @@ class Win32Bus(wspbus.Bus):
|
||||
(state.name, os.getpid()))
|
||||
self.events[state] = event
|
||||
return event
|
||||
|
||||
|
||||
def _get_state(self):
|
||||
return self._state
|
||||
def _set_state(self, value):
|
||||
@@ -95,10 +95,10 @@ class Win32Bus(wspbus.Bus):
|
||||
event = self._get_state_event(value)
|
||||
win32event.PulseEvent(event)
|
||||
state = property(_get_state, _set_state)
|
||||
|
||||
|
||||
def wait(self, state, interval=0.1, channel=None):
|
||||
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
|
||||
|
||||
|
||||
Since this class uses native win32event objects, the interval
|
||||
argument is ignored.
|
||||
"""
|
||||
@@ -116,15 +116,15 @@ class Win32Bus(wspbus.Bus):
|
||||
|
||||
class _ControlCodes(dict):
|
||||
"""Control codes used to "signal" a service via ControlService.
|
||||
|
||||
|
||||
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:
|
||||
|
||||
|
||||
>>> signal.SIGUSR1
|
||||
10
|
||||
control_codes['graceful'] = 128 + 10
|
||||
"""
|
||||
|
||||
|
||||
def key_for(self, obj):
|
||||
"""For the given value, return its corresponding key."""
|
||||
for key, val in self.items():
|
||||
@@ -146,26 +146,26 @@ def signal_child(service, command):
|
||||
|
||||
class PyWebService(win32serviceutil.ServiceFramework):
|
||||
"""Python Web Service."""
|
||||
|
||||
|
||||
_svc_name_ = "Python Web Service"
|
||||
_svc_display_name_ = "Python Web Service"
|
||||
_svc_deps_ = None # sequence of service names on which this depends
|
||||
_exe_name_ = "pywebsvc"
|
||||
_exe_args_ = None # Default to no arguments
|
||||
|
||||
|
||||
# Only exists on Windows 2000 or later, ignored on windows NT
|
||||
_svc_description_ = "Python Web Service"
|
||||
|
||||
|
||||
def SvcDoRun(self):
|
||||
from cherrypy import process
|
||||
process.bus.start()
|
||||
process.bus.block()
|
||||
|
||||
|
||||
def SvcStop(self):
|
||||
from cherrypy import process
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
process.bus.exit()
|
||||
|
||||
|
||||
def SvcOther(self, control):
|
||||
process.bus.publish(control_codes.key_for(control))
|
||||
|
||||
|
||||
@@ -81,21 +81,21 @@ _startup_cwd = os.getcwd()
|
||||
class ChannelFailures(Exception):
|
||||
"""Exception raised when errors occur in a listener during Bus.publish()."""
|
||||
delimiter = '\n'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Don't use 'super' here; Exceptions are old-style in Py2.4
|
||||
# See http://www.cherrypy.org/ticket/959
|
||||
Exception.__init__(self, *args, **kwargs)
|
||||
self._exceptions = list()
|
||||
|
||||
|
||||
def handle_exception(self):
|
||||
"""Append the current exception to self."""
|
||||
self._exceptions.append(sys.exc_info()[1])
|
||||
|
||||
|
||||
def get_instances(self):
|
||||
"""Return a list of seen exception instances."""
|
||||
return self._exceptions[:]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
exception_strings = map(repr, self.get_instances())
|
||||
return self.delimiter.join(exception_strings)
|
||||
@@ -112,7 +112,7 @@ class _StateEnum(object):
|
||||
name = None
|
||||
def __repr__(self):
|
||||
return "states.%s" % self.name
|
||||
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, self.State):
|
||||
value.name = key
|
||||
@@ -138,19 +138,19 @@ else:
|
||||
|
||||
class Bus(object):
|
||||
"""Process state-machine and messenger for HTTP site deployment.
|
||||
|
||||
|
||||
All listeners for a given channel are guaranteed to be called even
|
||||
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
|
||||
processing from inside a listener is to raise SystemExit and stop the
|
||||
whole server.
|
||||
"""
|
||||
|
||||
|
||||
states = states
|
||||
state = states.STOPPED
|
||||
execv = False
|
||||
max_cloexec_files = max_files
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.execv = False
|
||||
self.state = states.STOPPED
|
||||
@@ -158,32 +158,32 @@ class Bus(object):
|
||||
[(channel, set()) for channel
|
||||
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
|
||||
self._priorities = {}
|
||||
|
||||
|
||||
def subscribe(self, channel, callback, priority=None):
|
||||
"""Add the given callback at the given channel (if not present)."""
|
||||
if channel not in self.listeners:
|
||||
self.listeners[channel] = set()
|
||||
self.listeners[channel].add(callback)
|
||||
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, 'priority', 50)
|
||||
self._priorities[(channel, callback)] = priority
|
||||
|
||||
|
||||
def unsubscribe(self, channel, callback):
|
||||
"""Discard the given callback (if present)."""
|
||||
listeners = self.listeners.get(channel)
|
||||
if listeners and callback in listeners:
|
||||
listeners.discard(callback)
|
||||
del self._priorities[(channel, callback)]
|
||||
|
||||
|
||||
def publish(self, channel, *args, **kwargs):
|
||||
"""Return output of all subscribers for the given channel."""
|
||||
if channel not in self.listeners:
|
||||
return []
|
||||
|
||||
|
||||
exc = ChannelFailures()
|
||||
output = []
|
||||
|
||||
|
||||
items = [(self._priorities[(channel, listener)], listener)
|
||||
for listener in self.listeners[channel]]
|
||||
try:
|
||||
@@ -214,7 +214,7 @@ class Bus(object):
|
||||
if exc:
|
||||
raise exc
|
||||
return output
|
||||
|
||||
|
||||
def _clean_exit(self):
|
||||
"""An atexit handler which asserts the Bus is not running."""
|
||||
if self.state != states.EXITING:
|
||||
@@ -224,11 +224,11 @@ class Bus(object):
|
||||
"bus.block() after start(), or call bus.exit() before the "
|
||||
"main thread exits." % self.state, RuntimeWarning)
|
||||
self.exit()
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start all services."""
|
||||
atexit.register(self._clean_exit)
|
||||
|
||||
|
||||
self.state = states.STARTING
|
||||
self.log('Bus STARTING')
|
||||
try:
|
||||
@@ -248,13 +248,13 @@ class Bus(object):
|
||||
pass
|
||||
# Re-raise the original error
|
||||
raise e_info
|
||||
|
||||
|
||||
def exit(self):
|
||||
"""Stop all services and prepare to exit the process."""
|
||||
exitstate = self.state
|
||||
try:
|
||||
self.stop()
|
||||
|
||||
|
||||
self.state = states.EXITING
|
||||
self.log('Bus EXITING')
|
||||
self.publish('exit')
|
||||
@@ -267,31 +267,31 @@ class Bus(object):
|
||||
# can't just let exceptions propagate out unhandled.
|
||||
# Assume it's been logged and just die.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
|
||||
|
||||
if exitstate == states.STARTING:
|
||||
# exit() was called before start() finished, possibly due to
|
||||
# Ctrl-C because a start listener got stuck. In this case,
|
||||
# we could get stuck in a loop where Ctrl-C never exits the
|
||||
# process, so we just call os.exit here.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
|
||||
|
||||
def restart(self):
|
||||
"""Restart the process (may close connections).
|
||||
|
||||
|
||||
This method does not restart the process from the calling thread;
|
||||
instead, it stops the bus and asks the main thread to call execv.
|
||||
"""
|
||||
self.execv = True
|
||||
self.exit()
|
||||
|
||||
|
||||
def graceful(self):
|
||||
"""Advise all services to reload."""
|
||||
self.log('Bus graceful')
|
||||
self.publish('graceful')
|
||||
|
||||
|
||||
def block(self, interval=0.1):
|
||||
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
|
||||
|
||||
|
||||
This function is intended to be called only by the main thread.
|
||||
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
|
||||
@@ -309,7 +309,7 @@ class Bus(object):
|
||||
self.log('SystemExit raised: shutting down bus')
|
||||
self.exit()
|
||||
raise
|
||||
|
||||
|
||||
# Waiting for ALL child threads to finish is necessary on OS X.
|
||||
# See http://www.cherrypy.org/ticket/581.
|
||||
# It's also good to let them all shut down before allowing
|
||||
@@ -327,22 +327,22 @@ class Bus(object):
|
||||
if not d:
|
||||
self.log("Waiting for thread %s." % t.getName())
|
||||
t.join()
|
||||
|
||||
|
||||
if self.execv:
|
||||
self._do_execv()
|
||||
|
||||
|
||||
def wait(self, state, interval=0.1, channel=None):
|
||||
"""Poll for the given state(s) at intervals; publish to channel."""
|
||||
if isinstance(state, (tuple, list)):
|
||||
states = state
|
||||
else:
|
||||
states = [state]
|
||||
|
||||
|
||||
def _wait():
|
||||
while self.state not in states:
|
||||
time.sleep(interval)
|
||||
self.publish(channel)
|
||||
|
||||
|
||||
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
|
||||
# "The compiled machine code does not include the regular polling
|
||||
# done by Python, meaning that a KeyboardInterrupt will not be
|
||||
@@ -353,18 +353,18 @@ class Bus(object):
|
||||
sys.modules['psyco'].cannotcompile(_wait)
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
_wait()
|
||||
|
||||
|
||||
def _do_execv(self):
|
||||
"""Re-execute the current process.
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
args = sys.argv[:]
|
||||
self.log('Re-spawning %s' % ' '.join(args))
|
||||
|
||||
|
||||
if sys.platform[:4] == 'java':
|
||||
from _systemrestart import SystemRestart
|
||||
raise SystemRestart
|
||||
@@ -377,16 +377,16 @@ class Bus(object):
|
||||
if self.max_cloexec_files:
|
||||
self._set_cloexec()
|
||||
os.execv(sys.executable, args)
|
||||
|
||||
|
||||
def _set_cloexec(self):
|
||||
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
|
||||
|
||||
|
||||
If self.max_cloexec_files is an integer (the default), then on
|
||||
platforms which support it, it represents the max open files setting
|
||||
for the operating system. This function will be called just before
|
||||
the process is restarted via os.execv() to prevent open files
|
||||
from persisting into the new process.
|
||||
|
||||
|
||||
Set self.max_cloexec_files to 0 to disable this behavior.
|
||||
"""
|
||||
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
|
||||
@@ -395,7 +395,7 @@ class Bus(object):
|
||||
except IOError:
|
||||
continue
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
|
||||
|
||||
def stop(self):
|
||||
"""Stop all services."""
|
||||
self.state = states.STOPPING
|
||||
@@ -403,7 +403,7 @@ class Bus(object):
|
||||
self.publish('stop')
|
||||
self.state = states.STOPPED
|
||||
self.log('Bus STOPPED')
|
||||
|
||||
|
||||
def start_with_callback(self, func, args=None, kwargs=None):
|
||||
"""Start 'func' in a new thread T, then start self (and return T)."""
|
||||
if args is None:
|
||||
@@ -411,18 +411,18 @@ class Bus(object):
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
args = (func,) + args
|
||||
|
||||
|
||||
def _callback(func, *a, **kw):
|
||||
self.wait(states.STARTED)
|
||||
func(*a, **kw)
|
||||
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
|
||||
t.setName('Bus Callback ' + t.getName())
|
||||
t.start()
|
||||
|
||||
|
||||
self.start()
|
||||
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def log(self, msg="", level=20, traceback=False):
|
||||
"""Log the given message. Append the last traceback if requested."""
|
||||
if traceback:
|
||||
|
||||
@@ -20,10 +20,10 @@ local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
class Root:
|
||||
|
||||
|
||||
_cp_config = {'tools.log_tracebacks.on': True,
|
||||
}
|
||||
|
||||
|
||||
def index(self):
|
||||
return """<html>
|
||||
<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"),
|
||||
url("files/made_with_cherrypy_small.png"))
|
||||
index.exposed = True
|
||||
|
||||
|
||||
def default(self, *args, **kwargs):
|
||||
return "args: %s kwargs: %s" % (args, kwargs)
|
||||
default.exposed = True
|
||||
|
||||
|
||||
def other(self, a=2, b='bananas', c=None):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
if c is None:
|
||||
@@ -45,7 +45,7 @@ Or, just look at the pretty picture:<br />
|
||||
else:
|
||||
return "Have %d %s, %s." % (int(a), b, c)
|
||||
other.exposed = True
|
||||
|
||||
|
||||
files = cherrypy.tools.staticdir.handler(
|
||||
section="/files",
|
||||
dir=os.path.join(local_dir, "static"),
|
||||
|
||||
@@ -26,24 +26,24 @@ from cherrypy import wsgiserver
|
||||
|
||||
class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
|
||||
"""A wrapper for integrating Python's builtin ssl module with CherryPy."""
|
||||
|
||||
|
||||
certificate = None
|
||||
"""The filename of the server SSL certificate."""
|
||||
|
||||
|
||||
private_key = None
|
||||
"""The filename of the server's private key file."""
|
||||
|
||||
|
||||
def __init__(self, certificate, private_key, certificate_chain=None):
|
||||
if ssl is None:
|
||||
raise ImportError("You must install the ssl module to use HTTPS.")
|
||||
self.certificate = certificate
|
||||
self.private_key = private_key
|
||||
self.certificate_chain = certificate_chain
|
||||
|
||||
|
||||
def bind(self, sock):
|
||||
"""Wrap and return the given socket."""
|
||||
return sock
|
||||
|
||||
|
||||
def wrap(self, sock):
|
||||
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||||
try:
|
||||
@@ -67,7 +67,7 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
|
||||
return None, {}
|
||||
raise
|
||||
return s, self.get_environ(s)
|
||||
|
||||
|
||||
# TODO: fill this out more with mod ssl env
|
||||
def get_environ(self, sock):
|
||||
"""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
|
||||
}
|
||||
return ssl_environ
|
||||
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
|
||||
return wsgiserver.CP_makefile(sock, mode, bufsize)
|
||||
|
||||
@@ -45,13 +45,13 @@ except ImportError:
|
||||
|
||||
class SSL_fileobject(wsgiserver.CP_fileobject):
|
||||
"""SSL file object attached to a socket object."""
|
||||
|
||||
|
||||
ssl_timeout = 3
|
||||
ssl_retry = .01
|
||||
|
||||
|
||||
def _safe_call(self, is_reader, call, *args, **kwargs):
|
||||
"""Wrap the given call with SSL error-trapping.
|
||||
|
||||
|
||||
is_reader: if False EOF errors will be raised. If True, EOF errors
|
||||
will return "" (to emulate normal sockets).
|
||||
"""
|
||||
@@ -70,7 +70,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
|
||||
except SSL.SysCallError, e:
|
||||
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||||
return ""
|
||||
|
||||
|
||||
errnum = e.args[0]
|
||||
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
|
||||
return ""
|
||||
@@ -78,24 +78,24 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
|
||||
except SSL.Error, e:
|
||||
if is_reader and e.args == (-1, 'Unexpected EOF'):
|
||||
return ""
|
||||
|
||||
|
||||
thirdarg = None
|
||||
try:
|
||||
thirdarg = e.args[0][0][2]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
if thirdarg == 'http request':
|
||||
# The client is talking HTTP to an HTTPS server.
|
||||
raise wsgiserver.NoSSLError()
|
||||
|
||||
|
||||
raise wsgiserver.FatalSSLAlert(*e.args)
|
||||
except:
|
||||
raise
|
||||
|
||||
|
||||
if time.time() - start > self.ssl_timeout:
|
||||
raise socket.timeout("timed out")
|
||||
|
||||
|
||||
def recv(self, *args, **kwargs):
|
||||
buf = []
|
||||
r = super(SSL_fileobject, self).recv
|
||||
@@ -105,7 +105,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
|
||||
p = self._sock.pending()
|
||||
if not p:
|
||||
return "".join(buf)
|
||||
|
||||
|
||||
def sendall(self, *args, **kwargs):
|
||||
return self._safe_call(False, super(SSL_fileobject, self).sendall,
|
||||
*args, **kwargs)
|
||||
@@ -117,14 +117,14 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
|
||||
|
||||
class SSLConnection:
|
||||
"""A thread-safe wrapper for an SSL.Connection.
|
||||
|
||||
|
||||
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, *args):
|
||||
self._ssl_conn = SSL.Connection(*args)
|
||||
self._lock = threading.RLock()
|
||||
|
||||
|
||||
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
|
||||
'renegotiate', 'bind', 'listen', 'connect', 'accept',
|
||||
'setblocking', 'fileno', 'close', 'get_cipher_list',
|
||||
@@ -140,7 +140,7 @@ class SSLConnection:
|
||||
finally:
|
||||
self._lock.release()
|
||||
""" % (f, f))
|
||||
|
||||
|
||||
def shutdown(self, *args):
|
||||
self._lock.acquire()
|
||||
try:
|
||||
@@ -152,32 +152,32 @@ class SSLConnection:
|
||||
|
||||
class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||||
"""A wrapper for integrating pyOpenSSL with CherryPy."""
|
||||
|
||||
|
||||
context = None
|
||||
"""An instance of SSL.Context."""
|
||||
|
||||
|
||||
certificate = None
|
||||
"""The filename of the server SSL certificate."""
|
||||
|
||||
|
||||
private_key = None
|
||||
"""The filename of the server's private key file."""
|
||||
|
||||
|
||||
certificate_chain = None
|
||||
"""Optional. The filename of CA's intermediate certificate bundle.
|
||||
|
||||
|
||||
This is needed for cheaper "chained root" SSL certificates, and should be
|
||||
left as None if not required."""
|
||||
|
||||
|
||||
def __init__(self, certificate, private_key, certificate_chain=None):
|
||||
if SSL is None:
|
||||
raise ImportError("You must install pyOpenSSL to use HTTPS.")
|
||||
|
||||
|
||||
self.context = None
|
||||
self.certificate = certificate
|
||||
self.private_key = private_key
|
||||
self.certificate_chain = certificate_chain
|
||||
self._environ = None
|
||||
|
||||
|
||||
def bind(self, sock):
|
||||
"""Wrap and return the given socket."""
|
||||
if self.context is None:
|
||||
@@ -185,11 +185,11 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||||
conn = SSLConnection(self.context, sock)
|
||||
self._environ = self.get_environ()
|
||||
return conn
|
||||
|
||||
|
||||
def wrap(self, sock):
|
||||
"""Wrap and return the given socket, plus WSGI environ entries."""
|
||||
return sock, self._environ.copy()
|
||||
|
||||
|
||||
def get_context(self):
|
||||
"""Return an SSL.Context from self attributes."""
|
||||
# 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.use_certificate_file(self.certificate)
|
||||
return c
|
||||
|
||||
|
||||
def get_environ(self):
|
||||
"""Return WSGI environ entries to be merged into each request."""
|
||||
ssl_environ = {
|
||||
@@ -210,7 +210,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||||
## SSL_VERSION_INTERFACE string The mod_ssl program version
|
||||
## SSL_VERSION_LIBRARY string The OpenSSL program version
|
||||
}
|
||||
|
||||
|
||||
if self.certificate:
|
||||
# Server certificate attributes
|
||||
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_END': Validity of server's certificate (end time),
|
||||
})
|
||||
|
||||
|
||||
for prefix, dn in [("I", cert.get_issuer()),
|
||||
("S", cert.get_subject())]:
|
||||
# X509Name objects don't seem to have a way to get the
|
||||
# complete DN string. Use str() and slice it instead,
|
||||
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
|
||||
dnstr = str(dn)[18:-2]
|
||||
|
||||
|
||||
wsgikey = 'SSL_SERVER_%s_DN' % prefix
|
||||
ssl_environ[wsgikey] = dnstr
|
||||
|
||||
|
||||
# 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).
|
||||
while dnstr:
|
||||
@@ -242,9 +242,9 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||||
if key and value:
|
||||
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
|
||||
ssl_environ[wsgikey] = value
|
||||
|
||||
|
||||
return ssl_environ
|
||||
|
||||
|
||||
def makefile(self, sock, mode='r', bufsize=-1):
|
||||
if SSL and isinstance(sock, SSL.ConnectionType):
|
||||
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):
|
||||
import string
|
||||
_maketrans = string.maketrans
|
||||
|
||||
|
||||
# base64 support for Atom feeds that contain embedded binary data
|
||||
try:
|
||||
import base64, binascii
|
||||
@@ -240,7 +240,7 @@ if sgmllib.endbracket.search(' <').start(0):
|
||||
if match is not None:
|
||||
# Returning a new object in the calling thread's context
|
||||
# resolves a thread-safety.
|
||||
return EndBracketMatch(match)
|
||||
return EndBracketMatch(match)
|
||||
return None
|
||||
class EndBracketMatch:
|
||||
def __init__(self, match):
|
||||
@@ -334,7 +334,7 @@ class FeedParserDict(UserDict):
|
||||
if not self.has_key(key):
|
||||
self[key] = value
|
||||
return self[key]
|
||||
|
||||
|
||||
def has_key(self, key):
|
||||
try:
|
||||
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
|
||||
# __contains__ function below and exhausting the maximum recursion depth
|
||||
__has_key = has_key
|
||||
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self.__dict__[key]
|
||||
@@ -398,7 +398,7 @@ def _ebcdic_to_ascii(s):
|
||||
_ebcdic_to_ascii_map = _maketrans( \
|
||||
_l2bytes(range(256)), _l2bytes(emap))
|
||||
return s.translate(_ebcdic_to_ascii_map)
|
||||
|
||||
|
||||
_cp1252 = {
|
||||
unichr(128): unichr(8364), # euro sign
|
||||
unichr(130): unichr(8218), # single low-9 quotation mark
|
||||
@@ -451,7 +451,7 @@ class _FeedParserMixin:
|
||||
'http://purl.org/atom/ns#': '',
|
||||
'http://www.w3.org/2005/Atom': '',
|
||||
'http://purl.org/rss/1.0/modules/rss091#': '',
|
||||
|
||||
|
||||
'http://webns.net/mvcb/': 'admin',
|
||||
'http://purl.org/rss/1.0/modules/aggregation/': 'ag',
|
||||
'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_dangerous_markup = ['content', 'title', 'summary', 'info', 'tagline', 'subtitle', 'copyright', 'rights', 'description']
|
||||
html_types = ['text/html', 'application/xhtml+xml']
|
||||
|
||||
|
||||
def __init__(self, baseuri=None, baselang=None, encoding='utf-8'):
|
||||
if _debug: sys.stderr.write('initializing FeedParser\n')
|
||||
if not self._matchnamespaces:
|
||||
@@ -554,7 +554,7 @@ class _FeedParserMixin:
|
||||
# strict xml parsers do -- account for this difference
|
||||
if isinstance(self, _LooseFeedParser):
|
||||
attrs = [(k, v.replace('&', '&')) for k, v in attrs]
|
||||
|
||||
|
||||
# track xml:base and xml:lang
|
||||
attrsD = dict(attrs)
|
||||
baseuri = attrsD.get('xml:base', attrsD.get('base')) or self.baseuri
|
||||
@@ -582,7 +582,7 @@ class _FeedParserMixin:
|
||||
self.lang = lang
|
||||
self.basestack.append(self.baseuri)
|
||||
self.langstack.append(lang)
|
||||
|
||||
|
||||
# track namespaces
|
||||
for prefix, uri in attrs:
|
||||
if prefix.startswith('xmlns:'):
|
||||
@@ -620,7 +620,7 @@ class _FeedParserMixin:
|
||||
self.intextinput = 0
|
||||
if (not prefix) and tag not in ('title', 'link', 'description', 'url', 'href', 'width', 'height'):
|
||||
self.inimage = 0
|
||||
|
||||
|
||||
# call special handler (if defined) or default handler
|
||||
methodname = '_start_' + prefix + suffix
|
||||
try:
|
||||
@@ -754,7 +754,7 @@ class _FeedParserMixin:
|
||||
elif contentType == 'xhtml':
|
||||
contentType = 'application/xhtml+xml'
|
||||
return contentType
|
||||
|
||||
|
||||
def trackNamespace(self, prefix, uri):
|
||||
loweruri = uri.lower()
|
||||
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):
|
||||
return _urljoin(self.baseuri or '', uri)
|
||||
|
||||
|
||||
def decodeEntities(self, element, data):
|
||||
return data
|
||||
|
||||
@@ -788,7 +788,7 @@ class _FeedParserMixin:
|
||||
def pop(self, element, stripWhitespace=1):
|
||||
if not self.elementstack: return
|
||||
if self.elementstack[-1][0] != element: return
|
||||
|
||||
|
||||
element, expectingText, pieces = self.elementstack.pop()
|
||||
|
||||
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
|
||||
# This may not be the most correct way to accomplish this
|
||||
output = _base64decode(output.encode('utf-8')).decode('utf-8')
|
||||
|
||||
|
||||
# resolve relative URIs
|
||||
if (element in self.can_be_relative_uri) and output:
|
||||
output = self.resolveURI(output)
|
||||
|
||||
|
||||
# decode entities within embedded markup
|
||||
if not self.contentparams.get('base64', 0):
|
||||
output = self.decodeEntities(element, output)
|
||||
@@ -860,7 +860,7 @@ class _FeedParserMixin:
|
||||
if is_htmlish and RESOLVE_RELATIVE_URIS:
|
||||
if element in self.can_contain_relative_uris:
|
||||
output = _resolveRelativeURIs(output, self.baseuri, self.encoding, self.contentparams.get('type', 'text/html'))
|
||||
|
||||
|
||||
# parse microformats
|
||||
# (must do this before sanitizing because some microformats
|
||||
# rely on elements that we sanitize)
|
||||
@@ -876,7 +876,7 @@ class _FeedParserMixin:
|
||||
vcard = mfresults.get('vcard')
|
||||
if vcard:
|
||||
self._getContext()['vcard'] = vcard
|
||||
|
||||
|
||||
# sanitize embedded markup
|
||||
if is_htmlish and SANITIZE_HTML:
|
||||
if element in self.can_contain_dangerous_markup:
|
||||
@@ -906,7 +906,7 @@ class _FeedParserMixin:
|
||||
|
||||
if element == 'title' and self.hasTitle:
|
||||
return output
|
||||
|
||||
|
||||
# store output in appropriate place(s)
|
||||
if self.inentry and not self.insource:
|
||||
if element == 'content':
|
||||
@@ -962,7 +962,7 @@ class _FeedParserMixin:
|
||||
self.incontent -= 1
|
||||
self.contentparams.clear()
|
||||
return value
|
||||
|
||||
|
||||
# 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
|
||||
# the most common cases. As false positives often result in silent
|
||||
@@ -993,7 +993,7 @@ class _FeedParserMixin:
|
||||
prefix = self.namespacemap.get(prefix, prefix)
|
||||
name = prefix + ':' + suffix
|
||||
return name
|
||||
|
||||
|
||||
def _getAttribute(self, attrsD, name):
|
||||
return attrsD.get(self._mapToStandardPrefix(name))
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class _FeedParserMixin:
|
||||
pass
|
||||
attrsD['href'] = href
|
||||
return attrsD
|
||||
|
||||
|
||||
def _save(self, key, value, overwrite=False):
|
||||
context = self._getContext()
|
||||
if overwrite:
|
||||
@@ -1046,7 +1046,7 @@ class _FeedParserMixin:
|
||||
self.version = 'rss20'
|
||||
else:
|
||||
self.version = 'rss'
|
||||
|
||||
|
||||
def _start_dlhottitles(self, attrsD):
|
||||
self.version = 'hotrss'
|
||||
|
||||
@@ -1064,7 +1064,7 @@ class _FeedParserMixin:
|
||||
self._start_link({})
|
||||
self.elementstack[-1][-1] = attrsD['href']
|
||||
self._end_link()
|
||||
|
||||
|
||||
def _start_feed(self, attrsD):
|
||||
self.infeed = 1
|
||||
versionmap = {'0.1': 'atom01',
|
||||
@@ -1081,7 +1081,7 @@ class _FeedParserMixin:
|
||||
def _end_channel(self):
|
||||
self.infeed = 0
|
||||
_end_feed = _end_channel
|
||||
|
||||
|
||||
def _start_image(self, attrsD):
|
||||
context = self._getContext()
|
||||
if not self.inentry:
|
||||
@@ -1089,7 +1089,7 @@ class _FeedParserMixin:
|
||||
self.inimage = 1
|
||||
self.hasTitle = 0
|
||||
self.push('image', 0)
|
||||
|
||||
|
||||
def _end_image(self):
|
||||
self.pop('image')
|
||||
self.inimage = 0
|
||||
@@ -1101,7 +1101,7 @@ class _FeedParserMixin:
|
||||
self.hasTitle = 0
|
||||
self.push('textinput', 0)
|
||||
_start_textInput = _start_textinput
|
||||
|
||||
|
||||
def _end_textinput(self):
|
||||
self.pop('textinput')
|
||||
self.intextinput = 0
|
||||
@@ -1301,7 +1301,7 @@ class _FeedParserMixin:
|
||||
self.popContent('subtitle')
|
||||
_end_tagline = _end_subtitle
|
||||
_end_itunes_subtitle = _end_subtitle
|
||||
|
||||
|
||||
def _start_rights(self, attrsD):
|
||||
self.pushContent('rights', attrsD, 'text/plain', 1)
|
||||
_start_dc_rights = _start_rights
|
||||
@@ -1399,7 +1399,7 @@ class _FeedParserMixin:
|
||||
attrsD['rel']='license'
|
||||
if value: attrsD['href']=value
|
||||
context.setdefault('links', []).append(attrsD)
|
||||
|
||||
|
||||
def _start_creativecommons_license(self, attrsD):
|
||||
self.push('license', 1)
|
||||
_start_creativeCommons_license = _start_creativecommons_license
|
||||
@@ -1420,7 +1420,7 @@ class _FeedParserMixin:
|
||||
value = FeedParserDict({'relationships': relationships, 'href': href, 'name': name})
|
||||
if value not in xfn:
|
||||
xfn.append(value)
|
||||
|
||||
|
||||
def _addTag(self, term, scheme, label):
|
||||
context = self._getContext()
|
||||
tags = context.setdefault('tags', [])
|
||||
@@ -1438,7 +1438,7 @@ class _FeedParserMixin:
|
||||
self.push('category', 1)
|
||||
_start_dc_subject = _start_category
|
||||
_start_keywords = _start_category
|
||||
|
||||
|
||||
def _start_media_category(self, attrsD):
|
||||
attrsD.setdefault('scheme', 'http://search.yahoo.com/mrss/category_schema')
|
||||
self._start_category(attrsD)
|
||||
@@ -1446,11 +1446,11 @@ class _FeedParserMixin:
|
||||
def _end_itunes_keywords(self):
|
||||
for term in self.pop('itunes_keywords').split():
|
||||
self._addTag(term, 'http://www.itunes.com/', None)
|
||||
|
||||
|
||||
def _start_itunes_category(self, attrsD):
|
||||
self._addTag(attrsD.get('text'), 'http://www.itunes.com/', None)
|
||||
self.push('category', 1)
|
||||
|
||||
|
||||
def _end_category(self):
|
||||
value = self.pop('category')
|
||||
if not value: return
|
||||
@@ -1467,7 +1467,7 @@ class _FeedParserMixin:
|
||||
|
||||
def _start_cloud(self, attrsD):
|
||||
self._getContext()['cloud'] = FeedParserDict(attrsD)
|
||||
|
||||
|
||||
def _start_link(self, attrsD):
|
||||
attrsD.setdefault('rel', 'alternate')
|
||||
if attrsD['rel'] == 'self':
|
||||
@@ -1568,7 +1568,7 @@ class _FeedParserMixin:
|
||||
context = self._getContext()
|
||||
if context.has_key('generator_detail'):
|
||||
context['generator_detail']['name'] = value
|
||||
|
||||
|
||||
def _start_admin_generatoragent(self, attrsD):
|
||||
self.push('generator', 1)
|
||||
value = self._getAttribute(attrsD, 'rdf:resource')
|
||||
@@ -1583,7 +1583,7 @@ class _FeedParserMixin:
|
||||
if value:
|
||||
self.elementstack[-1][2].append(value)
|
||||
self.pop('errorreportsto')
|
||||
|
||||
|
||||
def _start_summary(self, attrsD):
|
||||
context = self._getContext()
|
||||
if context.has_key('summary'):
|
||||
@@ -1601,13 +1601,13 @@ class _FeedParserMixin:
|
||||
self.popContent(self._summaryKey or 'summary')
|
||||
self._summaryKey = None
|
||||
_end_itunes_summary = _end_summary
|
||||
|
||||
|
||||
def _start_enclosure(self, attrsD):
|
||||
attrsD = self._itsAnHrefDamnIt(attrsD)
|
||||
context = self._getContext()
|
||||
attrsD['rel']='enclosure'
|
||||
context.setdefault('links', []).append(FeedParserDict(attrsD))
|
||||
|
||||
|
||||
def _start_source(self, attrsD):
|
||||
if 'url' in attrsD:
|
||||
# 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'):
|
||||
self._getContext()['image'] = FeedParserDict({'href': attrsD.get('href')})
|
||||
_start_itunes_link = _start_itunes_image
|
||||
|
||||
|
||||
def _end_itunes_block(self):
|
||||
value = self.pop('itunes_block', 0)
|
||||
self._getContext()['itunes_block'] = (value == 'yes') and 1 or 0
|
||||
@@ -1718,12 +1718,12 @@ if _XML_AVAILABLE:
|
||||
self.bozo = 0
|
||||
self.exc = None
|
||||
self.decls = {}
|
||||
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
self.trackNamespace(prefix, uri)
|
||||
if uri == 'http://www.w3.org/1999/xlink':
|
||||
self.decls['xmlns:'+prefix] = uri
|
||||
|
||||
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
namespace, localname = name
|
||||
lowernamespace = str(namespace or '').lower()
|
||||
@@ -1805,7 +1805,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
|
||||
special = re.compile('''[<>'"]''')
|
||||
bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)")
|
||||
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',
|
||||
'source', 'track', 'wbr'
|
||||
]
|
||||
@@ -1837,7 +1837,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
|
||||
def feed(self, 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)
|
||||
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
|
||||
data = data.replace(''', "'")
|
||||
data = data.replace('"', '"')
|
||||
try:
|
||||
@@ -1910,7 +1910,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
|
||||
self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:])
|
||||
else:
|
||||
self.pieces.append('&#%(ref)s;' % locals())
|
||||
|
||||
|
||||
def handle_entityref(self, ref):
|
||||
# called for each entity reference, e.g. for '©', ref will be 'copy'
|
||||
# Reconstruct the original entity reference.
|
||||
@@ -1925,12 +1925,12 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
|
||||
# Store the original text verbatim.
|
||||
if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_data, text=%s\n' % text)
|
||||
self.pieces.append(text)
|
||||
|
||||
|
||||
def handle_comment(self, text):
|
||||
# called for each HTML comment, e.g. <!-- insert Javascript code here -->
|
||||
# Reconstruct the original comment.
|
||||
self.pieces.append('<!--%(text)s-->' % locals())
|
||||
|
||||
|
||||
def handle_pi(self, text):
|
||||
# called for each processing instruction, e.g. <?instruction>
|
||||
# Reconstruct original processing instruction.
|
||||
@@ -1942,7 +1942,7 @@ class _BaseHTMLProcessor(sgmllib.SGMLParser):
|
||||
# "http://www.w3.org/TR/html4/loose.dtd">
|
||||
# Reconstruct original DOCTYPE
|
||||
self.pieces.append('<!%(text)s>' % locals())
|
||||
|
||||
|
||||
_new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match
|
||||
def _scan_name(self, i, declstartpos):
|
||||
rawdata = self.rawdata
|
||||
@@ -2006,7 +2006,7 @@ class _LooseFeedParser(_FeedParserMixin, _BaseHTMLProcessor):
|
||||
data = data.replace('"', '"')
|
||||
data = data.replace(''', "'")
|
||||
return data
|
||||
|
||||
|
||||
def strattrs(self, attrs):
|
||||
return ''.join([' %s="%s"' % (n,v.replace('"','"')) for n,v in attrs])
|
||||
|
||||
@@ -2030,12 +2030,12 @@ class _MicroformatsParser:
|
||||
self.enclosures = []
|
||||
self.xfn = []
|
||||
self.vcard = None
|
||||
|
||||
|
||||
def vcardEscape(self, s):
|
||||
if type(s) in (type(''), type(u'')):
|
||||
s = s.replace(',', '\\,').replace(';', '\\;').replace('\n', '\\n')
|
||||
return s
|
||||
|
||||
|
||||
def vcardFold(self, s):
|
||||
s = re.sub(';+$', '', s)
|
||||
sFolded = ''
|
||||
@@ -2051,14 +2051,14 @@ class _MicroformatsParser:
|
||||
|
||||
def normalize(self, s):
|
||||
return re.sub(r'\s+', ' ', s).strip()
|
||||
|
||||
|
||||
def unique(self, aList):
|
||||
results = []
|
||||
for element in aList:
|
||||
if element not in results:
|
||||
results.append(element)
|
||||
return results
|
||||
|
||||
|
||||
def toISO8601(self, dt):
|
||||
return time.strftime('%Y-%m-%dT%H:%M:%SZ', dt)
|
||||
|
||||
@@ -2148,21 +2148,21 @@ class _MicroformatsParser:
|
||||
|
||||
def findVCards(self, elmRoot, bAgentParsing=0):
|
||||
sVCards = ''
|
||||
|
||||
|
||||
if not bAgentParsing:
|
||||
arCards = self.getPropertyValue(elmRoot, 'vcard', bAllowMultiple=1)
|
||||
else:
|
||||
arCards = [elmRoot]
|
||||
|
||||
|
||||
for elmCard in arCards:
|
||||
arLines = []
|
||||
|
||||
|
||||
def processSingleString(sProperty):
|
||||
sValue = self.getPropertyValue(elmCard, sProperty, self.STRING, bAutoEscape=1).decode(self.encoding)
|
||||
if sValue:
|
||||
arLines.append(self.vcardFold(sProperty.upper() + ':' + sValue))
|
||||
return sValue or u''
|
||||
|
||||
|
||||
def processSingleURI(sProperty):
|
||||
sValue = self.getPropertyValue(elmCard, sProperty, self.URI)
|
||||
if sValue:
|
||||
@@ -2185,7 +2185,7 @@ class _MicroformatsParser:
|
||||
if sContentType:
|
||||
sContentType = ';TYPE=' + sContentType.upper()
|
||||
arLines.append(self.vcardFold(sProperty.upper() + sEncoding + sContentType + sValueKey + ':' + sValue))
|
||||
|
||||
|
||||
def processTypeValue(sProperty, arDefaultType, arForceType=None):
|
||||
arResults = self.getPropertyValue(elmCard, sProperty, bAllowMultiple=1)
|
||||
for elmResult in arResults:
|
||||
@@ -2197,7 +2197,7 @@ class _MicroformatsParser:
|
||||
sValue = self.getPropertyValue(elmResult, 'value', self.EMAIL, 0)
|
||||
if sValue:
|
||||
arLines.append(self.vcardFold(sProperty.upper() + ';TYPE=' + ','.join(arType) + ':' + sValue))
|
||||
|
||||
|
||||
# AGENT
|
||||
# must do this before all other properties because it is destructive
|
||||
# (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);
|
||||
if sAgentValue:
|
||||
arLines.append(self.vcardFold('AGENT;VALUE=uri:' + sAgentValue))
|
||||
|
||||
|
||||
# FN (full name)
|
||||
sFN = processSingleString('fn')
|
||||
|
||||
|
||||
# N (name)
|
||||
elmName = self.getPropertyValue(elmCard, 'n')
|
||||
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)
|
||||
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)
|
||||
arLines.append(self.vcardFold('N:' + sFamilyName + ';' +
|
||||
arLines.append(self.vcardFold('N:' + sFamilyName + ';' +
|
||||
sGivenName + ';' +
|
||||
','.join(arAdditionalNames) + ';' +
|
||||
','.join(arHonorificPrefixes) + ';' +
|
||||
@@ -2245,25 +2245,25 @@ class _MicroformatsParser:
|
||||
arLines.append(self.vcardFold('N:' + arNames[0] + ';' + arNames[1]))
|
||||
else:
|
||||
arLines.append(self.vcardFold('N:' + arNames[1] + ';' + arNames[0]))
|
||||
|
||||
|
||||
# SORT-STRING
|
||||
sSortString = self.getPropertyValue(elmCard, 'sort-string', self.STRING, bAutoEscape=1)
|
||||
if sSortString:
|
||||
arLines.append(self.vcardFold('SORT-STRING:' + sSortString))
|
||||
|
||||
|
||||
# NICKNAME
|
||||
arNickname = self.getPropertyValue(elmCard, 'nickname', self.STRING, 1, 1)
|
||||
if arNickname:
|
||||
arLines.append(self.vcardFold('NICKNAME:' + ','.join(arNickname)))
|
||||
|
||||
|
||||
# PHOTO
|
||||
processSingleURI('photo')
|
||||
|
||||
|
||||
# BDAY
|
||||
dtBday = self.getPropertyValue(elmCard, 'bday', self.DATE)
|
||||
if dtBday:
|
||||
arLines.append(self.vcardFold('BDAY:' + self.toISO8601(dtBday)))
|
||||
|
||||
|
||||
# ADR (address)
|
||||
arAdr = self.getPropertyValue(elmCard, 'adr', bAllowMultiple=1)
|
||||
for elmAdr in arAdr:
|
||||
@@ -2285,38 +2285,38 @@ class _MicroformatsParser:
|
||||
sRegion + ';' +
|
||||
sPostalCode + ';' +
|
||||
sCountryName))
|
||||
|
||||
|
||||
# LABEL
|
||||
processTypeValue('label', ['intl','postal','parcel','work'])
|
||||
|
||||
|
||||
# TEL (phone number)
|
||||
processTypeValue('tel', ['voice'])
|
||||
|
||||
|
||||
# EMAIL
|
||||
processTypeValue('email', ['internet'], ['internet'])
|
||||
|
||||
|
||||
# MAILER
|
||||
processSingleString('mailer')
|
||||
|
||||
|
||||
# TZ (timezone)
|
||||
processSingleString('tz')
|
||||
|
||||
|
||||
# GEO (geographical information)
|
||||
elmGeo = self.getPropertyValue(elmCard, 'geo')
|
||||
if elmGeo:
|
||||
sLatitude = self.getPropertyValue(elmGeo, 'latitude', self.STRING, 0, 1)
|
||||
sLongitude = self.getPropertyValue(elmGeo, 'longitude', self.STRING, 0, 1)
|
||||
arLines.append(self.vcardFold('GEO:' + sLatitude + ';' + sLongitude))
|
||||
|
||||
|
||||
# TITLE
|
||||
processSingleString('title')
|
||||
|
||||
|
||||
# ROLE
|
||||
processSingleString('role')
|
||||
|
||||
# LOGO
|
||||
processSingleURI('logo')
|
||||
|
||||
|
||||
# ORG (organization)
|
||||
elmOrg = self.getPropertyValue(elmCard, 'org')
|
||||
if elmOrg:
|
||||
@@ -2330,39 +2330,39 @@ class _MicroformatsParser:
|
||||
else:
|
||||
arOrganizationUnit = self.getPropertyValue(elmOrg, 'organization-unit', self.STRING, 1, 1)
|
||||
arLines.append(self.vcardFold('ORG:' + sOrganizationName + ';' + ';'.join(arOrganizationUnit)))
|
||||
|
||||
|
||||
# CATEGORY
|
||||
arCategory = self.getPropertyValue(elmCard, 'category', self.STRING, 1, 1) + self.getPropertyValue(elmCard, 'categories', self.STRING, 1, 1)
|
||||
if arCategory:
|
||||
arLines.append(self.vcardFold('CATEGORIES:' + ','.join(arCategory)))
|
||||
|
||||
|
||||
# NOTE
|
||||
processSingleString('note')
|
||||
|
||||
|
||||
# REV
|
||||
processSingleString('rev')
|
||||
|
||||
|
||||
# SOUND
|
||||
processSingleURI('sound')
|
||||
|
||||
|
||||
# UID
|
||||
processSingleString('uid')
|
||||
|
||||
|
||||
# URL
|
||||
processSingleURI('url')
|
||||
|
||||
|
||||
# CLASS
|
||||
processSingleString('class')
|
||||
|
||||
|
||||
# KEY
|
||||
processSingleURI('key')
|
||||
|
||||
|
||||
if arLines:
|
||||
arLines = [u'BEGIN:vCard',u'VERSION:3.0'] + arLines + [u'END:vCard']
|
||||
sVCards += u'\n'.join(arLines) + u'\n'
|
||||
|
||||
|
||||
return sVCards.strip()
|
||||
|
||||
|
||||
def isProbablyDownloadable(self, elm):
|
||||
attrsD = elm.attrMap
|
||||
if not attrsD.has_key('href'): return 0
|
||||
@@ -2461,7 +2461,7 @@ class _RelativeURIResolver(_BaseHTMLProcessor):
|
||||
|
||||
def resolveURI(self, uri):
|
||||
return _makeSafeAbsoluteURI(_urljoin(self.baseuri, uri.strip()))
|
||||
|
||||
|
||||
def unknown_starttag(self, tag, attrs):
|
||||
if _debug:
|
||||
sys.stderr.write('tag: [%s] with attributes: [%s]\n' % (tag, str(attrs)))
|
||||
@@ -2575,7 +2575,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
|
||||
# svgtiny - foreignObject + linearGradient + radialGradient + stop
|
||||
svg_elements = ['a', 'animate', 'animateColor', 'animateMotion',
|
||||
'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',
|
||||
'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop',
|
||||
'svg', 'switch', 'text', 'title', 'tspan', 'use']
|
||||
@@ -2621,7 +2621,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
|
||||
self.unacceptablestack = 0
|
||||
self.mathmlOK = 0
|
||||
self.svgOK = 0
|
||||
|
||||
|
||||
def unknown_starttag(self, tag, attrs):
|
||||
acceptable_attributes = self.acceptable_attributes
|
||||
keymap = {}
|
||||
@@ -2683,7 +2683,7 @@ class _HTMLSanitizer(_BaseHTMLProcessor):
|
||||
clean_value = self.sanitize_style(value)
|
||||
if clean_value: clean_attrs.append((key,clean_value))
|
||||
_BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs)
|
||||
|
||||
|
||||
def unknown_endtag(self, tag):
|
||||
if not tag in self.acceptable_elements:
|
||||
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_303 = http_error_302
|
||||
http_error_307 = http_error_302
|
||||
|
||||
|
||||
def http_error_401(self, req, fp, code, msg, headers):
|
||||
# Check if
|
||||
# - 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)
|
||||
finally:
|
||||
opener.close() # JohnD
|
||||
|
||||
|
||||
# try to open with native open function (if url_file_stream_or_string is a filename)
|
||||
try:
|
||||
return open(url_file_stream_or_string, 'rb')
|
||||
@@ -2966,7 +2966,7 @@ _date_handlers = []
|
||||
def registerDateHandler(func):
|
||||
'''Register a date handler function (takes string, returns 9-tuple date in GMT)'''
|
||||
_date_handlers.insert(0, func)
|
||||
|
||||
|
||||
# ISO-8601 date parsing routines written by Fazal Majid.
|
||||
# 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
|
||||
@@ -2977,7 +2977,7 @@ def registerDateHandler(func):
|
||||
# Please note the order in templates is significant because we need a
|
||||
# greedy match.
|
||||
_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',
|
||||
'--MM-?DD', '--MM',
|
||||
'---DD',
|
||||
@@ -3079,7 +3079,7 @@ def _parse_date_iso8601(dateString):
|
||||
# Many implementations have bugs, but we'll pretend they don't.
|
||||
return time.localtime(time.mktime(tuple(tm)))
|
||||
registerDateHandler(_parse_date_iso8601)
|
||||
|
||||
|
||||
# 8-bit date handling routines written by ytrewq1.
|
||||
_korean_year = u'\ub144' # b3e2 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'\u03a0\u03b5\u03bc': u'Thu', # d0e5ec 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 = \
|
||||
@@ -3360,7 +3360,7 @@ def _parse_date_rfc822(dateString):
|
||||
# 'ET' is equivalent to 'EST', etc.
|
||||
_additional_timezones = {'AT': -400, 'ET': -500, 'CT': -600, 'MT': -700, 'PT': -800}
|
||||
rfc822._timezones.update(_additional_timezones)
|
||||
registerDateHandler(_parse_date_rfc822)
|
||||
registerDateHandler(_parse_date_rfc822)
|
||||
|
||||
def _parse_date_perforce(aDateString):
|
||||
"""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
|
||||
xml_data is a raw string (not Unicode)
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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.)
|
||||
|
||||
|
||||
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
|
||||
encoding given in the XML prefix of the document and default to
|
||||
'iso-8859-1' as per the HTTP specification (RFC 2616).
|
||||
|
||||
|
||||
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
|
||||
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':
|
||||
true_encoding = 'gb18030'
|
||||
return true_encoding, http_encoding, xml_encoding, sniffed_xml_encoding, acceptable_content_type
|
||||
|
||||
|
||||
def _toUTF8(data, 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 = start and start.start() or -1
|
||||
head,data = data[:start+1], data[start+1:]
|
||||
|
||||
|
||||
entity_pattern = re.compile(_s2bytes(r'^\s*<!ENTITY([^>]*?)>'), re.MULTILINE)
|
||||
entity_results=entity_pattern.findall(head)
|
||||
head = entity_pattern.sub(_s2bytes(''), head)
|
||||
@@ -3617,10 +3617,10 @@ def _stripDoctype(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)])
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
request_headers, if given, is a dict from http header name to value to add
|
||||
to the request; this overrides internally generated values.
|
||||
'''
|
||||
@@ -3861,7 +3861,7 @@ class TextSerializer(Serializer):
|
||||
stream.write('\n')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class PprintSerializer(Serializer):
|
||||
def write(self, stream=sys.stdout):
|
||||
if self.results.has_key('href'):
|
||||
@@ -3869,7 +3869,7 @@ class PprintSerializer(Serializer):
|
||||
from pprint import pprint
|
||||
pprint(self.results, stream)
|
||||
stream.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
from optparse import OptionParser
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class _GNTPBase(object):
|
||||
'SHA1': hashlib.sha1,
|
||||
'SHA256': hashlib.sha256,
|
||||
'SHA512': hashlib.sha512,
|
||||
}
|
||||
}
|
||||
self.headers = {}
|
||||
self.resources = {}
|
||||
|
||||
|
||||
+77
-77
@@ -3,7 +3,7 @@ from __future__ import generators
|
||||
httplib2
|
||||
|
||||
A caching http interface that supports ETags and gzip
|
||||
to conserve bandwidth.
|
||||
to conserve bandwidth.
|
||||
|
||||
Requires Python 2.3 or later
|
||||
|
||||
@@ -24,8 +24,8 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
|
||||
__license__ = "MIT"
|
||||
__version__ = "$Rev$"
|
||||
|
||||
import re
|
||||
import sys
|
||||
import re
|
||||
import sys
|
||||
import email
|
||||
import email.Utils
|
||||
import email.Message
|
||||
@@ -85,7 +85,7 @@ def has_timeout(timeout): # python 2.6
|
||||
return (timeout is not None)
|
||||
|
||||
__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error',
|
||||
'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
|
||||
'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
|
||||
'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError',
|
||||
'debuglevel']
|
||||
|
||||
@@ -113,8 +113,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'):
|
||||
# All exceptions raised here derive from HttpLib2Error
|
||||
class HttpLib2Error(Exception): pass
|
||||
|
||||
# Some exceptions can be caught and optionally
|
||||
# be turned back into responses.
|
||||
# Some exceptions can be caught and optionally
|
||||
# be turned back into responses.
|
||||
class HttpLib2ErrorWithResponse(HttpLib2Error):
|
||||
def __init__(self, desc, response, content):
|
||||
self.response = response
|
||||
@@ -176,7 +176,7 @@ def urlnorm(uri):
|
||||
raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri)
|
||||
authority = authority.lower()
|
||||
scheme = scheme.lower()
|
||||
if not path:
|
||||
if not path:
|
||||
path = "/"
|
||||
# Could do syntax based normalization of the URI before
|
||||
# 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_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")]
|
||||
retval = dict(parts_with_args + parts_wo_args)
|
||||
return retval
|
||||
return retval
|
||||
|
||||
# Whether to use a strict mode to parse WWW-Authenticate headers
|
||||
# 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:
|
||||
# Break off the scheme at the beginning of the line
|
||||
if headername == 'authentication-info':
|
||||
(auth_scheme, the_rest) = ('digest', authenticate)
|
||||
(auth_scheme, the_rest) = ('digest', authenticate)
|
||||
else:
|
||||
(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
|
||||
match = www_auth.search(the_rest)
|
||||
auth_params = {}
|
||||
@@ -279,17 +279,17 @@ def _entry_disposition(response_headers, request_headers):
|
||||
1. Cache-Control: max-stale
|
||||
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.
|
||||
This lets us ignore 's-maxage'. We can also ignore
|
||||
'proxy-invalidate' since we aren't a proxy.
|
||||
We will never return a stale document as
|
||||
fresh as a design decision, and thus the non-implementation
|
||||
of 'max-stale'. This also lets us safely ignore 'must-revalidate'
|
||||
We will never return a stale document as
|
||||
fresh as a design decision, and thus the non-implementation
|
||||
of 'max-stale'. This also lets us safely ignore 'must-revalidate'
|
||||
since we operate as if every server has sent 'must-revalidate'.
|
||||
Since we are private we get to ignore both 'public' and
|
||||
'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.
|
||||
So the only Cache-Control parameters we look at are:
|
||||
|
||||
@@ -298,7 +298,7 @@ def _entry_disposition(response_headers, request_headers):
|
||||
max-age
|
||||
min-fresh
|
||||
"""
|
||||
|
||||
|
||||
retval = "STALE"
|
||||
cc = _parse_cache_control(request_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'])
|
||||
except ValueError:
|
||||
min_fresh = 0
|
||||
current_age += min_fresh
|
||||
current_age += min_fresh
|
||||
if freshness_lifetime > current_age:
|
||||
retval = "FRESH"
|
||||
return retval
|
||||
return retval
|
||||
|
||||
def _decompressContent(response, 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()
|
||||
|
||||
|
||||
# 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.)
|
||||
# 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.
|
||||
# So we also need each Auth instance to be able to tell us
|
||||
# how close to the 'top' it is.
|
||||
@@ -443,7 +443,7 @@ class Authentication(object):
|
||||
or such returned from the last authorized response.
|
||||
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.
|
||||
"""
|
||||
return False
|
||||
@@ -461,7 +461,7 @@ class BasicAuthentication(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"""
|
||||
def __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()
|
||||
if self.challenge['algorithm'] != 'MD5':
|
||||
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
|
||||
|
||||
def request(self, method, request_uri, headers, content, cnonce = None):
|
||||
@@ -482,17 +482,17 @@ class DigestAuthentication(Authentication):
|
||||
H = lambda x: _md5(x).hexdigest()
|
||||
KD = lambda s, d: H("%s:%s" % (s, d))
|
||||
A2 = "".join([method, ":", request_uri])
|
||||
self.challenge['cnonce'] = cnonce or _cnonce()
|
||||
request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'],
|
||||
'%08x' % self.challenge['nc'],
|
||||
self.challenge['cnonce'],
|
||||
self.challenge['cnonce'] = cnonce or _cnonce()
|
||||
request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'],
|
||||
'%08x' % self.challenge['nc'],
|
||||
self.challenge['cnonce'],
|
||||
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"' % (
|
||||
self.credentials[0],
|
||||
self.credentials[0],
|
||||
self.challenge['realm'],
|
||||
self.challenge['nonce'],
|
||||
request_uri,
|
||||
request_uri,
|
||||
self.challenge['algorithm'],
|
||||
request_digest,
|
||||
self.challenge['qop'],
|
||||
@@ -506,14 +506,14 @@ class DigestAuthentication(Authentication):
|
||||
challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {})
|
||||
if 'true' == challenge.get('stale'):
|
||||
self.challenge['nonce'] = challenge['nonce']
|
||||
self.challenge['nc'] = 1
|
||||
self.challenge['nc'] = 1
|
||||
return True
|
||||
else:
|
||||
updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {})
|
||||
|
||||
if updated_challenge.has_key('nextnonce'):
|
||||
self.challenge['nonce'] = updated_challenge['nextnonce']
|
||||
self.challenge['nc'] = 1
|
||||
self.challenge['nc'] = 1
|
||||
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 = 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"' % (
|
||||
self.credentials[0],
|
||||
self.credentials[0],
|
||||
self.challenge['realm'],
|
||||
self.challenge['snonce'],
|
||||
cnonce,
|
||||
request_uri,
|
||||
request_uri,
|
||||
created,
|
||||
request_digest,
|
||||
keylist,
|
||||
@@ -583,7 +583,7 @@ class WsseAuthentication(Authentication):
|
||||
"""This is thinly tested and should not be relied upon.
|
||||
At this time there isn't any third party server to test against.
|
||||
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
|
||||
challenge but instead requiring your client to telepathically know that
|
||||
their endpoint is expecting WSSE profile="UsernameToken"."""
|
||||
@@ -629,7 +629,7 @@ class GoogleLoginAuthentication(Authentication):
|
||||
def request(self, method, request_uri, headers, content):
|
||||
"""Modify the request headers to add the appropriate
|
||||
Authorization header."""
|
||||
headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
|
||||
headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
|
||||
|
||||
|
||||
AUTH_SCHEME_CLASSES = {
|
||||
@@ -644,13 +644,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
|
||||
|
||||
class FileCache(object):
|
||||
"""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.
|
||||
"""
|
||||
def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
|
||||
self.cache = cache
|
||||
self.safe = safe
|
||||
if not os.path.exists(cache):
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(self.cache)
|
||||
|
||||
def get(self, key):
|
||||
@@ -688,7 +688,7 @@ class Credentials(object):
|
||||
def iter(self, domain):
|
||||
for (cdomain, name, password) in self.credentials:
|
||||
if cdomain == "" or domain == cdomain:
|
||||
yield (name, password)
|
||||
yield (name, password)
|
||||
|
||||
class KeyCerts(Credentials):
|
||||
"""Identical to Credentials except that
|
||||
@@ -772,7 +772,7 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
|
||||
sock.setproxy(*self.proxy_info.astuple())
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
|
||||
if has_timeout(self.timeout):
|
||||
sock.settimeout(self.timeout)
|
||||
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.
|
||||
self.follow_redirects = True
|
||||
|
||||
|
||||
# Which HTTP methods do we apply optimistic concurrency to, i.e.
|
||||
# which methods get an "if-match:" etag header added to them.
|
||||
self.optimistic_concurrency_methods = ["PUT"]
|
||||
@@ -831,7 +831,7 @@ the same interface as FileCache."""
|
||||
|
||||
self.ignore_etag = False
|
||||
|
||||
self.force_exception_to_status_code = False
|
||||
self.force_exception_to_status_code = False
|
||||
|
||||
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)]
|
||||
auth = auths and sorted(auths)[0][1] or None
|
||||
if auth:
|
||||
if auth:
|
||||
auth.request(method, request_uri, headers, body)
|
||||
|
||||
(response, content) = self._conn_request(conn, request_uri, method, body, headers)
|
||||
|
||||
if auth:
|
||||
if auth:
|
||||
if auth.response(response, body):
|
||||
auth.request(method, request_uri, headers, body)
|
||||
(response, content) = self._conn_request(conn, request_uri, method, body, headers )
|
||||
@@ -921,7 +921,7 @@ the same interface as FileCache."""
|
||||
|
||||
if response.status == 401:
|
||||
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, )
|
||||
if response.status != 401:
|
||||
self.authorizations.append(authorization)
|
||||
@@ -944,7 +944,7 @@ the same interface as FileCache."""
|
||||
if response.status == 301 and method in ["GET", "HEAD"]:
|
||||
response['-x-permanent-redirect-url'] = response['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)
|
||||
if headers.has_key('if-none-match'):
|
||||
del headers['if-none-match']
|
||||
@@ -954,7 +954,7 @@ the same interface as FileCache."""
|
||||
location = response['location']
|
||||
old_response = copy.deepcopy(response)
|
||||
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
|
||||
(response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1)
|
||||
response.previous = old_response
|
||||
@@ -963,7 +963,7 @@ the same interface as FileCache."""
|
||||
elif response.status in [200, 203] and method == "GET":
|
||||
# Don't cache 206's since we aren't going to handle byte range requests
|
||||
if not response.has_key('content-location'):
|
||||
response['content-location'] = absolute_uri
|
||||
response['content-location'] = absolute_uri
|
||||
_updateCache(headers, response, content, self.cache, cachekey)
|
||||
|
||||
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):
|
||||
""" 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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
'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.
|
||||
|
||||
The return value is a tuple of (response, content), the first
|
||||
being and instance of the 'Response' class, the second being
|
||||
The return value is a tuple of (response, content), the first
|
||||
being and instance of the 'Response' class, the second being
|
||||
a string that contains the response entity body.
|
||||
"""
|
||||
try:
|
||||
@@ -1085,13 +1085,13 @@ a string that contains the response entity body.
|
||||
# Determine our course of action:
|
||||
# Is the cached entry fresh or stale?
|
||||
# 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
|
||||
# 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
|
||||
entry_disposition = _entry_disposition(info, headers)
|
||||
|
||||
entry_disposition = _entry_disposition(info, headers)
|
||||
|
||||
if entry_disposition == "FRESH":
|
||||
if not cached_value:
|
||||
info['status'] = '504'
|
||||
@@ -1113,7 +1113,7 @@ a string that contains the response entity body.
|
||||
|
||||
if response.status == 304 and method == "GET":
|
||||
# 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.
|
||||
# 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)
|
||||
response = merged_response
|
||||
response.status = 200
|
||||
response.fromcache = True
|
||||
response.fromcache = True
|
||||
|
||||
elif response.status == 200:
|
||||
content = new_content
|
||||
else:
|
||||
self.cache.delete(cachekey)
|
||||
content = new_content
|
||||
else:
|
||||
content = new_content
|
||||
else:
|
||||
cc = _parse_cache_control(headers)
|
||||
if cc.has_key('only-if-cached'):
|
||||
info['status'] = '504'
|
||||
@@ -1146,7 +1146,7 @@ a string that contains the response entity body.
|
||||
response = e.response
|
||||
content = e.content
|
||||
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)):
|
||||
content = "Request Timeout"
|
||||
response = Response( {
|
||||
@@ -1156,24 +1156,24 @@ a string that contains the response entity body.
|
||||
})
|
||||
response.reason = "Request Timeout"
|
||||
else:
|
||||
content = str(e)
|
||||
content = str(e)
|
||||
response = Response( {
|
||||
"content-type": "text/plain",
|
||||
"status": "400",
|
||||
"content-length": len(content)
|
||||
})
|
||||
response.reason = "Bad Request"
|
||||
response.reason = "Bad Request"
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
||||
return (response, content)
|
||||
|
||||
|
||||
|
||||
|
||||
class Response(dict):
|
||||
"""An object more like email.Message than httplib.HTTPResponse."""
|
||||
|
||||
|
||||
"""Is this response from our local cache"""
|
||||
fromcache = False
|
||||
|
||||
@@ -1189,27 +1189,27 @@ class Response(dict):
|
||||
previous = None
|
||||
|
||||
def __init__(self, info):
|
||||
# info is either an email.Message or
|
||||
# info is either an email.Message or
|
||||
# an httplib.HTTPResponse object.
|
||||
if isinstance(info, httplib.HTTPResponse):
|
||||
for key, value in info.getheaders():
|
||||
self[key.lower()] = value
|
||||
for key, value in info.getheaders():
|
||||
self[key.lower()] = value
|
||||
self.status = info.status
|
||||
self['status'] = str(self.status)
|
||||
self.reason = info.reason
|
||||
self.version = info.version
|
||||
elif isinstance(info, email.Message.Message):
|
||||
for key, value in info.items():
|
||||
self[key] = value
|
||||
for key, value in info.items():
|
||||
self[key] = value
|
||||
self.status = int(self['status'])
|
||||
else:
|
||||
for key, value in info.iteritems():
|
||||
self[key] = value
|
||||
for key, value in info.iteritems():
|
||||
self[key] = value
|
||||
self.status = int(self.get('status', self.status))
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == 'dict':
|
||||
return self
|
||||
else:
|
||||
raise AttributeError, name
|
||||
return self
|
||||
else:
|
||||
raise AttributeError, name
|
||||
|
||||
@@ -16,7 +16,7 @@ import urlparse
|
||||
|
||||
|
||||
# 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:
|
||||
#
|
||||
# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
|
||||
@@ -49,7 +49,7 @@ escape_range = [
|
||||
(0xF0000, 0xFFFFD ),
|
||||
(0x100000, 0x10FFFD)
|
||||
]
|
||||
|
||||
|
||||
def encode(c):
|
||||
retval = c
|
||||
i = ord(c)
|
||||
@@ -63,19 +63,19 @@ def encode(c):
|
||||
|
||||
|
||||
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
|
||||
the IRI before passing it into the function."""
|
||||
the IRI before passing it into the function."""
|
||||
if isinstance(uri ,unicode):
|
||||
(scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
|
||||
authority = authority.encode('idna')
|
||||
# For each character in 'ucschar' or 'iprivate'
|
||||
# 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 = "".join([encode(c) for c in uri])
|
||||
return uri
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import unittest
|
||||
|
||||
@@ -83,7 +83,7 @@ if __name__ == "__main__":
|
||||
|
||||
def test_uris(self):
|
||||
"""Test that URIs are invariant under the transformation."""
|
||||
invariant = [
|
||||
invariant = [
|
||||
u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
|
||||
u"http://www.ietf.org/rfc/rfc2396.txt",
|
||||
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" ]
|
||||
for uri in invariant:
|
||||
self.assertEqual(uri, iri2uri(uri))
|
||||
|
||||
|
||||
def test_iri(self):
|
||||
""" 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}"))
|
||||
@@ -107,4 +107,4 @@ if __name__ == "__main__":
|
||||
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def autohandler(template, context, name='autohandler'):
|
||||
if len(tokens) == 1:
|
||||
break
|
||||
tokens[-2:] = [name]
|
||||
|
||||
|
||||
if not lookup.filesystem_checks:
|
||||
return lookup._uri_cache.setdefault(
|
||||
(autohandler, _template_uri, name), None)
|
||||
@@ -62,4 +62,4 @@ def _file_exists(lookup, path):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
# This module is part of Mako and is released under
|
||||
# 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"""
|
||||
|
||||
import re
|
||||
|
||||
def convert_comments(text):
|
||||
"""preprocess old style comments.
|
||||
|
||||
|
||||
example:
|
||||
|
||||
|
||||
from mako.ext.preprocessors import convert_comments
|
||||
t = Template(..., preprocessor=preprocess_comments)"""
|
||||
return re.sub(r'(?<=\n)\s*#[^#]', "##", text)
|
||||
|
||||
@@ -279,7 +279,7 @@ def auth(u, p):
|
||||
global user, password
|
||||
user = u
|
||||
password = p
|
||||
|
||||
|
||||
def hpauth(u, p):
|
||||
"""Set the username and password to be used in subsequent queries to
|
||||
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
|
||||
username/password are left unspecified, respectively.
|
||||
"""
|
||||
global parser_fun
|
||||
global parser_fun
|
||||
|
||||
if args is None:
|
||||
args = {}
|
||||
@@ -638,7 +638,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
|
||||
if hostname == '144.76.94.239:8181':
|
||||
base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '')
|
||||
req.add_header("Authorization", "Basic %s" % base64string)
|
||||
|
||||
|
||||
_log.debug("requesting with UA %s" % _useragent)
|
||||
if body:
|
||||
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`.
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
+60
-60
@@ -85,11 +85,11 @@ def generate_verifier(length=8):
|
||||
|
||||
class Consumer(object):
|
||||
"""A consumer of OAuth-protected services.
|
||||
|
||||
|
||||
The OAuth consumer is a "third-party" service that wants to access
|
||||
protected resources from an OAuth service provider on behalf of an end
|
||||
user. It's kind of the OAuth client.
|
||||
|
||||
|
||||
Usually a consumer must be registered with the service provider by the
|
||||
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
|
||||
@@ -97,7 +97,7 @@ class Consumer(object):
|
||||
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
|
||||
registered consumer.
|
||||
|
||||
|
||||
Once registered, the consumer can then use its consumer credentials to ask
|
||||
the service provider for a request token, kicking off the OAuth
|
||||
authorization process.
|
||||
@@ -125,12 +125,12 @@ class Consumer(object):
|
||||
class Token(object):
|
||||
"""An OAuth credential used to request authorization or a protected
|
||||
resource.
|
||||
|
||||
|
||||
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
|
||||
the signature, to prove that the requester is who the server gave the
|
||||
token to.
|
||||
|
||||
|
||||
When first negotiating the authorization, the consumer asks for a *request
|
||||
token* that the live user authorizes with the service provider. The
|
||||
consumer then exchanges the request token for an *access token* that can
|
||||
@@ -175,7 +175,7 @@ class Token(object):
|
||||
|
||||
def to_string(self):
|
||||
"""Returns this token as a plain string, suitable for storage.
|
||||
|
||||
|
||||
The resulting string includes the token's secret, so you should never
|
||||
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:
|
||||
data['oauth_callback_confirmed'] = self.callback_confirmed
|
||||
return urllib.urlencode(data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_string(s):
|
||||
"""Deserializes a token from a string like one returned by
|
||||
@@ -209,7 +209,7 @@ class Token(object):
|
||||
try:
|
||||
secret = params['oauth_token_secret'][0]
|
||||
except Exception:
|
||||
raise ValueError("'oauth_token_secret' not found in "
|
||||
raise ValueError("'oauth_token_secret' not found in "
|
||||
"OAuth request.")
|
||||
|
||||
token = Token(key, secret)
|
||||
@@ -225,45 +225,45 @@ class Token(object):
|
||||
|
||||
def setter(attr):
|
||||
name = attr.__name__
|
||||
|
||||
|
||||
def getter(self):
|
||||
try:
|
||||
return self.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
def deleter(self):
|
||||
del self.__dict__[name]
|
||||
|
||||
|
||||
return property(getter, attr, deleter)
|
||||
|
||||
|
||||
class Request(dict):
|
||||
|
||||
|
||||
"""The parameters and information for an HTTP request, suitable for
|
||||
authorizing with OAuth credentials.
|
||||
|
||||
|
||||
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
|
||||
key, and providing an access token authorized by the end user to access
|
||||
those resources.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
http_method = HTTP_METHOD
|
||||
http_url = None
|
||||
version = VERSION
|
||||
|
||||
|
||||
def __init__(self, method=HTTP_METHOD, url=None, parameters=None):
|
||||
if method is not None:
|
||||
self.method = method
|
||||
|
||||
|
||||
if url is not None:
|
||||
self.url = url
|
||||
|
||||
|
||||
if parameters is not None:
|
||||
self.update(parameters)
|
||||
|
||||
|
||||
@setter
|
||||
def url(self, value):
|
||||
parts = urlparse.urlparse(value)
|
||||
@@ -280,33 +280,33 @@ class Request(dict):
|
||||
|
||||
value = '%s://%s%s' % (scheme, netloc, path)
|
||||
self.__dict__['url'] = value
|
||||
|
||||
|
||||
@setter
|
||||
def method(self, value):
|
||||
self.__dict__['method'] = value.upper()
|
||||
|
||||
|
||||
def _get_timestamp_nonce(self):
|
||||
return self['oauth_timestamp'], self['oauth_nonce']
|
||||
|
||||
|
||||
def get_nonoauth_parameters(self):
|
||||
"""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_')])
|
||||
|
||||
|
||||
def to_header(self, realm=''):
|
||||
"""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_'))
|
||||
stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
|
||||
header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
|
||||
params_header = ', '.join(header_params)
|
||||
|
||||
|
||||
auth_header = 'OAuth realm="%s"' % realm
|
||||
if params_header:
|
||||
auth_header = "%s, %s" % (auth_header, params_header)
|
||||
|
||||
|
||||
return {'Authorization': auth_header}
|
||||
|
||||
|
||||
def to_postdata(self):
|
||||
"""Serialize as post data for a POST request."""
|
||||
return self.encode_postdata(self)
|
||||
@@ -327,7 +327,7 @@ class Request(dict):
|
||||
raise Error('Parameter not found: %s' % parameter)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def get_normalized_parameters(self):
|
||||
"""Return a string that contains the parameters that must be signed."""
|
||||
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)
|
||||
# Spaces must be encoded with "%20" instead of "+"
|
||||
return encoded_str.replace('+', '%20')
|
||||
|
||||
|
||||
def sign_request(self, signature_method, consumer, token):
|
||||
"""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'] = signature_method.sign(self, consumer, token)
|
||||
|
||||
|
||||
@classmethod
|
||||
def make_timestamp(cls):
|
||||
"""Get seconds since epoch (UTC)."""
|
||||
return str(int(time.time()))
|
||||
|
||||
|
||||
@classmethod
|
||||
def make_nonce(cls):
|
||||
"""Generate pseudorandom number."""
|
||||
return str(random.randint(0, 100000000))
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, http_method, http_url, headers=None, parameters=None,
|
||||
query_string=None):
|
||||
"""Combines multiple parameter sources."""
|
||||
if parameters is None:
|
||||
parameters = {}
|
||||
|
||||
|
||||
# Headers
|
||||
if headers and 'Authorization' in headers:
|
||||
auth_header = headers['Authorization']
|
||||
@@ -380,57 +380,57 @@ class Request(dict):
|
||||
except:
|
||||
raise Error('Unable to parse OAuth parameters from '
|
||||
'Authorization header.')
|
||||
|
||||
|
||||
# GET or POST query string.
|
||||
if query_string:
|
||||
query_params = cls._split_url_string(query_string)
|
||||
parameters.update(query_params)
|
||||
|
||||
|
||||
# URL parameters.
|
||||
param_str = urlparse.urlparse(http_url)[4] # query
|
||||
url_params = cls._split_url_string(param_str)
|
||||
parameters.update(url_params)
|
||||
|
||||
|
||||
if parameters:
|
||||
return cls(http_method, http_url, parameters)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_consumer_and_token(cls, consumer, token=None,
|
||||
http_method=HTTP_METHOD, http_url=None, parameters=None):
|
||||
if not parameters:
|
||||
parameters = {}
|
||||
|
||||
|
||||
defaults = {
|
||||
'oauth_consumer_key': consumer.key,
|
||||
'oauth_timestamp': cls.make_timestamp(),
|
||||
'oauth_nonce': cls.make_nonce(),
|
||||
'oauth_version': cls.version,
|
||||
}
|
||||
|
||||
|
||||
defaults.update(parameters)
|
||||
parameters = defaults
|
||||
|
||||
|
||||
if token:
|
||||
parameters['oauth_token'] = token.key
|
||||
|
||||
|
||||
return Request(http_method, http_url, parameters)
|
||||
|
||||
|
||||
@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):
|
||||
|
||||
if not parameters:
|
||||
parameters = {}
|
||||
|
||||
|
||||
parameters['oauth_token'] = token.key
|
||||
|
||||
|
||||
if callback:
|
||||
parameters['oauth_callback'] = callback
|
||||
|
||||
|
||||
return cls(http_method, http_url, parameters)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _split_header(header):
|
||||
"""Turn Authorization: header into parameters."""
|
||||
@@ -447,7 +447,7 @@ class Request(dict):
|
||||
# Remove quotes and unescape the value.
|
||||
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
|
||||
return params
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _split_url_string(param_str):
|
||||
"""Turn URL string into parameters."""
|
||||
@@ -460,7 +460,7 @@ class Request(dict):
|
||||
class Server(object):
|
||||
"""A skeletal implementation of a service provider, providing protected
|
||||
resources to requests from authorized consumers.
|
||||
|
||||
|
||||
This class implements the logic to check requests for authorization. You
|
||||
can use it with your web server or web framework to protect certain
|
||||
resources with OAuth.
|
||||
@@ -536,7 +536,7 @@ class Server(object):
|
||||
if not valid:
|
||||
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)
|
||||
|
||||
built = signature_method.sign(request, consumer, token)
|
||||
@@ -567,7 +567,7 @@ class Client(httplib2.Http):
|
||||
self.token = token
|
||||
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)
|
||||
|
||||
def set_signature_method(self, method):
|
||||
@@ -576,10 +576,10 @@ class Client(httplib2.Http):
|
||||
|
||||
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,
|
||||
force_auth_header=False):
|
||||
|
||||
|
||||
if not isinstance(headers, dict):
|
||||
headers = {}
|
||||
|
||||
@@ -587,7 +587,7 @@ class Client(httplib2.Http):
|
||||
parameters = dict(parse_qsl(body))
|
||||
elif method == "GET":
|
||||
parsed = urlparse.urlparse(uri)
|
||||
parameters = parse_qs(parsed.query)
|
||||
parameters = parse_qs(parsed.query)
|
||||
else:
|
||||
parameters = None
|
||||
|
||||
@@ -614,14 +614,14 @@ class Client(httplib2.Http):
|
||||
# don't call update twice.
|
||||
headers.update(req.to_header())
|
||||
|
||||
return httplib2.Http.request(self, uri, method=method, body=body,
|
||||
headers=headers, redirections=redirections,
|
||||
return httplib2.Http.request(self, uri, method=method, body=body,
|
||||
headers=headers, redirections=redirections,
|
||||
connection_type=connection_type)
|
||||
|
||||
|
||||
class SignatureMethod(object):
|
||||
"""A way of signing requests.
|
||||
|
||||
|
||||
The OAuth protocol lets consumers and service providers pick a way to sign
|
||||
requests. This interface shows the methods expected by the other `oauth`
|
||||
modules for signing requests. Subclass it and implement its methods to
|
||||
@@ -657,7 +657,7 @@ class SignatureMethod(object):
|
||||
|
||||
class SignatureMethod_HMAC_SHA1(SignatureMethod):
|
||||
name = 'HMAC-SHA1'
|
||||
|
||||
|
||||
def signing_base(self, request, consumer, token):
|
||||
sig = (
|
||||
escape(request.method),
|
||||
|
||||
@@ -36,6 +36,6 @@ class Library:
|
||||
if attributes.get('Play Count'):
|
||||
s.play_count = int(attributes.get('Play Count'))
|
||||
if attributes.get('Location'):
|
||||
s.location = attributes.get('Location')
|
||||
s.location = attributes.get('Location')
|
||||
songs.append(s)
|
||||
return songs
|
||||
@@ -42,5 +42,5 @@ class Song:
|
||||
album_rating = None
|
||||
play_count = None
|
||||
location = None
|
||||
|
||||
|
||||
#title = property(getTitle,setTitle)
|
||||
@@ -5,7 +5,7 @@ class XMLLibraryParser:
|
||||
s = f.read()
|
||||
lines = s.split("\n")
|
||||
self.dictionary = self.parser(lines)
|
||||
|
||||
|
||||
def getValue(self,restOfLine):
|
||||
value = re.sub("<.*?>","",restOfLine)
|
||||
u = unicode(value,"utf-8")
|
||||
|
||||
@@ -201,7 +201,7 @@ class GazelleAPI(object):
|
||||
Returns the inbox Mailbox for the logged in user
|
||||
"""
|
||||
return Mailbox(self, 'inbox', page, sort)
|
||||
|
||||
|
||||
def get_sentbox(self, page='1', sort='unread'):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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.boxtype = boxtype
|
||||
self.boxtype = boxtype
|
||||
self.current_page = page
|
||||
self.total_pages = None
|
||||
self.sort = sort
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/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)
|
||||
results[datas['apikey']] = res
|
||||
return results
|
||||
|
||||
|
||||
def callapi(self, method, path, args):
|
||||
headers = { 'User-Agent': USER_AGENT }
|
||||
if method == "POST":
|
||||
@@ -116,7 +116,7 @@ class PyNMA(object):
|
||||
'message': str(e)
|
||||
}
|
||||
pass
|
||||
|
||||
|
||||
return res
|
||||
|
||||
def _parse_reponse(self, response):
|
||||
@@ -133,5 +133,5 @@ class PyNMA(object):
|
||||
res['message'] = elem.firstChild.nodeValue
|
||||
res['type'] = elem.tagName
|
||||
return res
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -440,7 +440,7 @@ def merge_cookies(cookiejar, cookies):
|
||||
"""
|
||||
if not isinstance(cookiejar, cookielib.CookieJar):
|
||||
raise ValueError('You can only merge into CookieJar')
|
||||
|
||||
|
||||
if isinstance(cookies, dict):
|
||||
cookiejar = cookiejar_from_dict(
|
||||
cookies, cookiejar=cookiejar, overwrite=False)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
######################## BEGIN LICENSE BLOCK ########################
|
||||
# The Original Code is Mozilla Communicator client code.
|
||||
#
|
||||
#
|
||||
# The Initial Developer of the Original Code is
|
||||
# Netscape Communications Corporation.
|
||||
# Portions created by the Initial Developer are Copyright (C) 1998
|
||||
# the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
#
|
||||
# Contributor(s):
|
||||
# Mark Pilgrim - port to Python
|
||||
#
|
||||
@@ -13,12 +13,12 @@
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# 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,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# 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,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# 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,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
@@ -35,14 +35,14 @@
|
||||
#
|
||||
# Idea Distribution Ratio = 0.98653 / (1-0.98653) = 73.24
|
||||
# Random Distribution Ration = 512 / (2350-512) = 0.279.
|
||||
#
|
||||
# Typical Distribution Ratio
|
||||
#
|
||||
# Typical Distribution Ratio
|
||||
|
||||
EUCKR_TYPICAL_DISTRIBUTION_RATIO = 6.0
|
||||
|
||||
EUCKR_TABLE_SIZE = 2352
|
||||
|
||||
# Char to FreqOrder table ,
|
||||
# Char to FreqOrder table ,
|
||||
EUCKRCharToFreqOrder = ( \
|
||||
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,
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# 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,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# 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,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
|
||||
@@ -39,7 +39,7 @@ def unidecode(string):
|
||||
if codepoint < 0x80: # Basic ASCII
|
||||
retval.append(str(char))
|
||||
continue
|
||||
|
||||
|
||||
if codepoint > 0xeffff:
|
||||
continue # Characters in Private Use Area and above are ignored
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ class SafeConstructor(BaseConstructor):
|
||||
return str(value).decode('base64')
|
||||
except (binascii.Error, UnicodeEncodeError), exc:
|
||||
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(
|
||||
ur'''^(?P<year>[0-9][0-9][0-9][0-9])
|
||||
|
||||
+1
-1
@@ -674,7 +674,7 @@ class Emitter(object):
|
||||
# Check for indicators.
|
||||
if index == 0:
|
||||
# Leading indicators are special characters.
|
||||
if ch in u'#,[]{}&*!|>\'\"%@`':
|
||||
if ch in u'#,[]{}&*!|>\'\"%@`':
|
||||
flow_indicators = True
|
||||
block_indicators = True
|
||||
if ch in u'?:':
|
||||
|
||||
+1
-1
@@ -482,7 +482,7 @@ class Parser(object):
|
||||
token = self.peek_token()
|
||||
raise ParserError("while parsing a flow sequence", self.marks[-1],
|
||||
"expected ',' or ']', but got %r" % token.id, token.start_mark)
|
||||
|
||||
|
||||
if self.check_token(KeyToken):
|
||||
token = self.peek_token()
|
||||
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.
|
||||
if self.flow_level in self.possible_simple_keys:
|
||||
key = self.possible_simple_keys[self.flow_level]
|
||||
|
||||
|
||||
if key.required:
|
||||
raise ScannerError("while scanning a simple key", key.mark,
|
||||
"could not found expected ':'", self.get_mark())
|
||||
@@ -363,11 +363,11 @@ class Scanner(object):
|
||||
|
||||
# Read the token.
|
||||
mark = self.get_mark()
|
||||
|
||||
|
||||
# Add STREAM-START.
|
||||
self.tokens.append(StreamStartToken(mark, mark,
|
||||
encoding=self.encoding))
|
||||
|
||||
|
||||
|
||||
def fetch_stream_end(self):
|
||||
|
||||
@@ -381,7 +381,7 @@ class Scanner(object):
|
||||
|
||||
# Read the token.
|
||||
mark = self.get_mark()
|
||||
|
||||
|
||||
# Add STREAM-END.
|
||||
self.tokens.append(StreamEndToken(mark, mark))
|
||||
|
||||
@@ -389,7 +389,7 @@ class Scanner(object):
|
||||
self.done = True
|
||||
|
||||
def fetch_directive(self):
|
||||
|
||||
|
||||
# Set the current intendation to -1.
|
||||
self.unwind_indent(-1)
|
||||
|
||||
@@ -516,7 +516,7 @@ class Scanner(object):
|
||||
self.tokens.append(BlockEntryToken(start_mark, end_mark))
|
||||
|
||||
def fetch_key(self):
|
||||
|
||||
|
||||
# Block context needs additional checks.
|
||||
if not self.flow_level:
|
||||
|
||||
@@ -566,7 +566,7 @@ class Scanner(object):
|
||||
|
||||
# It must be a part of a complex key.
|
||||
else:
|
||||
|
||||
|
||||
# Block context needs additional checks.
|
||||
# (Do we really need them? They will be catched by the parser
|
||||
# anyway.)
|
||||
@@ -1024,14 +1024,14 @@ class Scanner(object):
|
||||
# Unfortunately, folding rules are ambiguous.
|
||||
#
|
||||
# This is the folding according to the specification:
|
||||
|
||||
|
||||
if folded and line_break == u'\n' \
|
||||
and leading_non_space and self.peek() not in u' \t':
|
||||
if not breaks:
|
||||
chunks.append(u' ')
|
||||
else:
|
||||
chunks.append(line_break)
|
||||
|
||||
|
||||
# This is Clark Evans's interpretation (also in the spec
|
||||
# examples):
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user