|-flasky |-app/ |-api_1_0 |-init.py |-users.py |-posts.py |-comments.py |-authentication.py |-errors.py |-decorators.py
1 app/api_1_0/init.py from flask import Blueprint api = Blueprint('api', name) from . import authentication, posts, users, comments, errors
2 app/init.py def create_app(config_name): # ... from .api_1_0 import api as api_1_0_blueprint app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
3 app/main/errors.py @main.app_errorhandler(404) def page_not_found(e): if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: response = jsonify({'error': 'not found'}) response.status_code = 404 return response return render_template('404.html'), 404
4 app/api_1_0/errors.py def forbidden(message): response = jsonify({'error': 'forbidden', 'message': message}) response.status_code = 403 return response
5 pip install flask-httpauth #验证 6 app/api_1_0/authentication.py from flask.ext.httpauth import HTTPBasicAuth from .errors import forbidden_error auth = HTTPBasicAuth() @auth.verify_password def verify_password(email, password): if email == '': g.current_user = AnonymousUser() return True user = User.query.filter_by(email = email).first() if not user: return False g.current_user = user return user.verify_password(password) @auth.error_handler def auth_error(): return unauthorized('Invalid credentials') @api.before_request @auth.login_required def before_request(): if not g.current_user.is_anonymous and not g.current_user.confirmed: return forbidden('Unconfirmed account') @auth.verify_password def verify_password(email_or_token, password): if email_or_token == '': g.current_user = AnonymousUser() return True if password == '': g.current_user = User.verify_auth_token(email_or_token) g.token_used = True return g.current_user is not None user = User.query.filter_by(email=email_or_token).first() if not user: return False g.current_user = user g.token_used = False return user.verify_password(password) @api.route('/token') def get_token(): if g.current_user.is_anonymous() or g.token_used: return unauthorized('Invalid credentials') return jsonify({'token': g.current_user.generate_auth_token(expiration=3600),'expiration': 3600}) 7 app/models.py from app.exceptions import ValidationError class User(db.Model):
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],expires_in=expiration)
return s.dumps({'id': self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'followed_posts': url_for('api.get_user_followed_posts',id=self.id,_external=True),
'post_count': self.posts.count()
}
return json_user
class Post(db.Model):
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id, _external=True),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author': url_for('api.get_user', id=self.author_id,_external=True),
'comments': url_for('api.get_post_comments', id=self.id,_external=True)
'comment_count': self.comments.count()
}
return json_post
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('post does not have a body')
return Post(body=body)
7 app/exceptions.py class ValidationError(ValueError): pass
8 app/api_1_0/errors.py @api.errorhandler(ValidationError) def validation_error(e): return bad_request(e.args[0])
9 app/api_1_0/posts.py @api.route('/posts/') @auth.login_required def get_posts(): posts = Post.query.all() return jsonify({ 'posts': [post.to_json() for post in posts] }) @api.route('/posts/<int:id>') @auth.login_required def get_post(id): post = Post.query.get_or_404(id) return jsonify(post.to_json()) @api.route('/posts/', methods=['POST']) @permission_required(Permission.WRITE_ARTICLES) def new_post(): post = Post.from_json(request.json) post.author = g.current_user db.session.add(post) db.session.commit() return jsonify(post.to_json()), 201, {'Location': url_for('api.get_post', id=post.id, _external=True)} @api.route('/posts/<int:id>', methods=['PUT']) @permission_required(Permission.WRITE_ARTICLES) def edit_post(id): post = Post.query.get_or_404(id) if g.current_user != post.author and not g.current_user.can(Permission.ADMINISTER): return forbidden('Insufficient permissions') post.body = request.json.get('body', post.body) db.session.add(post) return jsonify(post.to_json()) @api.route('/posts/') def get_posts(): page = request.args.get('page', 1, type=int) pagination = Post.query.paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],error_out=False) posts = pagination.items prev = None if pagination.has_prev: prev = url_for('api.get_posts', page=page-1, _external=True) next = None if pagination.has_next: next = url_for('api.get_posts', page=page+1, _external=True) return jsonify({ 'posts': [post.to_json() for post in posts], 'prev': prev, 'next': next, 'count': pagination.total }
10 app/api_1_0/decorators.py def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, *kwargs): if not g.current_user.can(permission): return forbidden('Insufficient permissions') return f(args, **kwargs) return decorated_function return decorator
11 app/main/views.py #关闭服务器路由 @main.route('/shutdown') def server_shutdown(): if not current_app.testing: abort(404) shutdown = request.environ.get('werkzeug.server.shutdown') if not shutdown: abort(500) shutdown() return 'Shutting down...' 12 app/main/views.py ##报告缓慢的数据库查询 from flask.ext.sqlalchemy import get_debug_queries @main.after_app_request def after_request(response): for query in get_debug_queries(): if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']: current_app.logger.warning( 'Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement, query.parameters, query.duration, query.context)) return response
13 manage.py #分析源码 @manager.command def profile(length=25, profile_dir=None): """Start the application under the code profiler.""" from werkzeug.contrib.profiler import ProfilerMiddleware app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],profile_dir=profile_dir) app.run()
14 app/email.py from threading import Thread from flask import current_app, render_template from flask_mail import Message from . import mail def send_async_email(app, msg): with app.app_context(): mail.send(msg) def send_email(to, subject, template, **kwargs): app = current_app._get_current_object() msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) msg.body = render_template(template + '.txt', **kwargs) msg.html = render_template(template + '.html', **kwargs) thr = Thread(target=send_async_email, args=[app, msg]) thr.start() return thr
15 manage.py
import os COV = None if os.environ.get('FLASK_COVERAGE'): import coverage COV = coverage.coverage(branch=True, include='app/*') COV.start()
if os.path.exists('.env'): print('Importing environment from .env...') for line in open('.env'): var = line.strip().split('=') if len(var) == 2: os.environ[var[0]] = var[1]
from app import create_app, db from app.models import User, Follow, Role, Permission, Post, Comment from flask_script import Manager, Shell from flask_migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default') manager = Manager(app) migrate = Migrate(app, db)
def make_shell_context(): return dict(app=app, db=db, User=User, Follow=Follow, Role=Role, Permission=Permission, Post=Post, Comment=Comment) manager.add_command("shell", Shell(make_context=make_shell_context)) manager.add_command('db', MigrateCommand)
@manager.command def test(coverage=False): """Run the unit tests.""" if coverage and not os.environ.get('FLASK_COVERAGE'): import sys os.environ['FLASK_COVERAGE'] = '1' os.execvp(sys.executable, [sys.executable] + sys.argv) import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests) if COV: COV.stop() COV.save() print('Coverage Summary:') COV.report() basedir = os.path.abspath(os.path.dirname(file)) covdir = os.path.join(basedir, 'tmp/coverage') COV.html_report(directory=covdir) print('HTML version: file://%s/index.html' % covdir) COV.erase()
@manager.command def profile(length=25, profile_dir=None): """Start the application under the code profiler.""" from werkzeug.contrib.profiler import ProfilerMiddleware app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length], profile_dir=profile_dir) app.run()
@manager.command def deploy(): """Run deployment tasks.""" from flask_migrate import upgrade from app.models import Role, User
# migrate database to latest revision
upgrade()
# create user roles
Role.insert_roles()
# create self-follows for all users
User.add_self_follows()
if name == 'main': manager.run()
16 config.py import os basedir = os.path.abspath(os.path.dirname(file))
class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' SSL_DISABLE = False SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_RECORD_QUERIES = True MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' FLASKY_MAIL_SENDER = 'Flasky Admin flasky@example.com' FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') FLASKY_POSTS_PER_PAGE = 20 FLASKY_FOLLOWERS_PER_PAGE = 50 FLASKY_COMMENTS_PER_PAGE = 30 FLASKY_SLOW_DB_QUERY_TIME=0.5
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') WTF_CSRF_ENABLED = False
class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
@classmethod
def init_app(cls, app):
Config.init_app(app)
# email errors to the administrators
import logging
from logging.handlers import SMTPHandler
credentials = None
secure = None
if getattr(cls, 'MAIL_USERNAME', None) is not None:
credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
if getattr(cls, 'MAIL_USE_TLS', None):
secure = ()
mail_handler = SMTPHandler(
mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
fromaddr=cls.FLASKY_MAIL_SENDER,
toaddrs=[cls.FLASKY_ADMIN],
subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',
credentials=credentials,
secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
class HerokuConfig(ProductionConfig): SSL_DISABLE = bool(os.environ.get('SSL_DISABLE'))
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# handle proxy server headers
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
# log to stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.WARNING)
app.logger.addHandler(file_handler)
class UnixConfig(ProductionConfig): @classmethod def init_app(cls, app): ProductionConfig.init_app(app)
# log to syslog
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.WARNING)
app.logger.addHandler(syslog_handler)
config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'heroku': HerokuConfig, 'unix': UnixConfig,
'default': DevelopmentConfig
}
https://github.com/miguelgrinberg/flasky
image.png
(venv) $ http --json --auth <email>:<password> GET \
http://127.0.0.1:5000/api/v1.0/posts HTTP/1.0 200 OK Content-Length: 7018 Content-Type: application/json Date: Sun, 22 Dec 2013 08:11:24 GMT Server: Werkzeug/0.9.4 Python/2.7.3 { "posts": [ ... ], "prev": null "next": "http://127.0.0.1:5000/api/v1.0/posts/?page=2", "count": 150 }
(venv) $ http --json --auth : GET http://127.0.0.1:5000/api/v1.0/posts/
(venv) $ http --auth <email>:<password> --json POST \
http://127.0.0.1:5000/api/v1.0/posts/ "body=I'm adding a post from the command line." HTTP/1.0 201 CREATED Content-Length: 360 Content-Type: application/json Date: Sun, 22 Dec 2013 08:30:27 GMT Location: http://127.0.0.1:5000/api/v1.0/posts/111 Server: Werkzeug/0.9.4 Python/2.7.3 { "author": "http://127.0.0.1:5000/api/v1.0/users/1", "body": "I'm adding a post from the command line.", "body_html": "<p>I'm adding a post from the <em>command line</em>.</p>", "comments": "http://127.0.0.1:5000/api/v1.0/posts/111/comments", "comment_count": 0, "timestamp": "Sun, 22 Dec 2013 08:30:27 GMT", "url": "http://127.0.0.1:5000/api/v1.0/posts/111" }
(venv) $ http --auth <email>:<password> --json GET \
http://127.0.0.1:5000/api/v1.0/token HTTP/1.0 200 OK Content-Length: 162 Content-Type: application/json Date: Sat, 04 Jan 2014 08:38:47 GMT Server: Werkzeug/0.9.4 Python/3.3.3 { "expiration": 3600, "token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..." }
(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1.0/posts/