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.auth import requires_auth |
||||
from couchpotato.core.event import fireEvent |
from couchpotato.core.event import fireEvent |
||||
from couchpotato.core.helpers.request import getParams, jsonified |
|
||||
from couchpotato.core.helpers.variable import md5 |
from couchpotato.core.helpers.variable import md5 |
||||
from couchpotato.core.logger import CPLog |
from couchpotato.core.logger import CPLog |
||||
from couchpotato.environment import Env |
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.engine import create_engine |
||||
from sqlalchemy.orm import scoped_session |
from sqlalchemy.orm import scoped_session |
||||
from sqlalchemy.orm.session import sessionmaker |
from sqlalchemy.orm.session import sessionmaker |
||||
from werkzeug.utils import redirect |
from tornado import template |
||||
|
from tornado.web import RequestHandler |
||||
import os |
import os |
||||
import time |
import time |
||||
|
|
||||
log = CPLog(__name__) |
log = CPLog(__name__) |
||||
|
|
||||
app = Flask(__name__, static_folder = 'nope') |
views = {} |
||||
web = Blueprint('web', __name__) |
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): |
def get_session(engine = None): |
||||
return Env.getSession(engine) |
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 view |
||||
@web.route('/') |
|
||||
@requires_auth |
|
||||
def index(): |
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 """ |
# API docs |
||||
@web.route('docs/') |
|
||||
@requires_auth |
|
||||
def apiDocs(): |
def apiDocs(): |
||||
from couchpotato import app |
|
||||
routes = [] |
routes = [] |
||||
for route, x in sorted(app.view_functions.iteritems()): |
|
||||
if route[0:4] == 'api.': |
for route in api.iterkeys(): |
||||
routes += [route[4:].replace('::', '.')] |
routes.append(route) |
||||
|
|
||||
if api_docs.get(''): |
if api_docs.get(''): |
||||
del api_docs[''] |
del api_docs[''] |
||||
del api_docs_missing[''] |
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/') |
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) |
||||
def getApiKey(): |
|
||||
|
|
||||
api = None |
addView('docs', apiDocs) |
||||
params = getParams() |
|
||||
username = Env.setting('username') |
|
||||
password = Env.setting('password') |
|
||||
|
|
||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): |
# Make non basic auth option to get api key |
||||
api = Env.setting('api_key') |
class KeyHandler(RequestHandler): |
||||
|
def get(self, *args, **kwargs): |
||||
|
api = None |
||||
|
username = Env.setting('username') |
||||
|
password = Env.setting('password') |
||||
|
|
||||
return jsonified({ |
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): |
||||
'success': api is not None, |
api = Env.setting('api_key') |
||||
'api_key': api |
|
||||
}) |
|
||||
|
|
||||
@app.errorhandler(404) |
self.write({ |
||||
def page_not_found(error): |
'success': api is not None, |
||||
index_url = url_for('web.index') |
'api_key': api |
||||
url = request.path[len(index_url):] |
}) |
||||
|
|
||||
|
def page_not_found(rh): |
||||
|
index_url = Env.get('web_base') |
||||
|
url = rh.request.uri[len(index_url):] |
||||
|
|
||||
if url[:3] != 'api': |
if url[:3] != 'api': |
||||
if request.path != '/': |
r = index_url + '#' + url.lstrip('/') |
||||
r = request.url.replace(request.path, index_url + '#' + url) |
rh.redirect(r) |
||||
else: |
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url) |
|
||||
return redirect(r) |
|
||||
else: |
else: |
||||
if not Env.get('dev'): |
if not Env.get('dev'): |
||||
time.sleep(0.1) |
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.core.helpers.variable import md5 |
||||
from couchpotato.environment import Env |
from couchpotato.environment import Env |
||||
from flask import request, Response |
import base64 |
||||
from functools import wraps |
|
||||
|
|
||||
def check_auth(username, password): |
def check_auth(username, password): |
||||
return username == Env.setting('username') and password == Env.setting('password') |
return username == Env.setting('username') and password == Env.setting('password') |
||||
|
|
||||
def authenticate(): |
def requires_auth(handler_class): |
||||
return Response( |
|
||||
'This is not the page you are looking for. *waves hand*', 401, |
|
||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'} |
|
||||
) |
|
||||
|
|
||||
def requires_auth(f): |
def wrap_execute(handler_execute): |
||||
|
|
||||
@wraps(f) |
def require_basic_auth(handler, kwargs): |
||||
def decorated(*args, **kwargs): |
if Env.setting('username') and Env.setting('password'): |
||||
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() |
|
||||
|
|
||||
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.api import addApiView |
||||
from couchpotato.core.event import fireEvent |
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.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): |
class Suggestion(Plugin): |
||||
|
|
||||
def __init__(self): |
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) |
movies = splitString(kwargs.get('movies', '')) |
||||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True) |
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, |
'success': True, |
||||
'empty': len(movies) == 0, |
'count': len(suggestions), |
||||
'total': total_movies, |
'suggestions': suggestions[:limit] |
||||
'movies': movies, |
} |
||||
}) |
|
||||
|
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