rewrite using NIH
This commit is contained in:
parent
440787ff67
commit
a4514e1f91
148
isso/__init__.py
148
isso/__init__.py
@ -20,51 +20,29 @@
|
||||
#
|
||||
# Isso – a lightweight Disqus alternative
|
||||
|
||||
__version__ = '0.2'
|
||||
__version__ = '0.3'
|
||||
|
||||
import sys; reload(sys)
|
||||
sys.setdefaultencoding('utf-8') # we only support UTF-8 and python 2.X :-)
|
||||
|
||||
import io
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from os.path import join, dirname
|
||||
from os.path import dirname
|
||||
from optparse import OptionParser, make_option, SUPPRESS_HELP
|
||||
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
from werkzeug.wsgi import SharedDataMiddleware
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.serving import run_simple
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.exceptions import HTTPException, NotFound, InternalServerError
|
||||
|
||||
from isso import admin, comment, db, migrate
|
||||
from isso.utils import determine, import_object, RegexConverter, IssoEncoder
|
||||
from isso import admin, comment, db, migrate, wsgi
|
||||
from isso.utils import determine, import_object, IssoEncoder
|
||||
|
||||
# override default json :func:`dumps`.
|
||||
_dumps = json.dumps
|
||||
setattr(json, 'dumps', lambda obj, **kw: _dumps(obj, cls=IssoEncoder, **kw))
|
||||
|
||||
# yep. lazy.
|
||||
url = lambda path, endpoint, methods: Rule(path, endpoint=endpoint, methods=methods)
|
||||
|
||||
url_map = Map([
|
||||
# moderation panel
|
||||
url('/', 'admin.login', ['GET', 'POST']),
|
||||
url('/admin/', 'admin.index', ['GET', 'POST']),
|
||||
|
||||
# comment API, note that the client side quotes the URL, but this is
|
||||
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
|
||||
url('/1.0/<re(".+"):path>/', 'comment.get', ['GET']),
|
||||
url('/1.0/<re(".+"):path>/new', 'comment.create', ['POST']),
|
||||
url('/1.0/<re(".+"):path>/<int:id>', 'comment.get', ['GET']),
|
||||
url('/1.0/<re(".+"):path>/<int:id>', 'comment.modify', ['PUT', 'DELETE']),
|
||||
url('/1.0/<re(".+"):path>/<int:id>/approve', 'comment.approve', ['PUT'])
|
||||
], converters={'re': RegexConverter})
|
||||
|
||||
|
||||
class Isso:
|
||||
class Isso(object):
|
||||
|
||||
PRODUCTION = True
|
||||
SECRET = 'secret'
|
||||
@ -75,6 +53,12 @@ class Isso:
|
||||
HOST = 'http://localhost:8000/'
|
||||
MAX_AGE = 15 * 60
|
||||
|
||||
HTTP_STATUS_CODES = {
|
||||
200: 'Ok', 201: 'Created', 202: 'Accepted', 301: 'Moved Permanently',
|
||||
400: 'Bad Request', 404: 'Not Found', 403: 'Forbidden',
|
||||
500: 'Internal Server Error',
|
||||
}
|
||||
|
||||
def __init__(self, conf):
|
||||
|
||||
self.__dict__.update(dict((k, v) for k, v in conf.iteritems() if k.isupper()))
|
||||
@ -85,6 +69,22 @@ class Isso:
|
||||
self.db = db.SQLite(self)
|
||||
|
||||
self.markup = import_object(conf.get('MARKUP', 'isso.markup.Markdown'))(conf)
|
||||
self.adapter = map(
|
||||
lambda r: (wsgi.Rule(r[0]), r[1], r[2] if isinstance(r[2], list) else [r[2]]), [
|
||||
|
||||
# moderation panel
|
||||
('/', admin.login, ['GET', 'POST']),
|
||||
('/admin/', admin.index, ['GET', 'POST']),
|
||||
|
||||
# comment API, note that the client side quotes the URL, but this is
|
||||
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
|
||||
('/1.0/<(.+?):path>/new', comment.create, 'POST'),
|
||||
('/1.0/<(.+?):path>/<(int):id>', comment.get, 'GET'),
|
||||
('/1.0/<(.+?):path>/<(int):id>', comment.modify, ['PUT', 'DELETE']),
|
||||
('/1.0/<(.+?):path>/<(int):id>/approve', comment.approve, 'PUT'),
|
||||
|
||||
('/1.0/<(.+?):path>', comment.get, 'GET'),
|
||||
])
|
||||
|
||||
def sign(self, obj):
|
||||
return self.signer.dumps(obj)
|
||||
@ -92,27 +92,71 @@ class Isso:
|
||||
def unsign(self, obj):
|
||||
return self.signer.loads(obj, max_age=self.MAX_AGE)
|
||||
|
||||
def dispatch(self, request, start_response):
|
||||
adapter = url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
module, function = endpoint.split('.', 1)
|
||||
handler = getattr(globals()[module], function)
|
||||
return handler(self, request.environ, request, **values)
|
||||
except NotFound, e:
|
||||
return Response('Not Found', 404)
|
||||
except HTTPException, e:
|
||||
return e
|
||||
except InternalServerError, e:
|
||||
return Response(e, 500)
|
||||
def status(self, code):
|
||||
return '%i %s' % (code, self.HTTP_STATUS_CODES[code])
|
||||
|
||||
def wsgi_app(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
response = self.dispatch(request, start_response)
|
||||
return response(environ, start_response)
|
||||
def dispatch(self, path, method):
|
||||
|
||||
for rule, handler, methods in self.adapter:
|
||||
if isinstance(methods, basestring):
|
||||
methods = [methods, ]
|
||||
if method not in methods:
|
||||
continue
|
||||
m = rule.match(path)
|
||||
if m is not None:
|
||||
return handler, m
|
||||
else:
|
||||
return (lambda app, environ, request, **kw: (404, 'Not Found', {}), {})
|
||||
|
||||
def wsgi(self, environ, start_response):
|
||||
|
||||
try:
|
||||
request = wsgi.Request(environ)
|
||||
handler, kwargs = self.dispatch(environ['PATH_INFO'], request.method)
|
||||
code, body, headers = handler(self, environ, request, **kwargs)
|
||||
|
||||
if code == 404:
|
||||
try:
|
||||
code, body, headers = wsgi.sendfile(environ['PATH_INFO'], dirname(__file__))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
if request == 'HEAD':
|
||||
body = ''
|
||||
|
||||
start_response(self.status(code), headers.items())
|
||||
return body
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except Exception:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
headers = [('Content-Type', 'text/html; charset=utf-8')]
|
||||
start_response(self.status(500), headers)
|
||||
return '<h1>' + self.status(500) + '</h1>'
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgi_app(environ, start_response)
|
||||
return self.wsgi(environ, start_response)
|
||||
|
||||
|
||||
class ReverseProxied(object):
|
||||
|
||||
def __init__(self, app, prefix=None):
|
||||
self.app = app
|
||||
self.prefix = prefix if prefix is not None else ''
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
script_name = environ.get('HTTP_X_SCRIPT_NAME', self.prefix)
|
||||
if script_name:
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
path_info = environ['PATH_INFO']
|
||||
if path_info.startswith(script_name):
|
||||
environ['PATH_INFO'] = path_info[len(script_name):]
|
||||
|
||||
scheme = environ.get('HTTP_X_SCHEME', '')
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
def main():
|
||||
@ -122,7 +166,7 @@ def main():
|
||||
make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db",
|
||||
help="use SQLite3 database"),
|
||||
make_option("--port", dest="port", default=8000, help="webserver port"),
|
||||
make_option("--test", dest="production", action="store_false", default=True,
|
||||
make_option("--debug", dest="production", action="store_false", default=True,
|
||||
help=SUPPRESS_HELP),
|
||||
]
|
||||
|
||||
@ -133,7 +177,7 @@ def main():
|
||||
print 'isso', __version__
|
||||
sys.exit(0)
|
||||
|
||||
app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production})
|
||||
app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production, 'MODERATION': False})
|
||||
|
||||
if len(args) > 0 and args[0] == 'import':
|
||||
if len(args) < 2:
|
||||
@ -142,8 +186,8 @@ def main():
|
||||
|
||||
with io.open(args[1], encoding='utf-8') as fp:
|
||||
migrate.disqus(app.db, fp.read())
|
||||
|
||||
else:
|
||||
app = SharedDataMiddleware(app, {
|
||||
'/static': join(dirname(__file__), 'static'),
|
||||
'/js': join(dirname(__file__), 'js')})
|
||||
run_simple('127.0.0.1', 8000, app, use_reloader=True)
|
||||
from wsgiref.simple_server import make_server
|
||||
httpd = make_server('127.0.0.1', 8080, app)
|
||||
httpd.serve_forever()
|
||||
|
@ -3,12 +3,12 @@
|
||||
# Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved.
|
||||
# License: BSD Style, 2 clauses. see isso/__init__.py
|
||||
|
||||
from werkzeug.utils import redirect
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from itsdangerous import SignatureExpired, BadSignature
|
||||
|
||||
from isso.wsgi import setcookie
|
||||
|
||||
|
||||
mako = TemplateLookup(directories=['isso/templates'], input_encoding='utf-8')
|
||||
render = lambda template, **context: mako.get_template(template).render_unicode(**context)
|
||||
|
||||
@ -16,12 +16,14 @@ render = lambda template, **context: mako.get_template(template).render_unicode(
|
||||
def login(app, environ, request):
|
||||
|
||||
if request.method == 'POST':
|
||||
if request.form.get('secret') == app.SECRET:
|
||||
rdr = redirect('/admin/', 301)
|
||||
rdr.set_cookie('session-admin', app.signer.dumps('*'), max_age=app.MAX_AGE)
|
||||
return rdr
|
||||
if request.form.getfirst('secret') == app.SECRET:
|
||||
return 301, '', {
|
||||
'Location': '/admin/',
|
||||
'Set-Cookie': setcookie('session-admin', app.signer.dumps('*'),
|
||||
max_age=app.MAX_AGE, path='/')
|
||||
}
|
||||
|
||||
return Response(render('login.mako'), content_type='text/html')
|
||||
return 200, render('login.mako').encode('utf-8'), {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
def index(app, environ, request):
|
||||
@ -29,7 +31,7 @@ def index(app, environ, request):
|
||||
try:
|
||||
app.unsign(request.cookies.get('session-admin', ''))
|
||||
except (SignatureExpired, BadSignature):
|
||||
return redirect('/')
|
||||
return 301, '', {'Location': '/'}
|
||||
|
||||
ctx = {'app': app, 'request': request}
|
||||
return Response(render('admin.mako', **ctx), content_type='text/html')
|
||||
return 200, render('admin.mako', **ctx).encode('utf-8'), {'Content-Type': 'text/html'}
|
||||
|
@ -6,50 +6,48 @@
|
||||
import cgi
|
||||
import urllib
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.exceptions import abort
|
||||
|
||||
from itsdangerous import SignatureExpired, BadSignature
|
||||
|
||||
from isso import json, models, utils
|
||||
from isso import json, models, utils, wsgi
|
||||
|
||||
|
||||
def create(app, environ, request, path):
|
||||
|
||||
if app.PRODUCTION and not utils.urlexists(app.HOST, '/' + path):
|
||||
return abort(404)
|
||||
return 400, 'URL does not exist', {}
|
||||
|
||||
try:
|
||||
comment = models.Comment.fromjson(request.data)
|
||||
except ValueError:
|
||||
return abort(400)
|
||||
except ValueError as e:
|
||||
return 400, unicode(e), {}
|
||||
|
||||
for attr in 'author', 'email', 'website':
|
||||
if getattr(comment, attr) is not None:
|
||||
try:
|
||||
setattr(comment, attr, cgi.escape(getattr(comment, attr)))
|
||||
except AttributeError:
|
||||
abort(400)
|
||||
return 400, '', {}
|
||||
|
||||
try:
|
||||
rv = app.db.add(path, comment)
|
||||
except ValueError:
|
||||
return abort(400)
|
||||
return 400, '', {}
|
||||
|
||||
md5 = rv.md5
|
||||
rv.text = app.markup.convert(rv.text)
|
||||
|
||||
response = Response(json.dumps(rv), 202 if rv.pending else 201, content_type='application/json')
|
||||
response.set_cookie('session-%s-%s' % (urllib.quote(path, ''), rv.id),
|
||||
app.signer.dumps([path, rv.id, md5]), max_age=app.MAX_AGE)
|
||||
return response
|
||||
return 202 if rv.pending else 201, json.dumps(rv), {
|
||||
'Content-Type': 'application/json',
|
||||
'Set-Cookie': wsgi.setcookie('%s-%s' % (path, rv.id),
|
||||
app.sign([path, rv.id, md5]), max_age=app.MAX_AGE, path='/')
|
||||
}
|
||||
|
||||
|
||||
def get(app, environ, request, path, id=None):
|
||||
|
||||
rv = list(app.db.retrieve(path)) if id is None else app.db.get(path, id)
|
||||
if not rv:
|
||||
abort(404)
|
||||
return 400, '', {}
|
||||
|
||||
if request.args.get('plain', '0') == '0':
|
||||
if isinstance(rv, list):
|
||||
@ -58,46 +56,47 @@ def get(app, environ, request, path, id=None):
|
||||
else:
|
||||
rv.text = app.markup.convert(rv.text)
|
||||
|
||||
return Response(json.dumps(rv), 200, content_type='application/json')
|
||||
return 200, json.dumps(rv), {'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def modify(app, environ, request, path, id):
|
||||
|
||||
try:
|
||||
rv = app.unsign(request.cookies.get('session-%s-%s' % (urllib.quote(path, ''), id), ''))
|
||||
except (SignatureExpired, BadSignature):
|
||||
rv = app.unsign(request.cookies.get('%s-%s' % (urllib.quote(path, ''), id), ''))
|
||||
except (SignatureExpired, BadSignature) as e:
|
||||
try:
|
||||
rv = app.unsign(request.cookies.get('session-admin', ''))
|
||||
rv = app.unsign(request.cookies.get('admin', ''))
|
||||
except (SignatureExpired, BadSignature):
|
||||
return abort(403)
|
||||
return 403, '', {}
|
||||
|
||||
# verify checksum, mallory might skip cookie deletion when he deletes a comment
|
||||
if not (rv == '*' or rv[0:2] == [path, id] or app.db.get(path, id).md5 != rv[2]):
|
||||
abort(403)
|
||||
return 403, '', {}
|
||||
|
||||
if request.method == 'PUT':
|
||||
try:
|
||||
rv = app.db.update(path, id, models.Comment.fromjson(request.data))
|
||||
rv.text = app.markup.convert(rv.text)
|
||||
return Response(json.dumps(rv), 200, content_type='application/json')
|
||||
return 200, json.dumps(rv), {'Content-Type': 'application/json'}
|
||||
except ValueError as e:
|
||||
return Response(unicode(e), 400)
|
||||
return 400, unicode(e), {}
|
||||
|
||||
if request.method == 'DELETE':
|
||||
rv = app.db.delete(path, id)
|
||||
|
||||
response = Response(json.dumps(rv), 200, content_type='application/json')
|
||||
response.delete_cookie('session-%s-%s' % (urllib.quote(path, ''), id))
|
||||
return response
|
||||
return 200, json.dumps(rv), {
|
||||
'Content-Type': 'application/json',
|
||||
'Set-Cookie': wsgi.setcookie(path + '-' + str(id), 'deleted', max_age=0, path='/')
|
||||
}
|
||||
|
||||
|
||||
def approve(app, environ, request, path, id):
|
||||
|
||||
try:
|
||||
if app.unsign(request.cookies.get('session-admin', '')) != '*':
|
||||
abort(403)
|
||||
if app.unsign(request.cookies.get('admin', '')) != '*':
|
||||
return 403, '', {}
|
||||
except (SignatureExpired, BadSignature):
|
||||
abort(403)
|
||||
return 403, '', {}
|
||||
|
||||
app.db.activate(path, id)
|
||||
return Response(json.dumps(app.db.get(path, id)), 200, content_type='application/json')
|
||||
return 200, json.dumps(app.db.get(path, id)), {'Content-Type': 'application/json'}
|
||||
|
@ -16,6 +16,9 @@
|
||||
* zfill(argument, i): zero fill `argument` with `i` zeros
|
||||
*/
|
||||
|
||||
// var prefix = "/comments";
|
||||
var prefix = "";
|
||||
|
||||
|
||||
function read(cookie){
|
||||
return(document.cookie.match('(^|; )' + cookie + '=([^;]*)') || 0)[2]
|
||||
@ -60,7 +63,7 @@ function create(data, func) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax('POST', '/1.0/' + encodeURIComponent(window.location.pathname) + '/new',
|
||||
$.ajax('POST', prefix + '/1.0/' + encodeURIComponent(window.location.pathname) + '/new',
|
||||
JSON.stringify(data), {'Content-Type': 'application/json'}).then(func);
|
||||
};
|
||||
|
||||
@ -70,7 +73,7 @@ function modify(id, data, func) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax('PUT', '/1.0/' + encodeURIComponent(window.location.pathname) + '/' + id,
|
||||
$.ajax('PUT', prefix + '/1.0/' + encodeURIComponent(window.location.pathname) + '/' + id,
|
||||
JSON.stringify(data), {'Content-Type': 'application/json'}).then(func)
|
||||
};
|
||||
|
||||
@ -169,14 +172,15 @@ function insert(post) {
|
||||
.append('<span class="note">Kommentar muss noch freigeschaltet werden</span>');
|
||||
}
|
||||
|
||||
if (read('session-' + path + '-' + post['id'])) {
|
||||
if (read(path + '-' + post['id'])) {
|
||||
$('#isso_' + post['id'] + '> footer > a:first-child')
|
||||
.after('<a class="delete" href="#">Löschen</a>')
|
||||
.after('<a class="edit" href="#">Bearbeiten</a>');
|
||||
|
||||
// DELETE
|
||||
$('#isso_' + post['id'] + ' > footer .delete').on('click', function(event) {
|
||||
$.ajax('DELETE', '/1.0/' + path + '/' + post['id']).then(function(status, rv) {
|
||||
$.ajax('DELETE', prefix + '/1.0/' + path + '/' + post['id'])
|
||||
.then(function(status, rv) {
|
||||
// XXX comment might not actually deleted
|
||||
$('#isso_' + post['id']).remove();
|
||||
});
|
||||
@ -188,7 +192,7 @@ function insert(post) {
|
||||
|
||||
if ($('#issoform_' + post['id']).length == 0) {
|
||||
|
||||
$.ajax('GET', '/1.0/' + path + '/' + post['id'], {'plain': '1'})
|
||||
$.ajax('GET', prefix + '/1.0/' + path + '/' + post['id'], {'plain': '1'})
|
||||
.then(function(status, rv) {
|
||||
rv = JSON.parse(rv);
|
||||
form(post['id'],
|
||||
@ -285,7 +289,8 @@ function initialize(thread) {
|
||||
|
||||
|
||||
function fetch(thread) {
|
||||
$.ajax('GET', '/1.0/' + encodeURIComponent(window.location.pathname) + '/',
|
||||
console.log(window.location.pathname);
|
||||
$.ajax('GET', prefix + '/1.0/' + encodeURIComponent(window.location.pathname),
|
||||
{}, {'Content-Type': 'application/json'}).then(function(status, rv) {
|
||||
|
||||
if (status != 200) {
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
def get(name, convert):
|
||||
limit = request.args.get(name)
|
||||
return convert(limit) if limit is not None else None
|
||||
return convert(limit[0]) if limit is not None else None
|
||||
%>
|
||||
|
||||
<%block name="title">
|
||||
|
@ -6,10 +6,9 @@
|
||||
import json
|
||||
import socket
|
||||
import httplib
|
||||
import urlparse
|
||||
import contextlib
|
||||
|
||||
import werkzeug.routing
|
||||
from urlparse import urlparse
|
||||
|
||||
from isso.models import Comment
|
||||
|
||||
@ -23,12 +22,6 @@ class IssoEncoder(json.JSONEncoder):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class RegexConverter(werkzeug.routing.BaseConverter):
|
||||
def __init__(self, url_map, *items):
|
||||
super(RegexConverter, self).__init__(url_map)
|
||||
self.regex = items[0]
|
||||
|
||||
|
||||
def urlexists(host, path):
|
||||
with contextlib.closing(httplib.HTTPConnection(host)) as con:
|
||||
try:
|
||||
@ -43,7 +36,7 @@ def determine(host):
|
||||
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
rv = urlparse.urlparse(host)
|
||||
rv = urlparse(host)
|
||||
return (rv.netloc + ':443') if rv.scheme == 'https' else rv.netloc
|
||||
|
||||
|
||||
|
121
isso/wsgi.py
Normal file
121
isso/wsgi.py
Normal file
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import cgi
|
||||
import tempfile
|
||||
import urlparse
|
||||
import mimetypes
|
||||
|
||||
from Cookie import SimpleCookie
|
||||
from urllib import quote
|
||||
|
||||
|
||||
class Request(object):
|
||||
|
||||
def __init__(self, environ):
|
||||
|
||||
# from bottle.py Copyright 2012, Marcel Hellkamp, License: MIT.
|
||||
maxread = max(0, int(environ['CONTENT_LENGTH'] or '0'))
|
||||
stream = environ['wsgi.input']
|
||||
body = tempfile.TemporaryFile(mode='w+b')
|
||||
while maxread > 0:
|
||||
part = stream.read(maxread)
|
||||
if not part:
|
||||
break
|
||||
body.write(part)
|
||||
maxread -= len(part)
|
||||
|
||||
self.body = body
|
||||
self.body.seek(0)
|
||||
self.environ = environ
|
||||
self.query_string = self.environ['QUERY_STRING']
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
self.body.seek(0)
|
||||
return self.body.read()
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return self.environ['REQUEST_METHOD']
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
return urlparse.parse_qs(self.environ['QUERY_STRING'])
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
if self.environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded':
|
||||
return cgi.FieldStorage(fp=self.body, environ=self.environ)
|
||||
return dict()
|
||||
|
||||
@property
|
||||
def cookies(self):
|
||||
cookie = SimpleCookie(self.environ.get('HTTP_COOKIE', ''))
|
||||
return {v.key: v.value for v in cookie.values()}
|
||||
|
||||
|
||||
class Rule(str):
|
||||
|
||||
repl = {'int': r'[0-9]+', 'float': r'\-?[0-9]+\.[0-9]+'}
|
||||
|
||||
def __init__(self, string):
|
||||
|
||||
first, last, rv = 0, -1, []
|
||||
f = lambda m: '(?P<%s>%s)' % (m.group(2), self.repl.get(m.group(1), m.group(1)))
|
||||
|
||||
for i, c in enumerate(string):
|
||||
if c == '<':
|
||||
first = i
|
||||
rv.append(re.escape(string[last+1:first]))
|
||||
if c == '>':
|
||||
last = i
|
||||
if last > first:
|
||||
rv.append(re.sub(r'<\(([^:]+)\):(\w+)>', f, string[first:last+1]))
|
||||
first = last
|
||||
|
||||
rv.append(re.escape(string[last+1:]))
|
||||
self.rule = re.compile('^' + ''.join(rv) + '$')
|
||||
|
||||
def match(self, obj):
|
||||
|
||||
match = re.match(self.rule, obj)
|
||||
if not match: return
|
||||
|
||||
kwargs = match.groupdict()
|
||||
for key, value in kwargs.items():
|
||||
for type in int, float:
|
||||
try:
|
||||
kwargs[key] = type(value)
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def sendfile(filename, root):
|
||||
|
||||
headers = {}
|
||||
root = os.path.abspath(root) + os.sep
|
||||
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
|
||||
|
||||
if not filename.startswith(root):
|
||||
return 403, '', headers
|
||||
|
||||
mimetype, encoding = mimetypes.guess_type(filename)
|
||||
if mimetype: headers['Content-Type'] = mimetype
|
||||
if encoding: headers['Content-Encoding'] = encoding
|
||||
|
||||
stats = os.stat(filename)
|
||||
headers['Content-Length'] = str(stats.st_size)
|
||||
|
||||
return 200, io.open(filename, 'rb'), headers
|
||||
|
||||
|
||||
def setcookie(name, value, **kwargs):
|
||||
return '; '.join([quote(name, '') + '=' + quote(value, '')] +
|
||||
[k.replace('_', '-') + '=' + str(v) for k, v in kwargs.iteritems()])
|
2
setup.py
2
setup.py
@ -28,7 +28,7 @@ setup(
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7"
|
||||
],
|
||||
install_requires=['werkzeug', 'mako', 'itsdangerous'],
|
||||
install_requires=['mako', 'itsdangerous'],
|
||||
entry_points={
|
||||
'console_scripts':
|
||||
['isso = isso:main'],
|
||||
|
@ -113,8 +113,8 @@ class TestComments(unittest.TestCase):
|
||||
data=json.dumps(comment(text='...'))).status_code == 201
|
||||
|
||||
for path in paths:
|
||||
assert self.get('/1.0/' + path)
|
||||
assert self.get('/1.0/' + path + '/1')
|
||||
assert self.get('/1.0/' + path + '/').status_code == 200
|
||||
assert self.get('/1.0/' + path + '/1').status_code == 200
|
||||
|
||||
def testDeleteAndCreateByDifferentUsersButSamePostId(self):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user