PK!>(j !create_flask_skeleton/__init__.pyfrom contextlib import contextmanager import re import os import sys import click from jinja2 import Template TEMPLATE_NAME = 'app' __version__ = '0.1.0' @click.command() @click.option('--version', help='version', is_flag=True) @click.option('--two', help='install python2 virtualenv', is_flag=True, default=False) @click.argument('name', required=False) def main(version, two, name): if version: click.echo('version {}'.format(__version__)) return if not name: click.echo('please give a project name') sys.exit(2) bootstrap(name, version=2 if two else 3) def mkdirs(name, dir_paths): if os.path.exists(name): click.echo( 'The directory {} already exists, try using a new directory name'.format(name)) sys.exit(1) root_dir = os.path.join(os.getcwd(), name) os.mkdir(root_dir) package_name = get_package_name(name) for dir_path in dir_paths: if dir_path.startswith(TEMPLATE_NAME): dir_path = package_name + dir_path[len(TEMPLATE_NAME):] path = os.path.join(os.getcwd(), name, dir_path) os.mkdir(path) def copy_files(name, file_paths, template_path): package_name = get_package_name(name) for file_path in file_paths: relative_path = file_path.replace(template_path + '/', '') if relative_path.startswith(TEMPLATE_NAME): relative_path = package_name + relative_path[len(TEMPLATE_NAME):] dest = os.path.join(os.getcwd(), name, relative_path) if file_path.endswith('.pyc'): continue content = replace_template(package_name, file_path) with open(dest, 'w') as f: f.write(content) def replace_template(name, file_path): with open(file_path) as f: content = f.read() if file_path.endswith('.html'): return content template = Template(content) return template.render(app=name) @contextmanager def cd(newdir): prevdir = os.getcwd() os.chdir(os.path.expanduser(newdir)) try: yield finally: os.chdir(prevdir) def install_packages(name, version): with cd(name): if os.system("poetry > /dev/null 2>&1") != 0: os.system("pip install poetry") # if version == 2: # os.system('pipenv --two') os.system("poetry install") click.echo("install requirements successfully") click.echo("cd to {} directory and run `poetry run flask run` to start development," " you may need to run `poetry run flask run` initdb to get a initial sqlite database setup" "".format(name)) package_pattern = re.compile('[a-zA-Z][a-zA-Z0-9_\-]+$') def get_package_name(name): return name.replace('-', '_') def bootstrap(name, version): if not package_pattern.match(name): click.echo('can not create package based on name {}'.format(name)) sys.exit(2) templates_path = os.path.abspath( os.path.join(os.path.dirname(__file__), 'template')) dir_paths = [] file_paths = [] for root, dirs, files in os.walk(templates_path): root_path = os.path.abspath(root) for f in files: file_paths.append(os.path.join(root_path, f)) if root != templates_path: dir_paths.append(root.replace('{}/'.format(templates_path), '')) mkdirs(name, dir_paths) copy_files(name, file_paths, templates_path) install_packages(name, version) if __name__ == '__main__': main() PK!sݹLL(create_flask_skeleton/template/.flaskenvFLASK_APP={{ app }}.app:create_app() FLASK_DEBUG=True FLASK_ENV=development PK!R)create_flask_skeleton/template/.gitignore# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .idea config.yaml PK!1+6create_flask_skeleton/template/.pre-commit-config.yaml[tool.poetry] name = "poi" version = "0.1.0" description = "" authors = ["Ryan Wang "] include = ["poi/templates/", "poi/static/"] [tool.poetry.dependencies] python = "^3.7" lazy_object_proxy = "^1.3" flask = "^1.0" flask_sqlalchemy = "^2.3" flask_migrate = "^2.3" ipython = "^7.2" python-dotenv = "^0.10.0" pyjwt = "^1.7" voluptuous = "^0.11.5" pymysql = "^0.9.3" gunicorn = "^19.9" flask_mail = "^0.9.1" pandas = "^0.23.4" xlrd = "^1.2" requests = "^2.21" xlsxwriter = "^1.1" typing_extensions = "^3.7" [tool.poetry.dev-dependencies] flake8 = "^3.6" black = {version = "^18.3-alpha.0",allows-prereleases = true} mypy = "^0.660" pytest = "^4.0" fabric = "^2.4" sqlalchemy-stubs = { git = "https://github.com/dropbox/sqlalchemy-stubs.git", branch = "master" } [tool.black] py36 = true exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" PK!~F**&create_flask_skeleton/template/LICENSEMIT License Copyright (c) 2017 Ryan Wang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!%??'create_flask_skeleton/template/Makefile start: poetry run flask run flake8: poetry run flake8 app PK!Occ(create_flask_skeleton/template/README.md# flask-starter-kit init for an flask app with my opinioned understanding ### features 1. simple validation 2. rest api friendly 3. basic jwt auth with different audience 4. sqalchemy integrated ### install poetry pip3 install poetry ### install requirements poetry install ### running 1. add APP_SETTINGS={your actual yaml config path} to your .env file, APP_SETTINGS=config.yaml is a good default choice. 2. add `FLASK_APP={{ app }}.main:app` and `FLASK_DEBUG=1` to your .env file 3. flask ishell 5. poetry run pytest tests 3. `poetry run flask run` or `poetry shell && flask run` PK!*create_flask_skeleton/template/__init__.pyPK!.create_flask_skeleton/template/app/__init__.pyPK!k@86 6 )create_flask_skeleton/template/app/api.pyimport functools from flask import Response, Flask, json, request from flask_sqlalchemy import Pagination from typing import Optional, List from mypy_extensions import TypedDict from .cors import append_cors_header from voluptuous import Schema, Invalid class ApiResult: def __init__(self, value, status=200, next_page=None): self.value = value self.status = status self.nex_page = next_page def to_response(self): return Response( json.dumps(self.value, ensure_ascii=False), status=self.status, mimetype="application/json", ) class ErrorDict(TypedDict): message: str code: int class ApiException(Exception): code: Optional[int] = None message: Optional[str] = None errors: Optional[List[ErrorDict]] = None status = 400 def __init__(self, message, status=None, code=None, errors=None): self.message = message or self.message self.status = status or self.status self.code = code or self.code self.errors = errors or self.errors def to_result(self): rv = {"message": self.message} if self.errors: rv["errors"] = self.errors if self.code: rv["code"] = self.code return ApiResult(rv, status=self.status) class NotAuthorized(ApiException): status = 401 class NotFound(ApiException): status = 404 message = "resource not found" class InvalidToken(ApiException): pass class AuthExpired(ApiException): pass class ApiFlask(Flask): def make_response(self, rv): if rv is None: rv = {} if isinstance(rv, Pagination): rv = { "pages": rv.pages, "has_prev": rv.has_prev, "has_next": rv.has_next, "total": rv.total, "items": rv.items, } from .globals import db if isinstance(rv, db.Model): rv = rv.as_dict() if isinstance(rv, (dict, list)): rv = ApiResult(rv) if isinstance(rv, ApiResult): response = rv.to_response() else: response = super(ApiFlask, self).make_response(rv) append_cors_header(response) return response def dataschema(schema): if isinstance(schema, dict): schema = Schema(schema) def decorator(f): @functools.wraps(f) def new_func(*args, **kwargs): try: kwargs.update(schema(request.get_json())) except Invalid as e: raise ApiException( 'Invalid data: {} (path "{}")'.format( e.msg, "".join(str(path) for path in e.path) ) ) return f(*args, **kwargs) return new_func return decorator PK!P# )create_flask_skeleton/template/app/app.pyimport importlib import os import yaml import logging import traceback from flask import request from .api import ApiException, ApiFlask from .globals import db, migrate from werkzeug.exceptions import NotFound, MethodNotAllowed, BadRequest from werkzeug.routing import RequestRedirect from werkzeug.utils import redirect def create_app(config=None): config = config or {} app = ApiFlask('{{ app }}') config_path = os.environ['APP_SETTINGS'] with open(config_path) as f: config.update(yaml.load(f)) app.config.update(config) register_blueprints(app) db.init_app(app) migrate.init_app(app) init_shell(app) register_error_handlers(app) return app def create_api_app(): pass def create_normal_app(): pass def register_blueprints(app): from .apps.admin import bp as admin app.register_blueprint(admin) from .apps.user import bp as user app.register_blueprint(user) def register_error_handlers(app): def wants_json_response(): return ( request.accept_mimetypes["application/json"] >= request.accept_mimetypes["text/html"] ) app.register_error_handler(ApiException, lambda err: err.to_result()) logger = logging.getLogger(__name__) def handle_err(e): if wants_json_response(): if isinstance(e, BadRequest): return ApiException(e.description).to_result() if isinstance(e, NotFound): return ApiException("Not Found", status=404).to_result() if isinstance(e, MethodNotAllowed): return ApiException("method not allowed", status=405).to_result() logger.exception("系统异常") if app.debug: return ApiException(traceback.format_exc(), status=500).to_result() return ApiException("系统异常", status=500).to_result() if isinstance(e, NotFound): return "resource not found", 404 if isinstance(e, MethodNotAllowed): return "method not allowed", 405 if isinstance(e, RequestRedirect): return redirect(e.new_url) raise e app.register_error_handler(Exception, handle_err) app.register_error_handler(ApiException, lambda err: err.to_result()) def init_shell(app): @app.cli.command("ishell") def shell(): # lazy import these modules as they are only used in the shell context from IPython import embed, InteractiveShell import cProfile import pdb main = importlib.import_module("__main__") banner = f"App: poi" from . import models ctx = main.__dict__ ctx.update( { **models.__dict__, "session": db.session, "pdb": pdb, "cProfile": cProfile, } ) with app.app_context(): ctx.update(app.make_shell_context()) InteractiveShell.colors = "Neutral" embed(user_ns=ctx, banner2=banner) PK!3create_flask_skeleton/template/app/apps/__init__.pyPK!ߩ>0create_flask_skeleton/template/app/apps/admin.pyfrom flask import Blueprint, request from ..auth import check_auth, encode_jwt from ..api import dataschema from ..models import User bp = Blueprint('admin', __name__, url_prefix='/admin') AUD_ADMIN = 'AUD_ADMIN' @bp.before_request def before_request(): if not request.endpoint.endswith('login'): check_auth(audience=AUD_ADMIN) @bp.route('/users') def users(): users = User.query.offset(0).limit(10) def serialize(user): return { 'id': user.id, 'name': user.name } return [serialize(user) for user in users] @bp.route('/login', methods=['POST']) @dataschema({ 'name': str, }) def login(name): user = User.query.filter_by(name=name).one() token = encode_jwt({'id': user.id}, AUD_ADMIN) return { 'token': token.decode('utf-8'), 'user': { 'name': user.name, 'id': user.id, } } PK!&/create_flask_skeleton/template/app/apps/user.pyimport functools from flask import Blueprint, g from ..auth import check_auth, encode_jwt from ..api import dataschema from ..models import User from ..globals import current_user bp = Blueprint('{{ app }}', __name__) @bp.route('/') def index(): return 'welcome to flask world!' @bp.route('/db') def db(): users = list(User.query.all()) return 'flask app with sqlalchemy {}'.format("
".join(_.name for _ in users)) @bp.route('/login', methods=['POST']) @dataschema({ 'name': str, }) def login(name): user = User.query.filter_by(name=name).one() token = encode_jwt({'id': user.id}, 'AUD_APP') return { 'token': token.decode('utf-8'), 'user': { 'name': user.name, 'id': user.id, } } def auth_callback(payload): user_id = payload["id"] g.user = User.find_one(User.id == user_id) def login_required(fn): functools.wraps(fn) def wrapper(*args, **kwargs): check_auth('AUD_APP', auth_callback) return fn(*args, **kwargs) return wrapper @bp.route('/profile') @login_required def profile(): return 'this is profile of {}'.format(current_user().name) @bp.route('/test', methods=['POST']) @dataschema({ 'a': int }) def test(a): return {'a': a} PK!$q*create_flask_skeleton/template/app/auth.pyfrom flask import request, current_app import jwt from .api import NotAuthorized, InvalidToken, AuthExpired JWT_ALGORITHM = "HS256" def get_token_auth_header(): auth = request.headers.get("Authorization", None) if not auth: raise NotAuthorized("Authorization header is expected") parts = auth.split() if parts[0].lower() != "bearer": raise NotAuthorized("Authorization header must start with bearer") elif len(parts) == 1: raise NotAuthorized("Token not found") elif len(parts) > 2: raise NotAuthorized("Authorization header must be" " Bearer token") return parts[1] def check_auth(audience, auth_callback=None): token = get_token_auth_header() try: # different app should not share token payload = decode_jwt(token, audience) except jwt.ExpiredSignatureError: raise AuthExpired("token is expired") except jwt.MissingRequiredClaimError: raise NotAuthorized("incorrect claims, please check the audience and issuer") except jwt.InvalidIssuerError: raise InvalidToken("issuer invalid") except jwt.InvalidAudience: raise InvalidToken("audience invalid") except jwt.InvalidTokenError: raise InvalidToken("Unable to parse authentication header") except jwt.InvalidSignatureError: raise InvalidToken("token secret not match") if auth_callback: auth_callback(payload) def decode_jwt(token, audience): return jwt.decode( token.encode("utf-8"), current_app.secret_key, algorithms=JWT_ALGORITHM, audience=audience, ) def encode_jwt(payload, audience): return jwt.encode({**payload, "aud": audience}, current_app.secret_key) PK!oש*create_flask_skeleton/template/app/cors.pyfrom flask import current_app, request ALLOWED_HEADERS = ( 'Authorization,' 'Content-Type', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Unmodified-Since', 'Range', 'X-Requested-With' ) access_control_allow_headers = ', '.join(ALLOWED_HEADERS) def append_cors_header(response): # always add Access-Control-Allow-Origin header response.headers.add( 'Access-Control-Allow-Origin', current_app.config.get('CORS_DOMAIN', '*') ) # allow cookies to be send response.headers.add('Access-Control-Allow-Credentials', 'true') # always add Access-Control-allow-Headers regard of whether # Access-Control-Request-Headers is provided to avoid confusion. # and safari requires this response.headers.add( 'Access-Control-Allow-Headers', access_control_allow_headers ) if request.method == 'OPTIONS': # for cors preflight request if request.headers.get('access-control-request-method'): # cache preflight for 24 hours, at least on the server side. response.headers.add('Access-Control-Max-Age', '86400') response.headers.add( 'Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, PATCH, PUT' ) else: # for actual request but not preflight # e.g: response.headers.add('Access-Control-Expose-Headers', '') return PK!E;;(create_flask_skeleton/template/app/db.pyimport datetime from types import GeneratorType from typing import List, Any, Optional, TypeVar, Type, Dict, Tuple, cast import flask_sqlalchemy from sqlalchemy.orm import load_only from sqlalchemy import desc, Column, DateTime, ForeignKey, Integer, inspect from sqlalchemy.ext.declarative import declared_attr from flask_sqlalchemy import Pagination from .api import NotFound T = TypeVar("T", bound="ModelClass") class ModelClass(flask_sqlalchemy.Model): query: "Query" EXCLUDED_FIELDS: Tuple[str] = cast(Tuple[str], ()) @declared_attr def id(cls): for base in cls.__mro__[1:-1]: if getattr(base, "__table__", None) is not None: type = ForeignKey(base.id) break else: type = Integer return Column(type, primary_key=True) created_at = Column(DateTime, default=datetime.datetime.now) updated_at = Column( DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now ) @classmethod def mget(cls: Type[T], ids, fields=None) -> List[T]: if isinstance(ids, GeneratorType): raise TypeError( "mget not support generator as we cannot know the item size of this" " generator before consume it, actually we should know if it is empty" " to avoid empty id list query" ) if not ids: return [] q = cls.query if fields: q = q.options(load_only(*fields)) ids = [int(id) for id in ids] result = q.filter(cls.id.in_(ids)).all() result = {r.id: r for r in result} return [result.get(i) for i in ids] def as_dict(self): return { c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs if c.key not in self.EXCLUDED_FIELDS } @classmethod def serialize_list(cls, array): return [_.as_dict() for _ in array] @classmethod def add(cls, **kwargs): from .globals import session model = cls(**kwargs) session.add(model) return model def delete(self): from .globals import session session.delete(self) @classmethod def last(cls: Type[T]) -> Optional[T]: return cls.query.order_by(cls.id.desc()).first() # type: ignore @classmethod def first(cls: Type[T]) -> Optional[T]: return cls.query.first() # type: ignore def update_from_dict(self, obj: Dict[str, Any], *fields: str): def safe_setattr(k, v): if k not in self.columns: raise TypeError(f"{k} is not a valid column attribute") setattr(self, k, v) if not fields: for k, v in obj.items(): safe_setattr(k, v) else: for field in fields: safe_setattr(field, obj[field]) @property def columns(self): return inspect(self).mapper.column_attrs @classmethod def from_dict(cls: Type[T], obj, *fields) -> T: model = cls() model.update_from_dict(obj, *fields) return model @classmethod def filter(cls, *args, **kwargs): return cls.query.filter(*args, **kwargs) @classmethod def filter_by(cls, **kwargs): return cls.query.filter_by(**kwargs) @classmethod def find_first(cls: Type[T], *args, **kwargs) -> Optional[T]: return cls.query.filter(*args, **kwargs).first() # type: ignore @classmethod def find_one(cls: Type[T], *args, **kwargs) -> T: return cls.query.filter(*args, **kwargs).one() # type: ignore @classmethod def find_by_one(cls: Type[T], *args, **kwargs) -> T: return cls.query.filter_by(*args, **kwargs).one() # type: ignore @classmethod def find_one_or_404(cls: Type[T], *args, message=None, **kwargs) -> T: return cls.query.filter(*args, **kwargs).one_or_404(message) # type: ignore @classmethod def find_one_or_none(cls: Type[T], *args, **kwargs) -> Optional[T]: return cls.query.filter(*args, **kwargs).one_or_none() # type: ignore @classmethod def find(cls: Type[T], *args, **kwargs) -> List[T]: return cls.query.filter(*args, **kwargs).all() # type: ignore @classmethod def find_by(cls: Type[T], *args, **kwargs) -> List[T]: return cls.query.filter_by(*args, **kwargs).all() # type: ignore @classmethod def all(cls: Type[T]) -> List[T]: return cls.query.all() # type: ignore @classmethod def get(cls: Type[T], *args, **kwargs) -> Optional[T]: return cls.query.get(*args, **kwargs) # type: ignore @classmethod def get_or_404(cls: Type[T], *args, **kwargs) -> T: return cls.query.get_or_404(*args, **kwargs) # type: ignore class Query(flask_sqlalchemy.BaseQuery): def paginate( self, page=None, per_page=None, error_out=False, max_per_page=None ) -> Pagination: return super().paginate(page, per_page, error_out, max_per_page) def id_desc(self) -> "Query": return self.order_by(desc("id")) # type: ignore def created_at_desc(self): return self.order_by(desc("created_at")) def one_or_404(self, message=None): rv = self.one_or_none() if rv is None: raise NotFound(message) return rv def get_or_404(self, ident, message=None): rv = self.get(ident) if rv is None: raise NotFound(message) return rv def first_or_404(self, message=None): rv = self.first() if rv is None: raise NotFound(message) return rv PK!-create_flask_skeleton/template/app/globals.pyfrom flask import g from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from .db import ModelClass, Query from sqlalchemy.orm import Session from typing import TYPE_CHECKING db = SQLAlchemy(model_class=ModelClass, query_class=Query) session: Session = db.session if TYPE_CHECKING: from .models import User class Model(ModelClass): pass else: Model = db.Model migrate = Migrate() def current_user() -> "User": return g.user # type: ignore PK!K`NN,create_flask_skeleton/template/app/models.pyfrom __future__ import print_function from sqlalchemy.ext.declarative import declarative_base from .globals import Model import sqlalchemy as sa Base = declarative_base() class User(Model): __tablename__ = 'users' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String(50), unique=True) email = sa.Column(sa.String(120), unique=True) is_admin = sa.Column(sa.Boolean, default=False) def __init__(self, name=None, email=None): self.name = name self.email = email def __repr__(self): return '' % self.name PK!l 4create_flask_skeleton/template/app/static/index.htmlstatic filePK!5Ҋ7create_flask_skeleton/template/app/templates/hello.html Hello from Flask {% if name %}

Hello {{ name }}!

{% else %}

Hello, World!

{% endif %}PK!9>'create_flask_skeleton/template/mypy.ini[mypy] python_version = 3.7 warn_return_any = True warn_unused_configs = True check_untyped_defs = false ignore_missing_imports = true plugins = sqlmypy PK!|㏻-create_flask_skeleton/template/pyproject.toml[tool.poetry] name = "{{ app }}" version = "0.1.0" description = "" authors = ["Ryan Wang "] include = ["{{ app }}/templates/", "{{ app }}/static/"] [tool.poetry.dependencies] python = "^3.7" flask = "^1.0" pyjwt = "^1.7" flask_sqlalchemy = "^2.3" flask_migrate = "^2.3" mypy_extensions = "^0.4.1" python-dotenv = "^0.10.1" ipython = "^7.2" pyyaml = "^3.13" voluptuous = "^0.11.5" [tool.poetry.dev-dependencies] pytest = "^4.2" flake8 = "^3.6" black = {version = "^18.3-alpha.0",allows-prereleases = true} mypy = "^0.660" pytest = "^4.0" fabric = "^2.4" sqlalchemy-stubs = { git = "https://github.com/dropbox/sqlalchemy-stubs.git", branch = "master" } [tool.black] py36 = true exclude = ''' /( \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" PK!0create_flask_skeleton/template/tests/__init__.pyPK!c{D0create_flask_skeleton/template/tests/test_app.pyfrom {{ app }}.app import create_app import pytest @pytest.fixture(scope='module') def app(): config = { 'TESTING': True, } app = create_app(config=config) with app.app_context(): # init_db(app) yield app @pytest.fixture(scope='module') def client(request, app): client = app.test_client() client.__enter__() request.addfinalizer(lambda: client.__exit__(None, None, None)) return client def test_home(client): rv = client.post('/test', json={'a': 12}) assert rv.is_json assert rv.json['a'] == 12 def test_home_bad(client): rv = client.post('/test', json={'a': 'str'}) assert rv.is_json assert rv.status_code == 400 PK!HHD8D6create_flask_skeleton-0.1.0.dist-info/entry_points.txtN+I/N.,()J.JM,IMI,-NI-ϳƃEaVy\\PK!~F**-create_flask_skeleton-0.1.0.dist-info/LICENSEMIT License Copyright (c) 2017 Ryan Wang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTU+create_flask_skeleton-0.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H]'.create_flask_skeleton-0.1.0.dist-info/METADATAMK19ZhnWX mQ XRўud)-"^<3#fx c2+>%kmD(vR'RsK,d) ;qP*_i۰iѧ $t"8k=6ޛIr)yd=_R- 5]Ǡ#8g+j,\?꿾Ltkڎ_,,+YL~0:%9qѩdYWPK!H~Ӣ0 ,create_flask_skeleton-0.1.0.dist-info/RECORDɒX,`2^ " †@yL>}[ՑYVdU ]Ѝ >՛U \w,PxICPFcX9m+79FmV2˦x~Сz E;)aN4Ah|4.t8܀ږǩ'm&51sP&_.,SxUQ,qR}gM TiءSWq|鏛fm-B}-`5Gf6ht(f跔gIjC^xyŇ[QU#l{ E\gHA{R?kNWe.oo`^@U(~7{3<< NFJ@]7҂IizxKRUrvxb zY^/ I1V&p|Z1nLK|f0@Vc/'ŽAfN95tn[5f"zL'`hUĜw3i YX8C,J3aQup=`c;>+b玱 E Y֒|Ƽ,$Ot`Z1A;(AK\ͬ%,SBiKu?!0{f̺hj Q*"JF|=8@_CBf! Yb3!dy=^t/+KKԏIcԏI}M:D_V%!늯R b g$zZEȾES!R2zK W :[Mw]ػ\P{\h"P9z6~GoIX/J$:i\8 !uCa5? Q-6.P.Ͳz4GrVL݄rlrfm݁!ט7u6 M,MWgVCӧjUu S~P^qK()AP:_Z0 u*v6F WkIY_5c]5 ?dFV YiGoam Ddgt o;V&n NSi|'y1:젵֝U+7ӢkLJW!R8-6'Ÿ~|.! h)+|5t7:˶5('~8/PK!>(j !create_flask_skeleton/__init__.pyPK!sݹLL( create_flask_skeleton/template/.flaskenvPK!R)create_flask_skeleton/template/.gitignorePK!1+6ocreate_flask_skeleton/template/.pre-commit-config.yamlPK!~F**&create_flask_skeleton/template/LICENSEPK!%??',create_flask_skeleton/template/MakefilePK!Occ(create_flask_skeleton/template/README.mdPK!*Ycreate_flask_skeleton/template/__init__.pyPK!.create_flask_skeleton/template/app/__init__.pyPK!k@86 6 )create_flask_skeleton/template/app/api.pyPK!P# )j+create_flask_skeleton/template/app/app.pyPK!37create_flask_skeleton/template/app/apps/__init__.pyPK!ߩ>07create_flask_skeleton/template/app/apps/admin.pyPK!&/;create_flask_skeleton/template/app/apps/user.pyPK!$q*/Acreate_flask_skeleton/template/app/auth.pyPK!oש*CHcreate_flask_skeleton/template/app/cors.pyPK!E;;(Ncreate_flask_skeleton/template/app/db.pyPK!-dcreate_flask_skeleton/template/app/globals.pyPK!K`NN,fcreate_flask_skeleton/template/app/models.pyPK!l 4gicreate_flask_skeleton/template/app/static/index.htmlPK!5Ҋ7icreate_flask_skeleton/template/app/templates/hello.htmlPK!9>'jcreate_flask_skeleton/template/mypy.iniPK!|㏻-kcreate_flask_skeleton/template/pyproject.tomlPK!0[ocreate_flask_skeleton/template/tests/__init__.pyPK!c{D0ocreate_flask_skeleton/template/tests/test_app.pyPK!HHD8D6rcreate_flask_skeleton-0.1.0.dist-info/entry_points.txtPK!~F**-Gscreate_flask_skeleton-0.1.0.dist-info/LICENSEPK!HڽTU+wcreate_flask_skeleton-0.1.0.dist-info/WHEELPK!H]'.Yxcreate_flask_skeleton-0.1.0.dist-info/METADATAPK!H~Ӣ0 ,ycreate_flask_skeleton-0.1.0.dist-info/RECORDPK ~