diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..35b58ff --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,45 @@ +version: 2 +jobs: + pypi: + docker: + - image: circleci/python:3.6.1 + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "requirements.txt" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: + name: install dependencies + command: | + python3 -m venv venv + . venv/bin/activate + pip install wheel twine + + - save_cache: + paths: + - ./venv + key: v1-dependencies-{{ checksum "requirements.txt" }} + + - run: + name: run build + command: | + echo 'dry run' + #. venv/bin/activate + #python setup.py sdist bdist_wheel + #twine upload --repository pypi dist/* + +workflows: + version: 2 + release: + jobs: + - pypi: + filters: + branches: + only: master diff --git a/README.md b/README.md index 35f15cd..98430f7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,146 @@ -# Flask-Best-Practices -このリポジトリはFlaskのベストプラクティス、実施的なテクニックを紹介するリポジトリです。 -以下の URL で色々確認できます。 +# Hermetica -https://github.com/yoshiya0503/Flask-Best-Practices/wiki + +[![CircleCI](https://circleci.com/gh/yoshiya0503/Hermetica.svg?style=shield&circle-token=4614abf3b106e5f31f9726ebaedfcebc5c7fa859)](https://circleci.com/gh/yoshiya0503/Hermetica) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](CONTRIBUTING.md#pull-requests) +[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) + +--- + +THIS IS NOT WEB FRAMEWORK TO REPLACE FLASK. + +Hermetica is scaffold tools, and wiki to implement better flask applications. + +When we try to build web applications by using flask web framework, there are to many patterns and practices. +This diversity make it difficult to implement apps that have simple architecture. +In other words, because of too many manners, options, and patters, to implement more bigger applications is not so easy. +(but, to implement small app by using flask is extreamly easy) + +Therefor, we try to implement the scaffold tools head for better architecture applications as mach as possible +based on our many experiences. + +* better and common directory structure +* scaffold to create tipycal API, model. +* select powerful packages(like SQLAlchemy Nose) + +# Installation + +We dare to support only python 3.x, because python 2.x will eventually deprecated almost all systems, and we have to get used to python 3.x quickly. + +``` +pip install hermetica +``` + +# Usage + +### Overview the usage + +hermetica has some subcommands, to create scaffold api, decorator, model. + +* api (url and routing method base or class base or flask-restful) +* model (database models, sqlalchemy or mongoengine) +* decorator (you can insert some code before enter the api, like a 'authentication') + +``` +→ hermetica --help +Usage: hermetica [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + api create api + decorator create decorator + init initialize your flask app + model create model +``` + +### initialize your flask project. + +``` +→ hermetica init --help +Usage: hermetica init [OPTIONS] + + initialize your flask app + +Options: + --api [restful|decorator|class] + Flask-Restful or Flask decorator or + methodview + --db [sqlalchemy|mongoengine] SQLAlchemy or Mongoengine or None + --decorator create decorator or None + --redis using Redis or None + --docker using container + --help Show this message and exit. +``` + +After create project scaffold, you will check `Pipfile` contents, if there are shortages in list of packages, you can +add other packages into `Pipfile`, and lock your package. +(We recommend you to use `pipenv` https://github.com/pypa/pipenv) + +Hermetica support docker. you can see `Dockerfile` and `docker-compose.yml` at your root of project. +We recommend you to use docker-compose, it will helpful to separate from other projects. + +``` +pipenv lock + +# if you set docker option, you can up the app container +docker-compose build +docker-compose up +``` + +### add api to your flask project. + +``` +→ hermetica api --help +Usage: hermetica api [OPTIONS] NAME + + create api + +Options: + --api [restful|decorator|class] + Flask-Restful or Flask decorator or + methodview + --version TEXT API version + --help Show this message and exit. +``` + +### add model to your flask project. + +``` +→ hermetica model --help +Usage: hermetica model [OPTIONS] NAME + + create model + +Options: + --db [sqlalchemy|mongoengine] SQLAlchemy or Mongoengine or None + --help Show this message and exit. +``` + +### add decorator to your flask project. + +``` +→ hermetica decorator --help +Usage: hermetica decorator [OPTIONS] NAME + + create decorator + +Options: + --help Show this message and exit. +``` + +# Development + +This repos is too young, so we provide few useful features yet. +we will grad if you send PRs... + +# See Before Wiki (Flask Best Practices) + +Why we apply broken change? Because, before repo source code is slightly trivial, +and we believe this change will not cause any negative impact to others. + +To create scaffold tools for flask will cause good affect the world rather than remain trivial code. +But there are no warries. Flask-best-Practices contents (wiki docs) remain here (but only for japanese). + +https://github.com/yoshiya0503/Hermetica/wiki diff --git a/sample_app/app/models/__init__.py b/hermetica/__init__.py similarity index 100% rename from sample_app/app/models/__init__.py rename to hermetica/__init__.py diff --git a/hermetica/cli.py b/hermetica/cli.py new file mode 100644 index 0000000..f3f4ee5 --- /dev/null +++ b/hermetica/cli.py @@ -0,0 +1,128 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +main.py +hermetica main script +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-24' +import os +import click +from hermetica.scaffold.app import App +from hermetica.scaffold.config import Config +from hermetica.scaffold.wsgi import WSGI +from hermetica.scaffold.api import API +from hermetica.scaffold.model import Model +from hermetica.scaffold.test import Test +from hermetica.scaffold.docker import Docker +from hermetica.scaffold.extension import Extension +from hermetica.scaffold.pipfile import Pipfile +from hermetica.scaffold.decorator import Decorator + +@click.group() +def main(): + pass + +@main.command() +@click.option('--api', default='restful', type=click.Choice(['restful', 'decorator', 'class']), help='Flask-Restful or Flask decorator or methodview') +@click.option('--db', default=None, type=click.Choice(['sqlalchemy', 'mongoengine']), help='SQLAlchemy or Mongoengine or None') +@click.option('--decorator', default=False, is_flag=True, help='create decorator or None') +@click.option('--redis', default=False, is_flag=True, flag_value='redis', help='using Redis or None') +@click.option('--docker', default=False, is_flag=True, flag_value='docker', help='using container') +def init(api, db, decorator, redis, docker): + """ initialize your flask app + """ + dirs = ['./app/', './test/', './config/', './app/api/v1/', './app/models/'] + for dir in dirs: + if os.path.exists(os.path.dirname(dir)): + click.echo('[WARNING] directory {} is already exists, skip to create this directory'.format(dir)) + continue + os.makedirs(os.path.dirname(dir)) + + app = App(db=db, redis=redis, api=api) + pipfile = Pipfile(db=db, redis=redis) + wsgi = WSGI(db=db) + config = Config(db=db, redis=redis) + extension = Extension(db=db, redis=redis) + test = Test(db=db, name='root') + api = API(api=api, name='root') + decorator = Decorator(name='root') + + with open('./Pipfile', 'w') as f: + f.write(pipfile.create_pipfile()) + with open('wsgi.py', 'w') as f: + f.write(wsgi.create_wsgi()) + + with open('app/__init__.py', 'w') as f: + f.write(app.create_app__init__()) + with open('app/extensions.py', 'w') as f: + f.write(extension.create_extensions()) + if decorator: + with open('app/decorators.py', 'w') as f: + f.write(decorator.create_decorators()) + + with open('config/__init__.py', 'w') as f: + f.write(config.create_config(name='config', env='test')) + with open('config/development.py', 'w') as f: + f.write(config.create_config(name='development', env='development')) + with open('config/production.py', 'w') as f: + f.write(config.create_config(name='production', env='production')) + + if docker: + docker = Docker(db=db, redis=redis) + with open('Dockerfile', 'w') as f: + f.write(docker.create_dockerfile()) + with open('docker-compose.yml', 'w') as f: + f.write(docker.create_docker_compose_yml()) + + with open('test/__init__.py', 'w') as f: + f.write(test.create__init__()) + with open('nose.cfg', 'w') as f: + f.write(test.create_nose_cfg()) + + with open('app/api/__init__.py', 'w') as f: + f.write(api.create__init__()) + with open('app/api/v1/__init__.py', 'w') as f: + pass + with open('app/api/v1/root.py', 'w') as f: + f.write(api.create_api()) + + if db: + model = Model(db=db, name='root') + with open('app/models/__init__.py', 'w') as f: + f.write(model.create__init__()) + with open('app/models/root.py', 'w') as f: + f.write(model.create_model()) + +@main.command() +@click.argument('name', type=str, required=True) +@click.option('--api', default='restful', type=click.Choice(['restful', 'decorator', 'class']), help='Flask-Restful or Flask decorator or methodview') +@click.option('--version', default='v1', help='API version') +def api(name, api, version): + """ create api + """ + path = 'app/api/{}/{}.py'.format(version, name) + api = API(api=api, name=name) + with open(path, 'w') as f: + f.write(api.create_api()) + +@main.command() +@click.argument('name', type=str, required=True) +@click.option('--db', default='sqlalchemy', type=click.Choice(['sqlalchemy', 'mongoengine']), help='SQLAlchemy or Mongoengine or None') +def model(name, db): + """ create model + """ + path = 'app/models/{}.py'.format(name) + model = Model(db=db, name=name) + with open(path, 'w') as f: + f.write(model.create_model()) + +@main.command() +@click.argument('name', type=str, required=True) +def decorator(name): + """ create decorator + """ + decorator = Decorator(name=name) + with open('app/decorators.py', 'a') as f: + f.write(decorator.create_decorator()) diff --git a/hermetica/scaffold/__init__.py b/hermetica/scaffold/__init__.py new file mode 100644 index 0000000..15c23d8 --- /dev/null +++ b/hermetica/scaffold/__init__.py @@ -0,0 +1,18 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +__init__.py +Scaffold Abstract +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-27' + + +class Scaffold(object): + def __init__(self, filepath): + self.filepath = filepath + + def write(self, source_code): + with open(self.filepath, 'w') as py: + py.write(source_code) diff --git a/hermetica/scaffold/api.py b/hermetica/scaffold/api.py new file mode 100644 index 0000000..f0c94a8 --- /dev/null +++ b/hermetica/scaffold/api.py @@ -0,0 +1,186 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +api.py +scaffold create_api +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-01' +from textwrap import dedent +from inflector import Inflector + + +class API(object): + """ API Scaffold + """ + + def __init__(self, api=None, name=None): + self.api = api + self.name = name + + def create__init__(self): + if self.api == 'restful': + return self.create_restful__init__() + if self.api == 'decorator': + return self.create_decorator__init__() + if self.api == 'class': + return self.create_method_view__init__() + + def create_api(self): + if self.api == 'restful': + return self.create_restful() + if self.api == 'decorator': + return self.create_decorator() + if self.api == 'class': + return self.create_method_view() + + def create_restful__init__(self): + name=Inflector().camelize(self.name) + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask import Blueprint + from flask_restful import Api + from app.api.v1.{name} import {Name} + + api_v1 = Blueprint('api/v1', __name__) + api = Api(api_v1) + api.add_resource({Name}, '/{names}', '/{names}/') + """.format( + name=Inflector().underscore(self.name), + names=Inflector().pluralize(self.name), + Name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_restful(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask_restful import Resource, fields, marshal_with, reqparse + + resource_fields = {{ + 'id': fields.Integer, + 'created_at': fields.DateTime, + 'updated_at': fields.DateTime, + }} + + class {name}(Resource): + + parser = reqparse.RequestParser() + parser.add_argument('query', type=str, help="query string") + parser.add_argument('body', type=str, help="body string") + + @marshal_with(resource_fields) + def get(self, id=None): + args = self.parser.parse_args() + if id is None: + return {{}}, 200 + return [], 200 + + @marshal_with(resource_fields) + def post(self): + args = self.parser.parse_args() + return {{}}, 201 + + @marshal_with(resource_fields) + def put(self, id=None): + args = self.parser.parse_args() + return {{}}, 204 + + @marshal_with(resource_fields) + def delete(self, id=None): + return {{}}, 204 + """.format( + name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_method_view__init__(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask import Blueprint + from app.api.v1.{name} import {Name} + + api_v1 = Blueprint('api/v1', __name__) + api_v1.add_url_rule('/{names}', view_func={Name}.as_view('{name}'), methods=['GET', 'POST']) + api_v1.add_url_rule('/{names}/', view_func={Name}.as_view('{name}'), methods=['GET', 'PUT', 'DELETE']) + """.format( + name=Inflector().underscore(self.name), + names=Inflector().pluralize(self.name), + Name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_method_view(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask.views import MethodView + + class {name}(MethodView): + def get(self, id=None): + if not id: return 'index' + return 'show' + + def post(self): + return 'create' + + def put(self, id): + return 'update' + + def delete(self, id): + return 'destroy' + + """.format( + name=Inflector().camelize(self.name), + ) + return dedent(source_code).strip() + + def create_decorator__init__(self): + instance = Inflector().underscore(self.name) + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from app.api.v1.{instance} import {instance} + """.format( + instance=instance, + ) + return dedent(source_code).strip() + + def create_decorator(self): + instance = Inflector().underscore(self.name) + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask import Blueprint + + {instance} = Blueprint('{instances}', __name__) + + @{instance}.route('/{instances}', methods=['GET']) + def index(): + return 'index' + + @{instance}.route('/{instances}/', methods=['GET']) + def show(id): + return 'show' + + @{instance}.route('/{instances}/', methods=['POST']) + def create(): + return 'create' + + @{instance}.route('/{instances}/', methods=['PUT']) + def update(id): + return 'update' + + @{instance}.route('/{instances}/', methods=['DELETE']) + def destroy(id): + return 'destroy' + + """.format( + instance=instance, + instances=Inflector().pluralize(instance) + ) + return dedent(source_code).strip() diff --git a/hermetica/scaffold/app.py b/hermetica/scaffold/app.py new file mode 100644 index 0000000..6084628 --- /dev/null +++ b/hermetica/scaffold/app.py @@ -0,0 +1,86 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +app.py +scaffold create_app +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-27' +from textwrap import dedent + + +class App(object): + """ App Scaffold + """ + + def __init__(self, api=None, db=None, redis=None): + self.api = api + self.db = db + self.redis = redis + + def create_app__init__(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from flask import Flask + {header} + + def create_app(env='development'): + app = Flask(__name__) + + if env == 'development': + app.config.from_object('config.development.Development') + elif env == 'production': + app.config.from_object('config.production.Production') + else: + app.config.from_object('config.development.Development') + + {extension} + {api} + + return app + """.format( + header=self.create_header(), + extension=self.create_extension(), + api=self.create_api(), + ) + return dedent(source_code).strip() + + def create_header(self): + import_db = '' + import_redis = '' + import_api = '' + if self.db: + import_db = 'from app.extensions import db' + if self.redis == 'redis': + import_redis = 'from app.extensions import redis' + if self.api: + import_api = 'from app import api' + + header = """ + {} + {} + {} + """.format(import_db, import_redis, import_api) + return header.strip() + + def create_extension(self): + create_db = '' + create_redis = '' + if self.db: + create_db = 'db.init_app(app)' + if self.redis == 'redis': + create_redis = 'redis.init_app(app)' + source_code = """ + {} + {} + """.format(create_db, create_redis) + return source_code.strip() + + def create_api(self): + if self.api in ('restful', 'class'): + return "app.register_blueprint(api.api_v1, url_prefix='/api/v1')" + else: + # TODO register dynamic api name + return "app.register_blueprint(api.root, url_prefix='/api/v1')" diff --git a/hermetica/scaffold/config.py b/hermetica/scaffold/config.py new file mode 100644 index 0000000..8783f97 --- /dev/null +++ b/hermetica/scaffold/config.py @@ -0,0 +1,56 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +config.py +scaffold create_config +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-01' +from textwrap import dedent +from inflector import Inflector + + +class Config(object): + """ Config Scaffold + """ + + def __init__(self, db=None, redis=None): + self.db = db + self.redis = redis + + def create_config(self, name='config', env='test'): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + + class {name}(object): + ENV = '{env}' + {db} + {redis} + """.format( + name=Inflector().camelize(name), + env=env, + db=self.create_sqlalchemy(), + redis=self.create_redis(), + ) + return dedent(source_code).strip() + + def create_sqlalchemy(self): + source_code = '' + if self.db == 'sqlalchemy': + source_code = """ + SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@db:3306/app_development?charset=utf8mb4' + SQLALCHEMY_TRACK_MODIFICATIONS = True + """ + if self.db == 'mongoengine': + source_code = """ + MONGODB_HOST = 'app_development' + MONGODB_PORT = 27017 + MONGODB_DB = 'root' + """ + return source_code + + def create_redis(self): + if self.redis == 'redis': + return "REDIS_URL = 'redis://:@redis:6379/0'" diff --git a/hermetica/scaffold/decorator.py b/hermetica/scaffold/decorator.py new file mode 100644 index 0000000..9868ebd --- /dev/null +++ b/hermetica/scaffold/decorator.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +decorator.py +scaffold decorator +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-24' +from textwrap import dedent +from inflector import Inflector + + +class Decorator(object): + """ Decorator Scaffold + """ + + def __init__(self, name=None): + self.name = name + + def create_decorators(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from functools import wraps + + def {name}(func): + @wraps(func) + def wrapper(*ar, **kw): + # something hear + return func(*ar, **kw) + return wrapper + """.format( + name=Inflector().underscore(self.name) + ) + return dedent(source_code).strip() + + def create_decorator(self): + source_code = """ + + def {name}(func): + @wraps(func) + def wrapper(*ar, **kw): + # something hear + return func(*ar, **kw) + return wrapper + """.format( + name=Inflector().underscore(self.name) + ) + return dedent(source_code) diff --git a/hermetica/scaffold/docker.py b/hermetica/scaffold/docker.py new file mode 100644 index 0000000..89f350d --- /dev/null +++ b/hermetica/scaffold/docker.py @@ -0,0 +1,110 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +docker.py +scaffold create_docker +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-01' +from textwrap import dedent + + +class Docker(object): + """ Docker Scaffold + """ + + def __init__(self, db=None, redis=None): + self.db = db + self.redis = redis + + def create_dockerfile(self): + source_code = """ + FROM python:3.6 + # -- Install Pipenv: + RUN pip install pipenv --upgrade + WORKDIR /tmp + # -- Adding Pipfiles + ADD ./Pipfile Pipfile + ADD ./Pipfile.lock Pipfile.lock + RUN pipenv install --deploy --system + """ + return dedent(source_code).strip() + + def create_docker_compose_yml(self): + source_code = """ + version: '3' + services: + api: + build: . + volumes: + - .:/app + working_dir: "/app" + environment: + FLASK_APP: "wsgi.py" + FLASK_DEBUG: "1" + command: "flask run --host=0.0.0.0" + ports: + - "5000:5000" + {links} + {db} + {redis} + """.format( + links=self.create_links(), + db=self.create_db(), + redis=self.create_redis(), + ) + return dedent(source_code).strip() + + def create_links(self): + if self.db and self.redis: + return """ + links: + - redis + - db + """.strip() + if self.db: + return """ + links: + - db + """.strip() + if self.redis: + return """ + links: + - redis + """.strip() + return '' + + def create_db(self): + source_code = '' + if self.db == 'sqlalchemy': + source_code = """ + db: + image: mysql:5.7 + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci + environment: + MYSQL_DATABASE: "app_development" + MYSQL_ROOT_PASSWORD: "root" + expose: + - "3306" + """ + if self.db == 'mongoengine': + source_code = """ + db: + image: mongo:latest + expose: + - "27017" + command: mongod --smallfiles --logpath=/dev/null + """ + return source_code.strip() + + def create_redis(self): + source_code = '' + if self.redis == 'redis': + source_code = """ + redis: + image: redis:3.2 + expose: + - "6379" + """ + return source_code.strip() diff --git a/hermetica/scaffold/extension.py b/hermetica/scaffold/extension.py new file mode 100644 index 0000000..d23bc5a --- /dev/null +++ b/hermetica/scaffold/extension.py @@ -0,0 +1,62 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +extension.py +scaffold create_extension +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-30' +from textwrap import dedent + + +class Extension(object): + """ Extension Scaffold + """ + + def __init__(self, db=None, redis=None): + self.db = db + self.redis = redis + + def create_extensions(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + {} + + {} + """.format( + self.create_header(), + self.create_instance(), + ) + return dedent(source_code).strip() + + def create_header(self): + import_db = '' + import_redis = '' + if self.db == 'sqlalchemy': + import_db = 'from flask_sqlalchemy import SQLAlchemy' + if self.db == 'mongoengine': + import_db = 'from flask_mongoengine import MongoEngine' + if self.redis == 'redis': + import_redis = 'from flask_redis import FlaskRedis' + header = """ + {} + {} + """.format(import_db, import_redis) + return header.strip() + + def create_instance(self): + instance_db = '' + instance_redis = '' + if self.db == 'sqlalchemy': + instance_db = 'db = SQLAlchemy()' + if self.db == 'mongoeingine': + instance_db = 'db = MongoEngine()' + if self.redis: + instance_redis = 'redis = FlaskRedis()' + instance = """ + {} + {} + """.format(instance_db, instance_redis) + return instance.strip() diff --git a/hermetica/scaffold/model.py b/hermetica/scaffold/model.py new file mode 100644 index 0000000..0665d41 --- /dev/null +++ b/hermetica/scaffold/model.py @@ -0,0 +1,101 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +model.py +scaffold model +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-27' +from textwrap import dedent +from inflector import Inflector + + +class Model(object): + """ Model Scaffold + """ + + def __init__(self, db=None, name=None): + self.db = db + self.name = name + + def create__init__(self): + if self.db == 'sqlalchemy': + return self.create_sqlalchemy__init__() + if self.db == 'mongoengine': + return self.create_mongoengine__init__() + + def create_model(self): + if self.db == 'sqlalchemy': + return self.create_sqlalchmey_model() + if self.db == 'mongoengine': + return self.create_mongoengine_model() + + def create_sqlalchemy__init__(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from datetime import datetime + from sqlalchemy.ext.declarative import declared_attr + from app.extensions import db + + + class Model(db.Model): + __abstract__ = True + + id = db.Column(db.Integer, primary_key=True) + + @declared_attr + def created_at(cls): + return db.Column(db.DateTime, default=datetime.utcnow) + + @declared_attr + def updated_at(cls): + return db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + """ + return dedent(source_code).strip() + + def create_sqlalchmey_model(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from app.models import Model + + class {name}(Model): + pass + """.format( + name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_mongoengine__init__(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from datetime import datetime + from mongoengine import Document, DateTimeField + + class Model(Document): + created_at = DateTimeField() + updated_at = DateTimeField(default=datetime.datetime.now) + + def save(self, *args, **kwargs): + if not self.created_at: + self.created_at = datetime.datetime.now() + self.updated_at = datetime.datetime.now() + return super(Model, self).save(*args, **kwargs) + """ + return dedent(source_code).strip() + + def create_mongoengine__model(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from app.models import Model + + class {name}(Model): + pass + """.format( + name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() diff --git a/hermetica/scaffold/pipfile.py b/hermetica/scaffold/pipfile.py new file mode 100644 index 0000000..1572327 --- /dev/null +++ b/hermetica/scaffold/pipfile.py @@ -0,0 +1,92 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +pipfile.py +scaffold pipfile +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-09' +from os import system +from textwrap import dedent + + +class Pipfile(object): + """ Pipfile Scaffold + """ + + def __init__(self, db=None, redis=None): + self.db = db + self.redis = redis + + def lock(self): + system('pipenv lock') + + def create_pipfile(self): + source_code = """ + [[source]] + + url = "https://pypi.python.org/simple" + verify_ssl = true + name = "pypi" + + [dev-packages] + {nose} + + [packages] + {flask} + {db} + {redis} + + [requires] + + python_version = "3.6" + """.format( + flask=self.create_flask(), + nose=self.create_nose(), + db=self.create_db(), + redis=self.create_redis() + ) + return dedent(source_code).strip() + + def create_flask(self): + source_code = """ + flask = "*" + flask-restful = "*" + gunicorn = "*" + """ + return source_code.strip() + + def create_nose(self): + source_code = """ + nose = "*" + coverage = "*" + rednose = "*" + nose-timer = "*" + factory-boy = "*" + """ + return source_code.strip() + + def create_db(self): + source_code = '' + if self.db == 'sqlalchemy': + source_code = """ + pymysql = "*" + flask-sqlalchemy = "*" + flask-migrate = "*" + """ + if self.db == 'mongoengine': + source_code = """ + pymongo = "*" + mongoengine = "*" + """ + return source_code.strip() + + def create_redis(self): + source_code = '' + if self.redis == 'redis': + source_code = """ + redis = "*" + flask-redis = "*" + """ + return source_code.strip() diff --git a/hermetica/scaffold/test.py b/hermetica/scaffold/test.py new file mode 100644 index 0000000..ab19238 --- /dev/null +++ b/hermetica/scaffold/test.py @@ -0,0 +1,176 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +test.py +Test Scaffold +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-27' +from textwrap import dedent +from inflector import Inflector + +class Test(object): + + def __init__(self, name=None, db=None): + self.db =db + self.name = name + + def create__init__(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + import json + from contextlib import contextmanager + from unittest import TestCase + from app import create_app + + app = create_app() + + class Experiment(TestCase): + + def setUp(self): + app.testing = True + self.client = app.test_client() + self.app_config = app.config + self.app_context = app.app_context() + self.app_context.push() + + def tearDown(self): + self.app_context.pop() + + def response_to_dict(self, response): + return json.loads(response.data) + + @contextmanager + def authenticate(self, user=None): + pass + """ + return dedent(source_code).strip() + + def create_nose_cfg(self): + source_code = """ + [nosetests] + verbosity=2 + with-timer=1 + rednose=True + nocapture=True + with-coverage=1 + cover-package=. + """ + return dedent(source_code).strip() + + def create_api_test(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from test import Experiment + from test.factories.{name}_factory import {Name}Factory + from app.models.{name} import {Name} + + class {Name}APITest(Experiment): + + def setUp(self): + super().setUp() + {Name}Factory.create_batch(5) + + def test_get_list_200(self): + {name} = {Name}.query.first() + url = "/api/v1/{names}" + response = self.client.get(url, data={}) + assert response.status_code == 200 + + def test_get_200(self): + {name} = {Name}.query.first() + url = "/api/v1/{names}/{{}}".format({name}.id) + response = self.client.get(url, data={}) + assert response.status_code == 200 + + def test_post_201(self): + url = "/api/v1/{names}" + response = self.client.post(url, data={}) + assert response.status_code == 201 + + def test_put_204(self): + {name} = {Name}.query.first() + url = "/api/v1/{names}/{{}}".format({name}.id) + response = self.client.put(url, data={}) + assert response.status_code == 204 + + def test_delete_204(self): + {name} = {Name}.query.first() + url = "/api/v1/{names}/{{}}".format({name}.id) + response = self.client.delete(url, data={}) + assert response.status_code == 204 + """.format( + name=Inflector().underscore(self.name), + names=Inflector().pluralize(self.name), + Name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_model_test(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from test import Experiment + from test.factories.{name}_factory import {Name}Factory + from app.models.{name} import {Name} + + class {Name}ModelTest(Experiment): + + def setUp(self): + {Name}Factory.create_batch(5) + + def something(self): + pass + """.format( + name=Inflector().underscore(self.name), + Name=Inflector().camelize(self.name) + ) + return dedent(source_code).strip() + + def create_factoryboy(self): + if self.db == 'sqlalchemy': + return self.create_sqlalchemy_factoryboy() + if self.db == 'mongoengine': + return self.create_mongoengine_factoryboy() + return '' + + def create_sqlalchemy_factoryboy(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from factory import Sequence, SubFactory, Iterator, fuzzy + from factory.alchemy import SQLAlchemyModelFactory + from app.extensions import db + from app.models.{name} import {Name} + + class {Name}Factory(SQLAlchemyModelFactory): + class Meta: + model = {Name} + sqlalchemy_session = db.session + + id = Sequence(lambda n: n+1) + """.format( + name=self.name, + Name=Inflector().camelize(self.name), + ) + return dedent(source_code).strip() + + def create_mongoengine_factoryboy(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + from factory import Sequence, SubFactory, Iterator, fuzzy + from factory.mongoengine import MongoEngineFactory + from app.models.{name} import {Name} + + class {Name}Factory(MongoEngineFactory): + class Meta: + model = {Name} + """.format( + name=self.name, + Name=Inflector().camelize(self.name), + ) + return dedent(source_code).strip() diff --git a/hermetica/scaffold/wsgi.py b/hermetica/scaffold/wsgi.py new file mode 100644 index 0000000..908bb05 --- /dev/null +++ b/hermetica/scaffold/wsgi.py @@ -0,0 +1,47 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +wsgi.py +scaffold create_wsgi +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-05-01' +from textwrap import dedent + + +class WSGI(object): + """ wsgi Scaffold + """ + + def __init__(self, db): + self.db = db + + def create_wsgi(self): + source_code = """ + #! /usr/bin/env python3 + # -*- encoding: utf-8 -*- + import os + {header} + + app = create_app(os.getenv('FLASK_ENV', None)) + {migrate} + + if __name__ == '__main__': + app.run(host='0.0.0.0') + """.format( + header=self.create_header(), + migrate=self.create_migrate() + ) + + return dedent(source_code).strip() + + def create_header(self): + if self.db == 'sqlalchemy': + return 'from app import create_app, db' + return 'from app import create_app' + + def create_migrate(self): + if self.db == 'sqlalchemy': + return 'migrate = Migrate(app, db)' + return '' diff --git a/methodview.py b/methodview.py deleted file mode 100644 index 301dee9..0000000 --- a/methodview.py +++ /dev/null @@ -1,32 +0,0 @@ -from flask.views import MethodView -from flask import Flask - -app = Flask(__name__) - -def middleware(func): - def deco(*args, **kwargs): - print('middleware') - return func(*args, **kwargs) - return deco - -class Resource(MethodView): - decorators = [middleware] - - def get(self, id=None): - if not id: return 'index' - return 'show' - - def post(self): - return 'create' - - def put(self, id): - return 'update' - - def delete(self, id): - return 'delete' - -resource_view = Resource.as_view('resource') -app.add_url_rule('/resource', view_func=resource_view, methods=['GET', 'POST']) -app.add_url_rule('/resource/', view_func=resource_view, methods=['GET', 'PUT', 'DELETE']) - -app.run() diff --git a/sample_app/app/__init__.py b/sample_app/app/__init__.py deleted file mode 100644 index 60ab885..0000000 --- a/sample_app/app/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -#! /usr/bin/env python3 -# -*- encoding: utf-8 -*- -""" -__init__.py -flask app -""" -__author__ = 'Yoshiya Ito ' -__version__ = '0.0.1' -__date__ = '30 03 2016' - -from flask import Flask -from flask.ext.mongoengine import MongoEngine -from flask.ext.restful import Api -from redis import Redis - -sample = Flask(__name__) -redis = Redis() -mongo = MongoEngine(sample) -api = Api(sample) - -import app.views diff --git a/sample_app/app/models/user.py b/sample_app/app/models/user.py deleted file mode 100644 index 0d51cc9..0000000 --- a/sample_app/app/models/user.py +++ /dev/null @@ -1,17 +0,0 @@ -#! /usr/bin/env python3 -# -*- encoding: utf-8 -*- -""" -user -User モデル -""" -__author__ = 'Yoshiya Ito ' -__version__ = '0.0.1' -__date__ = '30 03 2016' - -from app import mongo - - -class User(mongo.Document): - name = mongo.StringField() - email = mongo.StringField() - password = mongo.StringField() diff --git a/sample_app/app/views/__init__.py b/sample_app/app/views/__init__.py deleted file mode 100644 index 967aed1..0000000 --- a/sample_app/app/views/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env python3 -# -*- encoding: utf-8 -*- -""" -__init__.py -api マッピング -""" -__author__ = 'Yoshiya Ito ' -__version__ = '0.0.1' -__date__ = '30 03 2016' - -from app import api -from app.views.user import UserView - -api.add_resource( - UserView, - '/api/v1/users', - '/api/v1/users/' -) diff --git a/sample_app/app/views/user.py b/sample_app/app/views/user.py deleted file mode 100644 index 95bb464..0000000 --- a/sample_app/app/views/user.py +++ /dev/null @@ -1,30 +0,0 @@ -#! /usr/bin/env python3 -# -*- encoding: utf-8 -*- -""" -user.py -user api -""" -__author__ = 'Yoshiya Ito ' -__version__ = '0.0.1' -__date__ = '30 03 2016' - - -from flask.ext.restful import Resource -from app.models.user import User - - -class UserView(Resource): - - def get(self, id=None): - l = User.objects() - print(l) - return 'get' - - def post(self): - return 'post' - - def put(self, id): - return 'put' - - def delete(self, id): - return 'delete' diff --git a/sample_app/server.py b/sample_app/server.py deleted file mode 100644 index 7388651..0000000 --- a/sample_app/server.py +++ /dev/null @@ -1,13 +0,0 @@ -#! /usr/bin/env python3 -# -*- encoding: utf-8 -*- -""" -server.py -サーバ起動スクリプト -""" -__author__ = 'Yoshiya Ito ' -__version__ = '0.0.1' -__date__ = '30 03 2016' - -from app import sample - -sample.run() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4b83f8a --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +#! /usr/bin/env python3 +# -*- encoding: utf-8 -*- +""" +setup.py +setup script +""" +__author__ = 'Yoshiya Ito ' +__version__ = '1.0.0' +__date__ = '2018-04-24' +from setuptools import setup + +#with open('README.md', 'rt', encoding='utf8') as f: +# readme = f.read() + +setup( + name='Hermetica', + version='1.0.0', + description='scaffold command line interface for Flask application', + #long_description=readme, + author='Yoshiya Ito', + author_email='myon53@gmail.com', + url='https://github.com/yoshiya0503/Flask-Best-Practices.git', + license='MIT', + platforms='any', + packages=['hermetica', 'hermetica.scaffold'], + install_requires=[ + 'click>=5.1', + 'Inflector>=2.0', + ], + classifiers=[ + 'Environment :: Web Environment', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + entry_points=''' + [console_scripts] + hermetica=hermetica.cli:main + ''' +) diff --git a/test.py b/test.py deleted file mode 100644 index b7b89be..0000000 --- a/test.py +++ /dev/null @@ -1,29 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - -@app.before_request -def logging_before_request(): - print('before request') - -@app.after_request -def logging_after_request(res): - """ レスポンスが取れる - res オブジェクトを返す必要がある - """ - print('after request') - return res - -@app.teardown_request -def logging_end_of_request(exc): - """ 例外が取れる - """ - print('end_request') - -@app.route('/') -def hello(): - print('request') - return 'Hello World!' - -if __name__ == '__main__': - app.run()