548 changed files with 24450 additions and 66679 deletions
@ -1,84 +1,85 @@ |
|||
from couchpotato.api import api_docs, api_docs_missing |
|||
from couchpotato.api import api_docs, api_docs_missing, api |
|||
from couchpotato.core.auth import requires_auth |
|||
from couchpotato.core.event import fireEvent |
|||
from couchpotato.core.helpers.request import getParams, jsonified |
|||
from couchpotato.core.helpers.variable import md5 |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.environment import Env |
|||
from flask.app import Flask |
|||
from flask.blueprints import Blueprint |
|||
from flask.globals import request |
|||
from flask.helpers import url_for |
|||
from flask.templating import render_template |
|||
from sqlalchemy.engine import create_engine |
|||
from sqlalchemy.orm import scoped_session |
|||
from sqlalchemy.orm.session import sessionmaker |
|||
from werkzeug.utils import redirect |
|||
from tornado import template |
|||
from tornado.web import RequestHandler |
|||
import os |
|||
import time |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
app = Flask(__name__, static_folder = 'nope') |
|||
web = Blueprint('web', __name__) |
|||
views = {} |
|||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) |
|||
|
|||
# Main web handler |
|||
@requires_auth |
|||
class WebHandler(RequestHandler): |
|||
def get(self, route, *args, **kwargs): |
|||
route = route.strip('/') |
|||
if not views.get(route): |
|||
page_not_found(self) |
|||
return |
|||
self.write(views[route]()) |
|||
|
|||
def addView(route, func, static = False): |
|||
views[route] = func |
|||
|
|||
def get_session(engine = None): |
|||
return Env.getSession(engine) |
|||
|
|||
def addView(route, func, static = False): |
|||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func) |
|||
|
|||
""" Web view """ |
|||
@web.route('/') |
|||
@requires_auth |
|||
# Web view |
|||
def index(): |
|||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) |
|||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) |
|||
addView('', index) |
|||
|
|||
""" Api view """ |
|||
@web.route('docs/') |
|||
@requires_auth |
|||
# API docs |
|||
def apiDocs(): |
|||
from couchpotato import app |
|||
routes = [] |
|||
for route, x in sorted(app.view_functions.iteritems()): |
|||
if route[0:4] == 'api.': |
|||
routes += [route[4:].replace('::', '.')] |
|||
|
|||
for route in api.iterkeys(): |
|||
routes.append(route) |
|||
|
|||
if api_docs.get(''): |
|||
del api_docs[''] |
|||
del api_docs_missing[''] |
|||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) |
|||
|
|||
@web.route('getkey/') |
|||
def getApiKey(): |
|||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env) |
|||
|
|||
api = None |
|||
params = getParams() |
|||
username = Env.setting('username') |
|||
password = Env.setting('password') |
|||
addView('docs', apiDocs) |
|||
|
|||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): |
|||
api = Env.setting('api_key') |
|||
# Make non basic auth option to get api key |
|||
class KeyHandler(RequestHandler): |
|||
def get(self, *args, **kwargs): |
|||
api = None |
|||
username = Env.setting('username') |
|||
password = Env.setting('password') |
|||
|
|||
return jsonified({ |
|||
'success': api is not None, |
|||
'api_key': api |
|||
}) |
|||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): |
|||
api = Env.setting('api_key') |
|||
|
|||
@app.errorhandler(404) |
|||
def page_not_found(error): |
|||
index_url = url_for('web.index') |
|||
url = request.path[len(index_url):] |
|||
self.write({ |
|||
'success': api is not None, |
|||
'api_key': api |
|||
}) |
|||
|
|||
def page_not_found(rh): |
|||
index_url = Env.get('web_base') |
|||
url = rh.request.uri[len(index_url):] |
|||
|
|||
if url[:3] != 'api': |
|||
if request.path != '/': |
|||
r = request.url.replace(request.path, index_url + '#' + url) |
|||
else: |
|||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) |
|||
return redirect(r) |
|||
r = index_url + '#' + url.lstrip('/') |
|||
rh.redirect(r) |
|||
else: |
|||
if not Env.get('dev'): |
|||
time.sleep(0.1) |
|||
return 'Wrong API key used', 404 |
|||
|
|||
rh.set_status(404) |
|||
rh.write('Wrong API key used') |
|||
|
|||
|
@ -1,26 +1,40 @@ |
|||
from couchpotato.core.helpers.variable import md5 |
|||
from couchpotato.environment import Env |
|||
from flask import request, Response |
|||
from functools import wraps |
|||
import base64 |
|||
|
|||
def check_auth(username, password): |
|||
return username == Env.setting('username') and password == Env.setting('password') |
|||
|
|||
def authenticate(): |
|||
return Response( |
|||
'This is not the page you are looking for. *waves hand*', 401, |
|||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'} |
|||
) |
|||
def requires_auth(handler_class): |
|||
|
|||
def requires_auth(f): |
|||
def wrap_execute(handler_execute): |
|||
|
|||
@wraps(f) |
|||
def decorated(*args, **kwargs): |
|||
auth = getattr(request, 'authorization') |
|||
if Env.setting('username') and Env.setting('password'): |
|||
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))): |
|||
return authenticate() |
|||
def require_basic_auth(handler, kwargs): |
|||
if Env.setting('username') and Env.setting('password'): |
|||
|
|||
return f(*args, **kwargs) |
|||
auth_header = handler.request.headers.get('Authorization') |
|||
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None |
|||
if auth_decoded: |
|||
username, password = auth_decoded.split(':', 2) |
|||
|
|||
return decorated |
|||
if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))): |
|||
handler.set_status(401) |
|||
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"') |
|||
handler._transforms = [] |
|||
handler.finish() |
|||
|
|||
return False |
|||
|
|||
return True |
|||
|
|||
def _execute(self, transforms, *args, **kwargs): |
|||
|
|||
if not require_basic_auth(self, kwargs): |
|||
return False |
|||
return handler_execute(self, transforms, *args, **kwargs) |
|||
|
|||
return _execute |
|||
|
|||
handler_class._execute = wrap_execute(handler_class._execute) |
|||
|
|||
return handler_class |
|||
|
@ -1,22 +1,92 @@ |
|||
from couchpotato import get_session |
|||
from couchpotato.api import addApiView |
|||
from couchpotato.core.event import fireEvent |
|||
from couchpotato.core.helpers.request import jsonified, getParam |
|||
from couchpotato.core.helpers.encoding import ss |
|||
from couchpotato.core.helpers.variable import splitString, md5 |
|||
from couchpotato.core.plugins.base import Plugin |
|||
from couchpotato.core.settings.model import Movie |
|||
from couchpotato.environment import Env |
|||
from sqlalchemy.sql.expression import or_ |
|||
|
|||
class Suggestion(Plugin): |
|||
|
|||
def __init__(self): |
|||
|
|||
addApiView('suggestion.view', self.getView) |
|||
addApiView('suggestion.view', self.suggestView) |
|||
addApiView('suggestion.ignore', self.ignoreView) |
|||
|
|||
def getView(self): |
|||
def suggestView(self, **kwargs): |
|||
|
|||
limit_offset = getParam('limit_offset', None) |
|||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True) |
|||
movies = splitString(kwargs.get('movies', '')) |
|||
ignored = splitString(kwargs.get('ignored', '')) |
|||
limit = kwargs.get('limit', 6) |
|||
|
|||
return jsonified({ |
|||
if not movies or len(movies) == 0: |
|||
db = get_session() |
|||
active_movies = db.query(Movie) \ |
|||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all() |
|||
movies = [x.library.identifier for x in active_movies] |
|||
|
|||
if not ignored or len(ignored) == 0: |
|||
ignored = splitString(Env.prop('suggest_ignore', default = '')) |
|||
|
|||
cached_suggestion = self.getCache('suggestion_cached') |
|||
if cached_suggestion: |
|||
suggestions = cached_suggestion |
|||
else: |
|||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True) |
|||
self.setCache(md5(ss('suggestion_cached')), suggestions, timeout = 6048000) # Cache for 10 weeks |
|||
|
|||
return { |
|||
'success': True, |
|||
'empty': len(movies) == 0, |
|||
'total': total_movies, |
|||
'movies': movies, |
|||
}) |
|||
'count': len(suggestions), |
|||
'suggestions': suggestions[:limit] |
|||
} |
|||
|
|||
def ignoreView(self, imdb = None, limit = 6, remove_only = False, **kwargs): |
|||
|
|||
ignored = splitString(Env.prop('suggest_ignore', default = '')) |
|||
|
|||
if imdb: |
|||
if not remove_only: |
|||
ignored.append(imdb) |
|||
Env.prop('suggest_ignore', ','.join(set(ignored))) |
|||
|
|||
new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored) |
|||
|
|||
return { |
|||
'result': True, |
|||
'ignore_count': len(ignored), |
|||
'suggestions': new_suggestions[limit - 1:limit] |
|||
} |
|||
|
|||
def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None): |
|||
|
|||
# Combine with previous suggestion_cache |
|||
cached_suggestion = self.getCache('suggestion_cached') |
|||
new_suggestions = [] |
|||
|
|||
if ignore_imdb: |
|||
for cs in cached_suggestion: |
|||
if cs.get('imdb') != ignore_imdb: |
|||
new_suggestions.append(cs) |
|||
|
|||
# Get new results and add them |
|||
if len(new_suggestions) - 1 < limit: |
|||
|
|||
db = get_session() |
|||
active_movies = db.query(Movie) \ |
|||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all() |
|||
movies = [x.library.identifier for x in active_movies] |
|||
|
|||
if ignored: |
|||
ignored.extend([x.get('imdb') for x in new_suggestions]) |
|||
|
|||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True) |
|||
|
|||
if suggestions: |
|||
new_suggestions.extend(suggestions) |
|||
|
|||
self.setCache(md5(ss('suggestion_cached')), new_suggestions, timeout = 6048000) |
|||
|
|||
return new_suggestions |
|||
|
@ -0,0 +1,84 @@ |
|||
.suggestions { |
|||
} |
|||
|
|||
.suggestions > h2 { |
|||
height: 40px; |
|||
} |
|||
|
|||
.suggestions .movie_result { |
|||
display: inline-block; |
|||
width: 33.333%; |
|||
height: 150px; |
|||
} |
|||
|
|||
@media all and (max-width: 960px) { |
|||
.suggestions .movie_result { |
|||
width: 50%; |
|||
} |
|||
} |
|||
|
|||
@media all and (max-width: 600px) { |
|||
.suggestions .movie_result { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
.suggestions .movie_result .data { |
|||
left: 100px; |
|||
background: #4e5969; |
|||
border: none; |
|||
} |
|||
|
|||
.suggestions .movie_result .data .info { |
|||
top: 15px; |
|||
left: 15px; |
|||
right: 15px; |
|||
} |
|||
|
|||
.suggestions .movie_result .data .info h2 { |
|||
white-space: normal; |
|||
max-height: 120px; |
|||
font-size: 18px; |
|||
line-height: 18px; |
|||
} |
|||
|
|||
.suggestions .movie_result .data .info .year { |
|||
position: static; |
|||
display: block; |
|||
margin: 5px 0 0; |
|||
padding: 0; |
|||
opacity: .6; |
|||
} |
|||
|
|||
.suggestions .movie_result .data { |
|||
cursor: default; |
|||
} |
|||
|
|||
.suggestions .movie_result .options { |
|||
left: 100px; |
|||
} |
|||
|
|||
.suggestions .movie_result .thumbnail { |
|||
width: 100px; |
|||
} |
|||
|
|||
.suggestions .movie_result .actions { |
|||
position: absolute; |
|||
bottom: 10px; |
|||
right: 10px; |
|||
display: none; |
|||
width: 120px; |
|||
} |
|||
.suggestions .movie_result:hover .actions { |
|||
display: block; |
|||
} |
|||
.suggestions .movie_result .data.open .actions { |
|||
display: none; |
|||
} |
|||
|
|||
.suggestions .movie_result .actions a { |
|||
margin-left: 10px; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
|
@ -0,0 +1,102 @@ |
|||
var SuggestList = new Class({ |
|||
|
|||
Implements: [Options, Events], |
|||
|
|||
initialize: function(options){ |
|||
var self = this; |
|||
self.setOptions(options); |
|||
|
|||
self.create(); |
|||
}, |
|||
|
|||
create: function(){ |
|||
var self = this; |
|||
|
|||
self.el = new Element('div.suggestions', { |
|||
'events': { |
|||
'click:relay(a.delete)': function(e, el){ |
|||
(e).stop(); |
|||
|
|||
$(el).getParent('.movie_result').destroy(); |
|||
|
|||
Api.request('suggestion.ignore', { |
|||
'data': { |
|||
'imdb': el.get('data-ignore') |
|||
}, |
|||
'onComplete': self.fill.bind(self) |
|||
}); |
|||
|
|||
} |
|||
} |
|||
}).grab( |
|||
new Element('h2', { |
|||
'text': 'You might like these' |
|||
}) |
|||
); |
|||
|
|||
self.api_request = Api.request('suggestion.view', { |
|||
'onComplete': self.fill.bind(self) |
|||
}); |
|||
|
|||
}, |
|||
|
|||
fill: function(json){ |
|||
|
|||
var self = this; |
|||
|
|||
Object.each(json.suggestions, function(movie){ |
|||
|
|||
var m = new Block.Search.Item(movie, { |
|||
'onAdded': function(){ |
|||
self.afterAdded(m, movie) |
|||
} |
|||
}); |
|||
m.data_container.grab( |
|||
new Element('div.actions').adopt( |
|||
new Element('a.add.icon2', { |
|||
'title': 'Add movie with your default quality', |
|||
'data-add': movie.imdb, |
|||
'events': { |
|||
'click': m.showOptions.bind(m) |
|||
} |
|||
}), |
|||
$(new MA.IMDB(m)), |
|||
$(new MA.Trailer(m, { |
|||
'height': 150 |
|||
})), |
|||
new Element('a.delete.icon2', { |
|||
'title': 'Don\'t suggest this movie again', |
|||
'data-ignore': movie.imdb |
|||
}) |
|||
) |
|||
); |
|||
m.data_container.removeEvents('click'); |
|||
$(m).inject(self.el); |
|||
|
|||
}); |
|||
|
|||
}, |
|||
|
|||
afterAdded: function(m, movie){ |
|||
var self = this; |
|||
|
|||
setTimeout(function(){ |
|||
$(m).destroy(); |
|||
|
|||
Api.request('suggestion.ignore', { |
|||
'data': { |
|||
'imdb': movie.imdb, |
|||
'remove_only': true |
|||
}, |
|||
'onComplete': self.fill.bind(self) |
|||
}); |
|||
|
|||
}, 3000); |
|||
|
|||
}, |
|||
|
|||
toElement: function(){ |
|||
return this.el; |
|||
} |
|||
|
|||
}) |
@ -1,6 +0,0 @@ |
|||
from .main import V1Importer |
|||
|
|||
def start(): |
|||
return V1Importer() |
|||
|
|||
config = [] |
@ -1,30 +0,0 @@ |
|||
<html> |
|||
<head> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css"> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css"> |
|||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css"> |
|||
|
|||
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script> |
|||
|
|||
<script type="text/javascript"> |
|||
|
|||
window.addEvent('domready', function(){ |
|||
if($('old_db')) |
|||
$('old_db').addEvent('change', function(){ |
|||
$('form').submit(); |
|||
}); |
|||
}); |
|||
|
|||
</script> |
|||
|
|||
</head> |
|||
<body> |
|||
{% if message: %} |
|||
{{ message }} |
|||
{% else: %} |
|||
<form id="form" method="post" enctype="multipart/form-data"> |
|||
<input type="file" name="old_db" id="old_db" /> |
|||
</form> |
|||
{% endif %} |
|||
</body> |
|||
</html> |
@ -1,56 +0,0 @@ |
|||
from couchpotato.api import addApiView |
|||
from couchpotato.core.event import fireEventAsync |
|||
from couchpotato.core.helpers.variable import getImdb |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.plugins.base import Plugin |
|||
from couchpotato.environment import Env |
|||
from flask.globals import request |
|||
from flask.helpers import url_for |
|||
import os |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class V1Importer(Plugin): |
|||
|
|||
def __init__(self): |
|||
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST']) |
|||
|
|||
def fromOld(self): |
|||
|
|||
if request.method != 'POST': |
|||
return self.renderTemplate(__file__, 'form.html', url_for = url_for) |
|||
|
|||
file = request.files['old_db'] |
|||
|
|||
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db') |
|||
|
|||
if os.path.isfile(uploaded_file): |
|||
os.remove(uploaded_file) |
|||
|
|||
file.save(uploaded_file) |
|||
|
|||
try: |
|||
import sqlite3 |
|||
conn = sqlite3.connect(uploaded_file) |
|||
|
|||
wanted = [] |
|||
|
|||
t = ('want',) |
|||
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t) |
|||
for row in cur: |
|||
status, imdb = row |
|||
if getImdb(imdb): |
|||
wanted.append(imdb) |
|||
conn.close() |
|||
|
|||
wanted = set(wanted) |
|||
for imdb in wanted: |
|||
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False) |
|||
|
|||
message = 'Successfully imported %s movie(s)' % len(wanted) |
|||
except Exception, e: |
|||
message = 'Failed: %s' % e |
|||
|
|||
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message) |
|||
|
@ -0,0 +1,59 @@ |
|||
from .main import AwesomeHD |
|||
|
|||
def start(): |
|||
return AwesomeHD() |
|||
|
|||
config = [{ |
|||
'name': 'awesomehd', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'searcher', |
|||
'subtab': 'providers', |
|||
'list': 'torrent_providers', |
|||
'name': 'Awesome-HD', |
|||
'description': 'See <a href="https://awesome-hd.net">AHD</a>', |
|||
'wizard': True, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'type': 'enabler', |
|||
'default': False, |
|||
}, |
|||
{ |
|||
'name': 'passkey', |
|||
'default': '', |
|||
}, |
|||
{ |
|||
'name': 'only_internal', |
|||
'advanced': True, |
|||
'type': 'bool', |
|||
'default': 1, |
|||
'description': 'Only search for internal releases.' |
|||
}, |
|||
{ |
|||
'name': 'prefer_internal', |
|||
'advanced': True, |
|||
'type': 'bool', |
|||
'default': 1, |
|||
'description': 'Favors internal releases over non-internal releases.' |
|||
}, |
|||
{ |
|||
'name': 'favor', |
|||
'advanced': True, |
|||
'default': 'both', |
|||
'type': 'dropdown', |
|||
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')], |
|||
'description': 'Give extra scoring to encodes or remuxes.' |
|||
}, |
|||
{ |
|||
'name': 'extra_score', |
|||
'advanced': True, |
|||
'type': 'int', |
|||
'default': 20, |
|||
'description': 'Starting score for each release found via this provider.', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}] |
|||
|
@ -0,0 +1,64 @@ |
|||
from bs4 import BeautifulSoup |
|||
from couchpotato.core.helpers.variable import tryInt |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.providers.torrent.base import TorrentProvider |
|||
import re |
|||
import traceback |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class AwesomeHD(TorrentProvider): |
|||
|
|||
urls = { |
|||
'test' : 'https://awesome-hd.net/', |
|||
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s', |
|||
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s', |
|||
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s', |
|||
} |
|||
http_time_between_calls = 1 |
|||
|
|||
def _search(self, movie, quality, results): |
|||
|
|||
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal'))) |
|||
|
|||
if data: |
|||
try: |
|||
soup = BeautifulSoup(data) |
|||
authkey = soup.find('authkey').get_text() |
|||
entries = soup.find_all('torrent') |
|||
|
|||
for entry in entries: |
|||
|
|||
torrentscore = 0 |
|||
torrent_id = entry.find('id').get_text() |
|||
name = entry.find('name').get_text() |
|||
year = entry.find('year').get_text() |
|||
releasegroup = entry.find('releasegroup').get_text() |
|||
resolution = entry.find('resolution').get_text() |
|||
encoding = entry.find('encoding').get_text() |
|||
freeleech = entry.find('freeleech').get_text() |
|||
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding) |
|||
|
|||
if freeleech == '0.25' and self.conf('prefer_internal'): |
|||
torrent_desc += '/ Internal' |
|||
torrentscore += 200 |
|||
|
|||
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']: |
|||
torrentscore += 300 |
|||
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']: |
|||
torrentscore += 200 |
|||
|
|||
results.append({ |
|||
'id': torrent_id, |
|||
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)), |
|||
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')), |
|||
'detail_url': self.urls['detail'] % torrent_id, |
|||
'size': self.parseSize(entry.find('size').get_text()), |
|||
'seeders': tryInt(entry.find('seeders').get_text()), |
|||
'leechers': tryInt(entry.find('leechers').get_text()), |
|||
'score': torrentscore |
|||
}) |
|||
|
|||
except: |
|||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) |
@ -0,0 +1,42 @@ |
|||
from .main import TorrentBytes |
|||
|
|||
def start(): |
|||
return TorrentBytes() |
|||
|
|||
config = [{ |
|||
'name': 'torrentbytes', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'searcher', |
|||
'subtab': 'providers', |
|||
'list': 'torrent_providers', |
|||
'name': 'TorrentBytes', |
|||
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>', |
|||
'wizard': True, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'type': 'enabler', |
|||
'default': False, |
|||
}, |
|||
{ |
|||
'name': 'username', |
|||
'default': '', |
|||
}, |
|||
{ |
|||
'name': 'password', |
|||
'default': '', |
|||
'type': 'password', |
|||
}, |
|||
{ |
|||
'name': 'extra_score', |
|||
'advanced': True, |
|||
'label': 'Extra Score', |
|||
'type': 'int', |
|||
'default': 20, |
|||
'description': 'Starting score for each release found via this provider.', |
|||
} |
|||
], |
|||
}, |
|||
], |
|||
}] |
@ -0,0 +1,82 @@ |
|||
from bs4 import BeautifulSoup |
|||
from couchpotato.core.helpers.encoding import tryUrlencode |
|||
from couchpotato.core.helpers.variable import tryInt |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.providers.torrent.base import TorrentProvider |
|||
import traceback |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class TorrentBytes(TorrentProvider): |
|||
|
|||
urls = { |
|||
'test' : 'https://www.torrentbytes.net/', |
|||
'login' : 'https://www.torrentbytes.net/takelogin.php', |
|||
'login_check' : 'https://www.torrentbytes.net/inbox.php', |
|||
'detail' : 'https://www.torrentbytes.net/details.php?id=%s', |
|||
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d', |
|||
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s', |
|||
} |
|||
|
|||
cat_ids = [ |
|||
([5], ['720p', '1080p']), |
|||
([19], ['cam']), |
|||
([19], ['ts', 'tc']), |
|||
([19], ['r5', 'scr']), |
|||
([19], ['dvdrip']), |
|||
([5], ['brrip']), |
|||
([20], ['dvdr']), |
|||
] |
|||
|
|||
http_time_between_calls = 1 #seconds |
|||
cat_backup_id = None |
|||
|
|||
def _searchOnTitle(self, title, movie, quality, results): |
|||
|
|||
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0]) |
|||
data = self.getHTMLData(url, opener = self.login_opener) |
|||
|
|||
if data: |
|||
html = BeautifulSoup(data) |
|||
|
|||
try: |
|||
result_table = html.find('table', attrs = {'border' : '1'}) |
|||
if not result_table: |
|||
return |
|||
|
|||
entries = result_table.find_all('tr') |
|||
|
|||
for result in entries[1:]: |
|||
cells = result.find_all('td') |
|||
|
|||
link = cells[1].find('a', attrs = {'class' : 'index'}) |
|||
|
|||
full_id = link['href'].replace('details.php?id=', '') |
|||
torrent_id = full_id[:6] |
|||
|
|||
results.append({ |
|||
'id': torrent_id, |
|||
'name': link.contents[0], |
|||
'url': self.urls['download'] % (torrent_id, link.contents[0]), |
|||
'detail_url': self.urls['detail'] % torrent_id, |
|||
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]), |
|||
'seeders': tryInt(cells[8].find('span').contents[0]), |
|||
'leechers': tryInt(cells[9].find('span').contents[0]), |
|||
}) |
|||
|
|||
except: |
|||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc())) |
|||
|
|||
def getLoginParams(self): |
|||
return tryUrlencode({ |
|||
'username': self.conf('username'), |
|||
'password': self.conf('password'), |
|||
'login': 'submit', |
|||
}) |
|||
|
|||
def loginSuccess(self, output): |
|||
return 'logout.php' in output.lower() or 'Welcome' in output.lower() |
|||
|
|||
loginCheckSuccess = loginSuccess |
|||
|
@ -0,0 +1,33 @@ |
|||
from main import Yify |
|||
|
|||
def start(): |
|||
return Yify() |
|||
|
|||
config = [{ |
|||
'name': 'yify', |
|||
'groups': [ |
|||
{ |
|||
'tab': 'searcher', |
|||
'subtab': 'providers', |
|||
'list': 'torrent_providers', |
|||
'name': 'Yify', |
|||
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.', |
|||
'wizard': False, |
|||
'options': [ |
|||
{ |
|||
'name': 'enabled', |
|||
'type': 'enabler', |
|||
'default': 0 |
|||
}, |
|||
{ |
|||
'name': 'extra_score', |
|||
'advanced': True, |
|||
'label': 'Extra Score', |
|||
'type': 'int', |
|||
'default': 0, |
|||
'description': 'Starting score for each release found via this provider.', |
|||
} |
|||
], |
|||
} |
|||
] |
|||
}] |
@ -0,0 +1,53 @@ |
|||
from couchpotato.core.helpers.variable import tryInt |
|||
from couchpotato.core.logger import CPLog |
|||
from couchpotato.core.providers.torrent.base import TorrentProvider |
|||
import traceback |
|||
|
|||
log = CPLog(__name__) |
|||
|
|||
|
|||
class Yify(TorrentProvider): |
|||
|
|||
urls = { |
|||
'test' : 'https://yify-torrents.com/api', |
|||
'search' : 'https://yify-torrents.com/api/list.json?keywords=%s&quality=%s', |
|||
'detail': 'https://yify-torrents.com/api/movie.json?id=%s' |
|||
} |
|||
|
|||
http_time_between_calls = 1 #seconds |
|||
|
|||
def search(self, movie, quality): |
|||
|
|||
if not quality.get('hd', False): |
|||
return [] |
|||
|
|||
return super(Yify, self).search(movie, quality) |
|||
|
|||
def _searchOnTitle(self, title, movie, quality, results): |
|||
|
|||
data = self.getJsonData(self.urls['search'] % (title, quality['identifier'])) |
|||
|
|||
if data and data.get('MovieList'): |
|||
try: |
|||
for result in data.get('MovieList'): |
|||
|
|||
try: |
|||
title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._') |
|||
title = title.replace('.-.', '-') |
|||
title = title.replace('..', '.') |
|||
except: |
|||
continue |
|||
|
|||
results.append({ |
|||
'id': result['MovieID'], |
|||
'name': title, |
|||
'url': result['TorrentUrl'], |
|||
'detail_url': self.urls['detail'] % result['MovieID'], |
|||
'size': self.parseSize(result['Size']), |
|||
'seeders': tryInt(result['TorrentSeeds']), |
|||
'leechers': tryInt(result['TorrentPeers']) |
|||
}) |
|||
|
|||
except: |
|||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) |
|||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,262 @@ |
|||
""" |
|||
copied from |
|||
werkzeug.contrib.cache |
|||
~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from cache.posixemulation import rename |
|||
from itertools import izip |
|||
from time import time |
|||
import os |
|||
import re |
|||
import tempfile |
|||
try: |
|||
from hashlib import md5 |
|||
except ImportError: |
|||
from md5 import new as md5 |
|||
|
|||
try: |
|||
import cPickle as pickle |
|||
except ImportError: |
|||
import pickle |
|||
|
|||
|
|||
def _items(mappingorseq): |
|||
"""Wrapper for efficient iteration over mappings represented by dicts |
|||
or sequences:: |
|||
|
|||
>>> for k, v in _items((i, i*i) for i in xrange(5)): |
|||
... assert k*k == v |
|||
|
|||
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))): |
|||
... assert k*k == v |
|||
|
|||
""" |
|||
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \ |
|||
else mappingorseq |
|||
|
|||
|
|||
class BaseCache(object): |
|||
"""Baseclass for the cache systems. All the cache systems implement this |
|||
API or a superset of it. |
|||
|
|||
:param default_timeout: the default timeout that is used if no timeout is |
|||
specified on :meth:`set`. |
|||
""" |
|||
|
|||
def __init__(self, default_timeout = 300): |
|||
self.default_timeout = default_timeout |
|||
|
|||
def delete(self, key): |
|||
"""Deletes `key` from the cache. If it does not exist in the cache |
|||
nothing happens. |
|||
|
|||
:param key: the key to delete. |
|||
""" |
|||
pass |
|||
|
|||
def get_many(self, *keys): |
|||
"""Returns a list of values for the given keys. |
|||
For each key a item in the list is created. Example:: |
|||
|
|||
foo, bar = cache.get_many("foo", "bar") |
|||
|
|||
If a key can't be looked up `None` is returned for that key |
|||
instead. |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
return map(self.get, keys) |
|||
|
|||
def get_dict(self, *keys): |
|||
"""Works like :meth:`get_many` but returns a dict:: |
|||
|
|||
d = cache.get_dict("foo", "bar") |
|||
foo = d["foo"] |
|||
bar = d["bar"] |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
return dict(izip(keys, self.get_many(*keys))) |
|||
|
|||
def set(self, key, value, timeout = None): |
|||
"""Adds a new key/value to the cache (overwrites value, if key already |
|||
exists in the cache). |
|||
|
|||
:param key: the key to set |
|||
:param value: the value for the key |
|||
:param timeout: the cache timeout for the key (if not specified, |
|||
it uses the default timeout). |
|||
""" |
|||
pass |
|||
|
|||
def add(self, key, value, timeout = None): |
|||
"""Works like :meth:`set` but does not overwrite the values of already |
|||
existing keys. |
|||
|
|||
:param key: the key to set |
|||
:param value: the value for the key |
|||
:param timeout: the cache timeout for the key or the default |
|||
timeout if not specified. |
|||
""" |
|||
pass |
|||
|
|||
def set_many(self, mapping, timeout = None): |
|||
"""Sets multiple keys and values from a mapping. |
|||
|
|||
:param mapping: a mapping with the keys/values to set. |
|||
:param timeout: the cache timeout for the key (if not specified, |
|||
it uses the default timeout). |
|||
""" |
|||
for key, value in _items(mapping): |
|||
self.set(key, value, timeout) |
|||
|
|||
def delete_many(self, *keys): |
|||
"""Deletes multiple keys at once. |
|||
|
|||
:param keys: The function accepts multiple keys as positional |
|||
arguments. |
|||
""" |
|||
for key in keys: |
|||
self.delete(key) |
|||
|
|||
def clear(self): |
|||
"""Clears the cache. Keep in mind that not all caches support |
|||
completely clearing the cache. |
|||
""" |
|||
pass |
|||
|
|||
def inc(self, key, delta = 1): |
|||
"""Increments the value of a key by `delta`. If the key does |
|||
not yet exist it is initialized with `delta`. |
|||
|
|||
For supporting caches this is an atomic operation. |
|||
|
|||
:param key: the key to increment. |
|||
:param delta: the delta to add. |
|||
""" |
|||
self.set(key, (self.get(key) or 0) + delta) |
|||
|
|||
def dec(self, key, delta = 1): |
|||
"""Decrements the value of a key by `delta`. If the key does |
|||
not yet exist it is initialized with `-delta`. |
|||
|
|||
For supporting caches this is an atomic operation. |
|||
|
|||
:param key: the key to increment. |
|||
:param delta: the delta to subtract. |
|||
""" |
|||
self.set(key, (self.get(key) or 0) - delta) |
|||
|
|||
|
|||
class FileSystemCache(BaseCache): |
|||
"""A cache that stores the items on the file system. This cache depends |
|||
on being the only user of the `cache_dir`. Make absolutely sure that |
|||
nobody but this cache stores files there or otherwise the cache will |
|||
randomly delete files therein. |
|||
|
|||
:param cache_dir: the directory where cache files are stored. |
|||
:param threshold: the maximum number of items the cache stores before |
|||
it starts deleting some. |
|||
:param default_timeout: the default timeout that is used if no timeout is |
|||
specified on :meth:`~BaseCache.set`. |
|||
:param mode: the file mode wanted for the cache files, default 0600 |
|||
""" |
|||
|
|||
#: used for temporary files by the FileSystemCache |
|||
_fs_transaction_suffix = '.__wz_cache' |
|||
|
|||
def __init__(self, cache_dir, threshold = 500, default_timeout = 300, mode = 0600): |
|||
BaseCache.__init__(self, default_timeout) |
|||
self._path = cache_dir |
|||
self._threshold = threshold |
|||
self._mode = mode |
|||
if not os.path.exists(self._path): |
|||
os.makedirs(self._path) |
|||
|
|||
def _list_dir(self): |
|||
"""return a list of (fully qualified) cache filenames |
|||
""" |
|||
return [os.path.join(self._path, fn) for fn in os.listdir(self._path) |
|||
if not fn.endswith(self._fs_transaction_suffix)] |
|||
|
|||
def _prune(self): |
|||
entries = self._list_dir() |
|||
if len(entries) > self._threshold: |
|||
now = time() |
|||
for idx, fname in enumerate(entries): |
|||
remove = False |
|||
f = None |
|||
try: |
|||
try: |
|||
f = open(fname, 'rb') |
|||
expires = pickle.load(f) |
|||
remove = expires <= now or idx % 3 == 0 |
|||
finally: |
|||
if f is not None: |
|||
f.close() |
|||
except Exception: |
|||
pass |
|||
if remove: |
|||
try: |
|||
os.remove(fname) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def clear(self): |
|||
for fname in self._list_dir(): |
|||
try: |
|||
os.remove(fname) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def _get_filename(self, key): |
|||
hash = md5(key).hexdigest() |
|||
return os.path.join(self._path, hash) |
|||
|
|||
def get(self, key): |
|||
filename = self._get_filename(key) |
|||
try: |
|||
f = open(filename, 'rb') |
|||
try: |
|||
if pickle.load(f) >= time(): |
|||
return pickle.load(f) |
|||
finally: |
|||
f.close() |
|||
os.remove(filename) |
|||
except Exception: |
|||
return None |
|||
|
|||
def add(self, key, value, timeout = None): |
|||
filename = self._get_filename(key) |
|||
if not os.path.exists(filename): |
|||
self.set(key, value, timeout) |
|||
|
|||
def set(self, key, value, timeout = None): |
|||
if timeout is None: |
|||
timeout = self.default_timeout |
|||
filename = self._get_filename(key) |
|||
self._prune() |
|||
try: |
|||
fd, tmp = tempfile.mkstemp(suffix = self._fs_transaction_suffix, |
|||
dir = self._path) |
|||
f = os.fdopen(fd, 'wb') |
|||
try: |
|||
pickle.dump(int(time() + timeout), f, 1) |
|||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) |
|||
finally: |
|||
f.close() |
|||
rename(tmp, filename) |
|||
os.chmod(filename, self._mode) |
|||
except (IOError, OSError): |
|||
pass |
|||
|
|||
def delete(self, key): |
|||
try: |
|||
os.remove(self._get_filename(key)) |
|||
except (IOError, OSError): |
|||
pass |
@ -1,44 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask |
|||
~~~~~ |
|||
|
|||
A microframework based on Werkzeug. It's extensively documented |
|||
and follows best practice patterns. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
__version__ = '0.9' |
|||
|
|||
# utilities we import from Werkzeug and Jinja2 that are unused |
|||
# in the module but are exported as public interface. |
|||
from werkzeug.exceptions import abort |
|||
from werkzeug.utils import redirect |
|||
from jinja2 import Markup, escape |
|||
|
|||
from .app import Flask, Request, Response |
|||
from .config import Config |
|||
from .helpers import url_for, jsonify, json_available, flash, \ |
|||
send_file, send_from_directory, get_flashed_messages, \ |
|||
get_template_attribute, make_response, safe_join, \ |
|||
stream_with_context |
|||
from .globals import current_app, g, request, session, _request_ctx_stack, \ |
|||
_app_ctx_stack |
|||
from .ctx import has_request_context, has_app_context, \ |
|||
after_this_request |
|||
from .module import Module |
|||
from .blueprints import Blueprint |
|||
from .templating import render_template, render_template_string |
|||
|
|||
# the signals |
|||
from .signals import signals_available, template_rendered, request_started, \ |
|||
request_finished, got_request_exception, request_tearing_down |
|||
|
|||
# only import json if it's available |
|||
if json_available: |
|||
from .helpers import json |
|||
|
|||
# backwards compat, goes away in 1.0 |
|||
from .sessions import SecureCookieSession as Session |
File diff suppressed because it is too large
@ -1,345 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.blueprints |
|||
~~~~~~~~~~~~~~~~ |
|||
|
|||
Blueprints are the recommended way to implement larger or more |
|||
pluggable applications in Flask 0.7 and later. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
from functools import update_wrapper |
|||
|
|||
from .helpers import _PackageBoundObject, _endpoint_from_view_func |
|||
|
|||
|
|||
class BlueprintSetupState(object): |
|||
"""Temporary holder object for registering a blueprint with the |
|||
application. An instance of this class is created by the |
|||
:meth:`~flask.Blueprint.make_setup_state` method and later passed |
|||
to all register callback functions. |
|||
""" |
|||
|
|||
def __init__(self, blueprint, app, options, first_registration): |
|||
#: a reference to the current application |
|||
self.app = app |
|||
|
|||
#: a reference to the blueprint that created this setup state. |
|||
self.blueprint = blueprint |
|||
|
|||
#: a dictionary with all options that were passed to the |
|||
#: :meth:`~flask.Flask.register_blueprint` method. |
|||
self.options = options |
|||
|
|||
#: as blueprints can be registered multiple times with the |
|||
#: application and not everything wants to be registered |
|||
#: multiple times on it, this attribute can be used to figure |
|||
#: out if the blueprint was registered in the past already. |
|||
self.first_registration = first_registration |
|||
|
|||
subdomain = self.options.get('subdomain') |
|||
if subdomain is None: |
|||
subdomain = self.blueprint.subdomain |
|||
|
|||
#: The subdomain that the blueprint should be active for, `None` |
|||
#: otherwise. |
|||
self.subdomain = subdomain |
|||
|
|||
url_prefix = self.options.get('url_prefix') |
|||
if url_prefix is None: |
|||
url_prefix = self.blueprint.url_prefix |
|||
|
|||
#: The prefix that should be used for all URLs defined on the |
|||
#: blueprint. |
|||
self.url_prefix = url_prefix |
|||
|
|||
#: A dictionary with URL defaults that is added to each and every |
|||
#: URL that was defined with the blueprint. |
|||
self.url_defaults = dict(self.blueprint.url_values_defaults) |
|||
self.url_defaults.update(self.options.get('url_defaults', ())) |
|||
|
|||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options): |
|||
"""A helper method to register a rule (and optionally a view function) |
|||
to the application. The endpoint is automatically prefixed with the |
|||
blueprint's name. |
|||
""" |
|||
if self.url_prefix: |
|||
rule = self.url_prefix + rule |
|||
options.setdefault('subdomain', self.subdomain) |
|||
if endpoint is None: |
|||
endpoint = _endpoint_from_view_func(view_func) |
|||
defaults = self.url_defaults |
|||
if 'defaults' in options: |
|||
defaults = dict(defaults, **options.pop('defaults')) |
|||
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), |
|||
view_func, defaults=defaults, **options) |
|||
|
|||
|
|||
class Blueprint(_PackageBoundObject): |
|||
"""Represents a blueprint. A blueprint is an object that records |
|||
functions that will be called with the |
|||
:class:`~flask.blueprint.BlueprintSetupState` later to register functions |
|||
or other things on the main application. See :ref:`blueprints` for more |
|||
information. |
|||
|
|||
.. versionadded:: 0.7 |
|||
""" |
|||
|
|||
warn_on_modifications = False |
|||
_got_registered_once = False |
|||
|
|||
def __init__(self, name, import_name, static_folder=None, |
|||
static_url_path=None, template_folder=None, |
|||
url_prefix=None, subdomain=None, url_defaults=None): |
|||
_PackageBoundObject.__init__(self, import_name, template_folder) |
|||
self.name = name |
|||
self.url_prefix = url_prefix |
|||
self.subdomain = subdomain |
|||
self.static_folder = static_folder |
|||
self.static_url_path = static_url_path |
|||
self.deferred_functions = [] |
|||
self.view_functions = {} |
|||
if url_defaults is None: |
|||
url_defaults = {} |
|||
self.url_values_defaults = url_defaults |
|||
|
|||
def record(self, func): |
|||
"""Registers a function that is called when the blueprint is |
|||
registered on the application. This function is called with the |
|||
state as argument as returned by the :meth:`make_setup_state` |
|||
method. |
|||
""" |
|||
if self._got_registered_once and self.warn_on_modifications: |
|||
from warnings import warn |
|||
warn(Warning('The blueprint was already registered once ' |
|||
'but is getting modified now. These changes ' |
|||
'will not show up.')) |
|||
self.deferred_functions.append(func) |
|||
|
|||
def record_once(self, func): |
|||
"""Works like :meth:`record` but wraps the function in another |
|||
function that will ensure the function is only called once. If the |
|||
blueprint is registered a second time on the application, the |
|||
function passed is not called. |
|||
""" |
|||
def wrapper(state): |
|||
if state.first_registration: |
|||
func(state) |
|||
return self.record(update_wrapper(wrapper, func)) |
|||
|
|||
def make_setup_state(self, app, options, first_registration=False): |
|||
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` |
|||
object that is later passed to the register callback functions. |
|||
Subclasses can override this to return a subclass of the setup state. |
|||
""" |
|||
return BlueprintSetupState(self, app, options, first_registration) |
|||
|
|||
def register(self, app, options, first_registration=False): |
|||
"""Called by :meth:`Flask.register_blueprint` to register a blueprint |
|||
on the application. This can be overridden to customize the register |
|||
behavior. Keyword arguments from |
|||
:func:`~flask.Flask.register_blueprint` are directly forwarded to this |
|||
method in the `options` dictionary. |
|||
""" |
|||
self._got_registered_once = True |
|||
state = self.make_setup_state(app, options, first_registration) |
|||
if self.has_static_folder: |
|||
state.add_url_rule(self.static_url_path + '/<path:filename>', |
|||
view_func=self.send_static_file, |
|||
endpoint='static') |
|||
|
|||
for deferred in self.deferred_functions: |
|||
deferred(state) |
|||
|
|||
def route(self, rule, **options): |
|||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the |
|||
:func:`url_for` function is prefixed with the name of the blueprint. |
|||
""" |
|||
def decorator(f): |
|||
endpoint = options.pop("endpoint", f.__name__) |
|||
self.add_url_rule(rule, endpoint, f, **options) |
|||
return f |
|||
return decorator |
|||
|
|||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options): |
|||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for |
|||
the :func:`url_for` function is prefixed with the name of the blueprint. |
|||
""" |
|||
if endpoint: |
|||
assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" |
|||
self.record(lambda s: |
|||
s.add_url_rule(rule, endpoint, view_func, **options)) |
|||
|
|||
def endpoint(self, endpoint): |
|||
"""Like :meth:`Flask.endpoint` but for a blueprint. This does not |
|||
prefix the endpoint with the blueprint name, this has to be done |
|||
explicitly by the user of this method. If the endpoint is prefixed |
|||
with a `.` it will be registered to the current blueprint, otherwise |
|||
it's an application independent endpoint. |
|||
""" |
|||
def decorator(f): |
|||
def register_endpoint(state): |
|||
state.app.view_functions[endpoint] = f |
|||
self.record_once(register_endpoint) |
|||
return f |
|||
return decorator |
|||
|
|||
def app_template_filter(self, name=None): |
|||
"""Register a custom template filter, available application wide. Like |
|||
:meth:`Flask.template_filter` but for a blueprint. |
|||
|
|||
:param name: the optional name of the filter, otherwise the |
|||
function name will be used. |
|||
""" |
|||
def decorator(f): |
|||
self.add_app_template_filter(f, name=name) |
|||
return f |
|||
return decorator |
|||
|
|||
def add_app_template_filter(self, f, name=None): |
|||
"""Register a custom template filter, available application wide. Like |
|||
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly |
|||
like the :meth:`app_template_filter` decorator. |
|||
|
|||
:param name: the optional name of the filter, otherwise the |
|||
function name will be used. |
|||
""" |
|||
def register_template(state): |
|||
state.app.jinja_env.filters[name or f.__name__] = f |
|||
self.record_once(register_template) |
|||
|
|||
def before_request(self, f): |
|||
"""Like :meth:`Flask.before_request` but for a blueprint. This function |
|||
is only executed before each request that is handled by a function of |
|||
that blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def before_app_request(self, f): |
|||
"""Like :meth:`Flask.before_request`. Such a function is executed |
|||
before each request, even if outside of a blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def before_app_first_request(self, f): |
|||
"""Like :meth:`Flask.before_first_request`. Such a function is |
|||
executed before the first request to the application. |
|||
""" |
|||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) |
|||
return f |
|||
|
|||
def after_request(self, f): |
|||
"""Like :meth:`Flask.after_request` but for a blueprint. This function |
|||
is only executed after each request that is handled by a function of |
|||
that blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.after_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def after_app_request(self, f): |
|||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function |
|||
is executed after each request, even if outside of the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.after_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def teardown_request(self, f): |
|||
"""Like :meth:`Flask.teardown_request` but for a blueprint. This |
|||
function is only executed when tearing down requests handled by a |
|||
function of that blueprint. Teardown request functions are executed |
|||
when the request context is popped, even when no actual request was |
|||
performed. |
|||
""" |
|||
self.record_once(lambda s: s.app.teardown_request_funcs |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def teardown_app_request(self, f): |
|||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a |
|||
function is executed when tearing down each request, even if outside of |
|||
the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.teardown_request_funcs |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def context_processor(self, f): |
|||
"""Like :meth:`Flask.context_processor` but for a blueprint. This |
|||
function is only executed for requests handled by a blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.template_context_processors |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def app_context_processor(self, f): |
|||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a |
|||
function is executed each request, even if outside of the blueprint. |
|||
""" |
|||
self.record_once(lambda s: s.app.template_context_processors |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def app_errorhandler(self, code): |
|||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This |
|||
handler is used for all requests, even if outside of the blueprint. |
|||
""" |
|||
def decorator(f): |
|||
self.record_once(lambda s: s.app.errorhandler(code)(f)) |
|||
return f |
|||
return decorator |
|||
|
|||
def url_value_preprocessor(self, f): |
|||
"""Registers a function as URL value preprocessor for this |
|||
blueprint. It's called before the view functions are called and |
|||
can modify the url values provided. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_value_preprocessors |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def url_defaults(self, f): |
|||
"""Callback function for URL defaults for this blueprint. It's called |
|||
with the endpoint and values and should update the values passed |
|||
in place. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_default_functions |
|||
.setdefault(self.name, []).append(f)) |
|||
return f |
|||
|
|||
def app_url_value_preprocessor(self, f): |
|||
"""Same as :meth:`url_value_preprocessor` but application wide. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_value_preprocessors |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def app_url_defaults(self, f): |
|||
"""Same as :meth:`url_defaults` but application wide. |
|||
""" |
|||
self.record_once(lambda s: s.app.url_default_functions |
|||
.setdefault(None, []).append(f)) |
|||
return f |
|||
|
|||
def errorhandler(self, code_or_exception): |
|||
"""Registers an error handler that becomes active for this blueprint |
|||
only. Please be aware that routing does not happen local to a |
|||
blueprint so an error handler for 404 usually is not handled by |
|||
a blueprint unless it is caused inside a view function. Another |
|||
special case is the 500 internal server error which is always looked |
|||
up from the application. |
|||
|
|||
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator |
|||
of the :class:`~flask.Flask` object. |
|||
""" |
|||
def decorator(f): |
|||
self.record_once(lambda s: s.app._register_error_handler( |
|||
self.name, code_or_exception, f)) |
|||
return f |
|||
return decorator |
@ -1,168 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.config |
|||
~~~~~~~~~~~~ |
|||
|
|||
Implements the configuration related objects. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
from __future__ import with_statement |
|||
|
|||
import imp |
|||
import os |
|||
import errno |
|||
|
|||
from werkzeug.utils import import_string |
|||
|
|||
|
|||
class ConfigAttribute(object): |
|||
"""Makes an attribute forward to the config""" |
|||
|
|||
def __init__(self, name, get_converter=None): |
|||
self.__name__ = name |
|||
self.get_converter = get_converter |
|||
|
|||
def __get__(self, obj, type=None): |
|||
if obj is None: |
|||
return self |
|||
rv = obj.config[self.__name__] |
|||
if self.get_converter is not None: |
|||
rv = self.get_converter(rv) |
|||
return rv |
|||
|
|||
def __set__(self, obj, value): |
|||
obj.config[self.__name__] = value |
|||
|
|||
|
|||
class Config(dict): |
|||
"""Works exactly like a dict but provides ways to fill it from files |
|||
or special dictionaries. There are two common patterns to populate the |
|||
config. |
|||
|
|||
Either you can fill the config from a config file:: |
|||
|
|||
app.config.from_pyfile('yourconfig.cfg') |
|||
|
|||
Or alternatively you can define the configuration options in the |
|||
module that calls :meth:`from_object` or provide an import path to |
|||
a module that should be loaded. It is also possible to tell it to |
|||
use the same module and with that provide the configuration values |
|||
just before the call:: |
|||
|
|||
DEBUG = True |
|||
SECRET_KEY = 'development key' |
|||
app.config.from_object(__name__) |
|||
|
|||
In both cases (loading from any Python file or loading from modules), |
|||
only uppercase keys are added to the config. This makes it possible to use |
|||
lowercase values in the config file for temporary values that are not added |
|||
to the config or to define the config keys in the same file that implements |
|||
the application. |
|||
|
|||
Probably the most interesting way to load configurations is from an |
|||
environment variable pointing to a file:: |
|||
|
|||
app.config.from_envvar('YOURAPPLICATION_SETTINGS') |
|||
|
|||
In this case before launching the application you have to set this |
|||
environment variable to the file you want to use. On Linux and OS X |
|||
use the export statement:: |
|||
|
|||
export YOURAPPLICATION_SETTINGS='/path/to/config/file' |
|||
|
|||
On windows use `set` instead. |
|||
|
|||
:param root_path: path to which files are read relative from. When the |
|||
config object is created by the application, this is |
|||
the application's :attr:`~flask.Flask.root_path`. |
|||
:param defaults: an optional dictionary of default values |
|||
""" |
|||
|
|||
def __init__(self, root_path, defaults=None): |
|||
dict.__init__(self, defaults or {}) |
|||
self.root_path = root_path |
|||
|
|||
def from_envvar(self, variable_name, silent=False): |
|||
"""Loads a configuration from an environment variable pointing to |
|||
a configuration file. This is basically just a shortcut with nicer |
|||
error messages for this line of code:: |
|||
|
|||
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) |
|||
|
|||
:param variable_name: name of the environment variable |
|||
:param silent: set to `True` if you want silent failure for missing |
|||
files. |
|||
:return: bool. `True` if able to load config, `False` otherwise. |
|||
""" |
|||
rv = os.environ.get(variable_name) |
|||
if not rv: |
|||
if silent: |
|||
return False |
|||
raise RuntimeError('The environment variable %r is not set ' |
|||
'and as such configuration could not be ' |
|||
'loaded. Set this variable and make it ' |
|||
'point to a configuration file' % |
|||
variable_name) |
|||
return self.from_pyfile(rv, silent=silent) |
|||
|
|||
def from_pyfile(self, filename, silent=False): |
|||
"""Updates the values in the config from a Python file. This function |
|||
behaves as if the file was imported as module with the |
|||
:meth:`from_object` function. |
|||
|
|||
:param filename: the filename of the config. This can either be an |
|||
absolute filename or a filename relative to the |
|||
root path. |
|||
:param silent: set to `True` if you want silent failure for missing |
|||
files. |
|||
|
|||
.. versionadded:: 0.7 |
|||
`silent` parameter. |
|||
""" |
|||
filename = os.path.join(self.root_path, filename) |
|||
d = imp.new_module('config') |
|||
d.__file__ = filename |
|||
try: |
|||
execfile(filename, d.__dict__) |
|||
except IOError, e: |
|||
if silent and e.errno in (errno.ENOENT, errno.EISDIR): |
|||
return False |
|||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror |
|||
raise |
|||
self.from_object(d) |
|||
return True |
|||
|
|||
def from_object(self, obj): |
|||
"""Updates the values from the given object. An object can be of one |
|||
of the following two types: |
|||
|
|||
- a string: in this case the object with that name will be imported |
|||
- an actual object reference: that object is used directly |
|||
|
|||
Objects are usually either modules or classes. |
|||
|
|||
Just the uppercase variables in that object are stored in the config. |
|||
Example usage:: |
|||
|
|||
app.config.from_object('yourapplication.default_config') |
|||
from yourapplication import default_config |
|||
app.config.from_object(default_config) |
|||
|
|||
You should not use this function to load the actual configuration but |
|||
rather configuration defaults. The actual config should be loaded |
|||
with :meth:`from_pyfile` and ideally from a location not within the |
|||
package because the package might be installed system wide. |
|||
|
|||
:param obj: an import name or object |
|||
""" |
|||
if isinstance(obj, basestring): |
|||
obj = import_string(obj) |
|||
for key in dir(obj): |
|||
if key.isupper(): |
|||
self[key] = getattr(obj, key) |
|||
|
|||
def __repr__(self): |
|||
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) |
@ -1,295 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
flask.ctx |
|||
~~~~~~~~~ |
|||
|
|||
Implements the objects required to keep the context. |
|||
|
|||
:copyright: (c) 2011 by Armin Ronacher. |
|||
:license: BSD, see LICENSE for more details. |
|||
""" |
|||
|
|||
import sys |
|||
|
|||
from werkzeug.exceptions import HTTPException |
|||
|
|||
from .globals import _request_ctx_stack, _app_ctx_stack |
|||
from .module import blueprint_is_module |
|||
|
|||
|
|||
class _RequestGlobals(object): |
|||
"""A plain object.""" |
|||
pass |
|||
|
|||
|
|||
def after_this_request(f): |
|||
"""Executes a function after this request. This is useful to modify |
|||
response objects. The function is passed the response object and has |
|||
to return the same or a new one. |
|||
|
|||
Example:: |
|||
|
|||
@app.route('/') |
|||
def index(): |
|||
@after_this_request |
|||
def add_header(response): |
|||
response.headers['X-Foo'] = 'Parachute' |
|||
return response |
|||
return 'Hello World!' |
|||
|
|||
This is more useful if a function other than the view function wants to |
|||
modify a response. For instance think of a decorator that wants to add |
|||
some headers without converting the return value into a response object. |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
_request_ctx_stack.top._after_request_functions.append(f) |
|||
return f |
|||
|
|||
|
|||
def has_request_context(): |
|||
"""If you have code that wants to test if a request context is there or |
|||
not this function can be used. For instance, you may want to take advantage |
|||
of request information if the request object is available, but fail |
|||
silently if it is unavailable. |
|||
|
|||
:: |
|||
|
|||
class User(db.Model): |
|||
|
|||
def __init__(self, username, remote_addr=None): |
|||
self.username = username |
|||
if remote_addr is None and has_request_context(): |
|||
remote_addr = request.remote_addr |
|||
self.remote_addr = remote_addr |
|||
|
|||
Alternatively you can also just test any of the context bound objects |
|||
(such as :class:`request` or :class:`g` for truthness):: |
|||
|
|||
class User(db.Model): |
|||
|
|||
def __init__(self, username, remote_addr=None): |
|||
self.username = username |
|||
if remote_addr is None and request: |
|||
remote_addr = request.remote_addr |
|||
self.remote_addr = remote_addr |
|||
|
|||
.. versionadded:: 0.7 |
|||
""" |
|||
return _request_ctx_stack.top is not None |
|||
|
|||
|
|||
def has_app_context(): |
|||
"""Works like :func:`has_request_context` but for the application |
|||
context. You can also just do a boolean check on the |
|||
:data:`current_app` object instead. |
|||
|
|||
.. versionadded:: 0.9 |
|||
""" |
|||
return _app_ctx_stack.top is not None |
|||
|
|||
|
|||
class AppContext(object): |
|||
"""The application context binds an application object implicitly |
|||
to the current thread or greenlet, similar to how the |
|||
:class:`RequestContext` binds request information. The application |
|||
context is also implicitly created if a request context is created |
|||
but the application is not on top of the individual application |
|||
context. |
|||
""" |
|||
|
|||
def __init__(self, app): |
|||
self.app = app |
|||
self.url_adapter = app.create_url_adapter(None) |
|||
|
|||
# Like request context, app contexts can be pushed multiple times |
|||
# but there a basic "refcount" is enough to track them. |
|||
self._refcnt = 0 |
|||
|
|||
def push(self): |
|||
"""Binds the app context to the current context.""" |
|||
self._refcnt += 1 |
|||
_app_ctx_stack.push(self) |
|||
|
|||
def pop(self, exc=None): |
|||
"""Pops the app context.""" |
|||
self._refcnt -= 1 |
|||
if self._refcnt <= 0: |
|||
if exc is None: |
|||
exc = sys.exc_info()[1] |
|||
self.app.do_teardown_appcontext(exc) |
|||
rv = _app_ctx_stack.pop() |
|||
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ |
|||
% (rv, self) |
|||
|
|||
def __enter__(self): |
|||
self.push() |
|||
return self |
|||
|
|||
def __exit__(self, exc_type, exc_value, tb): |
|||
self.pop(exc_value) |
|||
|
|||
|
|||
class RequestContext(object): |
|||
"""The request context contains all request relevant information. It is |
|||
created at the beginning of the request and pushed to the |
|||
`_request_ctx_stack` and removed at the end of it. It will create the |
|||
URL adapter and request object for the WSGI environment provided. |
|||
|
|||
Do not attempt to use this class directly, instead use |
|||
:meth:`~flask.Flask.test_request_context` and |
|||
:meth:`~flask.Flask.request_context` to create this object. |
|||
|
|||
When the request context is popped, it will evaluate all the |
|||
functions registered on the application for teardown execution |
|||
(:meth:`~flask.Flask.teardown_request`). |
|||
|
|||
The request context is automatically popped at the end of the request |
|||
for you. In debug mode the request context is kept around if |
|||
exceptions happen so that interactive debuggers have a chance to |
|||
introspect the data. With 0.4 this can also be forced for requests |
|||
that did not fail and outside of `DEBUG` mode. By setting |
|||
``'flask._preserve_context'`` to `True` on the WSGI environment the |
|||
context will not pop itself at the end of the request. This is used by |
|||
the :meth:`~flask.Flask.test_client` for example to implement the |
|||
deferred cleanup functionality. |
|||
|
|||
You might find this helpful for unittests where you need the |
|||
information from the context local around for a little longer. Make |
|||
sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in |
|||
that situation, otherwise your unittests will leak memory. |
|||
""" |
|||
|
|||
def __init__(self, app, environ): |
|||
self.app = app |
|||
self.request = app.request_class(environ) |
|||
self.url_adapter = app.create_url_adapter(self.request) |
|||
self.g = app.request_globals_class() |
|||
self.flashes = None |
|||
self.session = None |
|||
|
|||
# Request contexts can be pushed multiple times and interleaved with |
|||
# other request contexts. Now only if the last level is popped we |
|||
# get rid of them. Additionally if an application context is missing |
|||
# one is created implicitly so for each level we add this information |
|||
self._implicit_app_ctx_stack = [] |
|||
|
|||
# indicator if the context was preserved. Next time another context |
|||
# is pushed the preserved context is popped. |
|||
self.preserved = False |
|||
|
|||
# Functions that should be executed after the request on the response |
|||
# object. These will be called before the regular "after_request" |
|||
# functions. |
|||
self._after_request_functions = [] |
|||
|
|||
self.match_request() |
|||
|
|||
# XXX: Support for deprecated functionality. This is going away with |
|||
# Flask 1.0 |
|||
blueprint = self.request.blueprint |
|||
if blueprint is not None: |
|||
# better safe than sorry, we don't want to break code that |
|||
# already worked |
|||
bp = app.blueprints.get(blueprint) |
|||
if bp is not None and blueprint_is_module(bp): |
|||
self.request._is_old_module = True |
|||
|
|||
def match_request(self): |
|||
"""Can be overridden by a subclass to hook into the matching |
|||
of the request. |
|||
""" |
|||
try: |
|||
url_rule, self.request.view_args = \ |
|||
self.url_adapter.match(return_rule=True) |
|||
self.request.url_rule = url_rule |
|||
except HTTPException, e: |
|||
self.request.routing_exception = e |
|||
|
|||
def push(self): |
|||
"""Binds the request context to the current context.""" |
|||
# If an exception ocurrs in debug mode or if context preservation is |
|||
# activated under exception situations exactly one context stays |
|||
# on the stack. The rationale is that you want to access that |
|||
# information under debug situations. However if someone forgets to |
|||
# pop that context again we want to make sure that on the next push |
|||
# it's invalidated otherwise we run at risk that something leaks |
|||
# memory. This is usually only a problem in testsuite since this |
|||
# functionality is not active in production environments. |
|||
top = _request_ctx_stack.top |
|||
if top is not None and top.preserved: |
|||
top.pop() |
|||
|
|||
# Before we push the request context we have to ensure that there |
|||
# is an application context. |
|||
app_ctx = _app_ctx_stack.top |
|||
if app_ctx is None or app_ctx.app != self.app: |
|||
app_ctx = self.app.app_context() |
|||
app_ctx.push() |
|||
self._implicit_app_ctx_stack.append(app_ctx) |
|||
else: |
|||
self._implicit_app_ctx_stack.append(None) |
|||
|
|||
_request_ctx_stack.push(self) |
|||
|
|||
# Open the session at the moment that the request context is |
|||
# available. This allows a custom open_session method to use the |
|||
# request context (e.g. flask-sqlalchemy). |
|||
self.session = self.app.open_session(self.request) |
|||
if self.session is None: |
|||
self.session = self.app.make_null_session() |
|||
|
|||
def pop(self, exc=None): |
|||
"""Pops the request context and unbinds it by doing that. This will |
|||
also trigger the execution of functions registered by the |
|||
:meth:`~flask.Flask.teardown_request` decorator. |
|||
|
|||
.. versionchanged:: 0.9 |
|||
Added the `exc` argument. |
|||
""" |
|||
app_ctx = self._implicit_app_ctx_stack.pop() |
|||
|
|||
clear_request = False |
|||
if not self._implicit_app_ctx_stack: |
|||
self.preserved = False |
|||
if exc is None: |
|||
exc = sys.exc_info()[1] |
|||
self.app.do_teardown_request(exc) |
|||
clear_request = True |
|||
|
|||
rv = _request_ctx_stack.pop() |
|||
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ |
|||
% (rv, self) |
|||
|
|||
# get rid of circular dependencies at the end of the request |
|||
# so that we don't require the GC to be active. |
|||
if clear_request: |
|||
rv.request.environ['werkzeug.request'] = None |
|||
|
|||
# Get rid of the app as well if necessary. |
|||
if app_ctx is not None: |
|||
app_ctx.pop(exc) |
|||
|
|||
def __enter__(self): |
|||
self.push() |
|||
return self |
|||
|
|||
def __exit__(self, exc_type, exc_value, tb): |
|||
# do not pop the request stack if we are in debug mode and an |
|||
# exception happened. This will allow the debugger to still |
|||
# access the request object in the interactive shell. Furthermore |
|||
# the context can be force kept alive for the test client. |
|||
# See flask.testing for how this works. |
|||
if self.request.environ.get('flask._preserve_context') or \ |
|||
(tb is not None and self.app.preserve_context_on_exception): |
|||
self.preserved = True |
|||
else: |
|||
self.pop(exc_value) |
|||
|
|||
def __repr__(self): |
|||
return '<%s \'%s\' [%s] of %s>' % ( |
|||
self.__class__.__name__, |
|||
self.request.url, |
|||
self.request.method, |
|||
self.app.name |
|||
) |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue