[
  {
    "path": ".github/workflows/install_lint_test.yml",
    "content": "name: Python application\n\non: [push, pull_request]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python 3.7\n      uses: actions/setup-python@v1\n      with:\n        python-version: 3.7\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install -r requirements.txt\n        pip install -r requirements_test.txt\n    - name: Lint with flake8\n      run: |\n        pip install flake8\n        # stop the build if there are Python syntax errors or undefined names\n        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics\n        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n        flake8 pydaria/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics\n    - name: Test with pytest\n      run: |\n        pytest pydaria/tests -v\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n*.db\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n.PHONY: all clean install test \n\nhelp:\n\t@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ \"^[#.]\") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'\n\nall: clean install test \n\ntest:\n\tpytest pydaria/tests -v\n\ninstall:\n\tpip install --upgrade pip\n\tpip install -r requirements.txt\n\tpip install -r requirements_dev.txt\n\tpip install -r requirements_test.txt\n\nclean:\n\t@find . -name '*.pyc' -exec rm -rf {} \\;\n\t@find . -name '__pycache__' -exec rm -rf {} \\;\n\t@find . -name 'Thumbs.db' -exec rm -rf {} \\;\n\t@find . -name '*~' -exec rm -rf {} \\;\n\trm -rf .cache\n\trm -rf build\n\trm -rf dist\n\trm -rf *.egg-info\n\trm -rf htmlcov\n\trm -rf .tox/\n\trm -rf docs/_build\n"
  },
  {
    "path": "README.md",
    "content": "# Arquitetura Definitiva para Projetos Flask\n\nTutorial em texto em: https://codeshow.com.br/arquitetura-web-python-flask/\n\nVideo: https://youtu.be/-qWySnuoaTM\nSlides: http://bit.ly/codeshow-003-arquitetura-flask ou [googledrive](https://docs.google.com/presentation/d/e/2PACX-1vTZfj2xF3-Nf4NZO8V4HNr2rQNt0ci2kP19OT3Uhrzljl7MZj5Txl_AVlNt4upnCl3aYEJDAfiELpd7/pub?start=false&loop=false&delayms=15000)\n\nCódigo parte da apresentação na https://pyjamas.live conference\n\n---\n\n## Clone\n\n```bash\ngit clone https://github.com/codeshow/003-arquitetura-flask.git\n```\n\n## ou faça o download\n\nhttps://github.com/codeshow/003-arquitetura-flask/archive/new.zip\n\nou\n\n```bash\nwget https://github.com/codeshow/003-arquitetura-flask/archive/new.zip\n```\n\n## Ambiente\n\nPython 3.6+\nAtive a sua virtualenv\n\n```bash\npip install -r requirements.txt\npip install -r requirements_dev.txt\npip install -r requirements_test.txt\n```\n\n## Testando\n\n```bash\npytest pydaria/tests\n```\n\n## Executando\n\n```bash\nflask create-db  # rodar uma vez\nflask populate-db # rodar uma vez\nflask add-user -u admin -p 1234  # adiciona usuario admin\nflask run\n```\n\nAcesse:\n\n- Website: http://localhost:5000\n- Admin: http://localhost:5000/admin/\n  - user: admin, senha: 1234\n- API GET:\n  - https://localhost:5000/api/v1/product/\n  - https://localhost:5000/api/v1/product/1\n  - https://localhost:5000/api/v1/product/2\n  - https://localhost:5000/api/v1/product/3\n\n\n## Structure\n\n```bash\n.\n├── Makefile\n├── pydaria  (MAIN PACKAGE)\n│   ├── app.py  (APP FACTORIES)\n│   ├── blueprints  (BLUEPRINT FACTORIES)\n│   │   ├── __init__.py\n│   │   ├── restapi  (REST API)\n│   │   │   ├── __init__.py\n│   │   │   └── resources.py\n│   │   └── webui  (FRONT END)\n│   │       ├── __init__.py\n│   │       ├── templates\n│   │       │   ├── index.html\n│   │       │   └── product.html\n│   │       └── views.py\n│   ├── ext (EXTENSION FACTORIES)\n│   │   ├── admin.py\n│   │   ├── appearance.py\n│   │   ├── auth.py\n│   │   ├── commands.py\n│   │   ├── configuration.py\n│   │   ├── database.py\n│   │   └── __init__.py\n│   ├── __init__.py\n│   ├── models.py  (DATABASE MODELS)\n│   └── tests  (TESTS)\n│       ├── conftest.py\n│       ├── __init__.py\n│       └── test_api.py\n├── README.md\n├── requirements_dev.txt\n├── requirements_test.txt\n├── requirements.txt\n└── settings.toml  (SETTINGS)\n\n7 directories, 26 files\n```\n"
  },
  {
    "path": "pydaria/__init__.py",
    "content": ""
  },
  {
    "path": "pydaria/app.py",
    "content": "from flask import Flask\n\nfrom pydaria.ext import configuration\n\n\ndef minimal_app(**config):\n    app = Flask(__name__)\n    configuration.init_app(app, **config)\n    return app\n\n\ndef create_app(**config):\n    app = minimal_app(**config)\n    configuration.load_extensions(app)\n    return app\n"
  },
  {
    "path": "pydaria/blueprints/__init__.py",
    "content": ""
  },
  {
    "path": "pydaria/blueprints/restapi/__init__.py",
    "content": "from flask import Blueprint\nfrom flask_restful import Api\n\nfrom .resources import ProductItemResource, ProductResource\n\nbp = Blueprint(\"restapi\", __name__, url_prefix=\"/api/v1\")\napi = Api(bp)\n\n\ndef init_app(app):\n    api.add_resource(ProductResource, \"/product/\")\n    api.add_resource(ProductItemResource, \"/product/<product_id>\")\n    app.register_blueprint(bp)\n"
  },
  {
    "path": "pydaria/blueprints/restapi/resources.py",
    "content": "from flask import abort, jsonify\nfrom flask_restful import Resource\n\nfrom pydaria.models import Product\n\n\nclass ProductResource(Resource):\n    def get(self):\n        products = Product.query.all() or abort(204)\n        return jsonify(\n            {\"products\": [product.to_dict() for product in products]}\n        )\n\n\nclass ProductItemResource(Resource):\n    def get(self, product_id):\n        product = Product.query.filter_by(id=product_id).first() or abort(404)\n        return jsonify(product.to_dict())\n"
  },
  {
    "path": "pydaria/blueprints/webui/__init__.py",
    "content": "from flask import Blueprint\n\nfrom .views import index, product\n\nbp = Blueprint(\"webui\", __name__, template_folder=\"templates\")\n\nbp.add_url_rule(\"/\", view_func=index)\nbp.add_url_rule(\n    \"/product/<product_id>\", view_func=product, endpoint=\"productview\"\n)\n\n\ndef init_app(app):\n    app.register_blueprint(bp)\n"
  },
  {
    "path": "pydaria/blueprints/webui/templates/index.html",
    "content": "{% extends \"bootstrap/base.html\" %}\n{% block title %}{{config.get('TITLE')}}{% endblock %}\n\n{% block navbar %}\n<div class=\"navbar\">\n<div class=\"navbar-header\">\n    <a class=\"navbar-brand\" href=\"#\">\n    <img alt=\"Brand\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAB+0lEQVR4AcyYg5LkUBhG+1X2PdZGaW3btm3btm3bHttWrPomd1r/2Jn/VJ02TpxcH4CQ/dsuazWgzbIdrm9dZVd4pBz4zx2igTaFHrhvjneVXNHCSqIlFEjiwMyyyOBilRgGSqLNF1jnwNQdIvAt48C3IlBmHCiLQHC2zoHDu6zG1iXn6+y62ScxY9AODO6w0pvAqf23oSE4joOfH6OxfMoRnoGUm+de8wykbFt6wZtA07QwtNOqKh3ZbS3Wzz2F+1c/QJY0UCJ/J3kXWJfv7VhxCRRV1jGw7XI+gcO7rEFFRvdYxydwcPsVsC0bQdKScngt4iUTD4Fy/8p7PoHzRu1DclwmgmiqgUXjD3oTKHbAt869qdJ7l98jNTEblPTkXMwetpvnftA0LLHb4X8kiY9Kx6Q+W7wJtG0HR7fdrtYz+x7iya0vkEtUULIzCjC21wY+W/GYXusRH5kGytWTLxgEEhePPwhKYb7EK3BQuxWwTBuUkd3X8goUn6fMHLyTT+DCsQdAEXNzSMeVPAJHdF2DmH8poCREp3uwm7HsGq9J9q69iuunX6EgrwQVObjpBt8z6rdPfvE8kiiyhsvHnomrQx6BxYUyYiNS8f75H1w4/ISepDZLoDhNJ9cdNUquhRsv+6EP9oNH7Iff2A9g8h8CLt1gH0Qf9NMQAFnO60BJFQe0AAAAAElFTkSuQmCC\">\n    </a>\n</div>\n</div>\n{% endblock %}\n\n{% block content %}\n<div class=\"container\">\n<h1>{{config.get('TITLE')}}</h1>\n\n<div class=\"jumbotron\">\n<ul class=\"list-group\">\n    {% for product in products %}\n    <li class=\"list-group-item\">\n        <a href=\"{{url_for('webui.productview', product_id=product.id)}}\">{{product.name}}- {{ \"%0.2f\" | format(product.price)}}</a>\n    </li>\n    {% endfor %}\n</ul>\n</div>\n\n</div>\n{% endblock %}\n\n\n\n"
  },
  {
    "path": "pydaria/blueprints/webui/templates/product.html",
    "content": "{% extends \"index.html\" %}\n{% block content %}\n<div class=\"container\">\n<h1>{{ product.name }}</h1>\n\n<div class=\"jumbotron\">\n<h2>R$ {{ \"%0.2f\" | format(product.price)}}</h2>\n<p>\n    {{product.description}}\n</p>\n</div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "pydaria/blueprints/webui/views.py",
    "content": "from flask import abort, render_template\nfrom pydaria.models import Product\n\n\ndef index():\n    products = Product.query.all()\n    return render_template(\"index.html\", products=products)\n\n\ndef product(product_id):\n    product = Product.query.filter_by(id=product_id).first() or abort(\n        404, \"produto nao encontrado\"\n    )\n    return render_template(\"product.html\", product=product)\n"
  },
  {
    "path": "pydaria/ext/__init__.py",
    "content": ""
  },
  {
    "path": "pydaria/ext/admin.py",
    "content": "from flask_admin import Admin\nfrom flask_admin.base import AdminIndexView\nfrom flask_admin.contrib import sqla\nfrom flask_simplelogin import login_required\nfrom werkzeug.security import generate_password_hash\n\nfrom pydaria.ext.database import db\nfrom pydaria.models import Product, User\n\n# Proteger o admin com login via Monkey Patch\nAdminIndexView._handle_view = login_required(AdminIndexView._handle_view)\nsqla.ModelView._handle_view = login_required(sqla.ModelView._handle_view)\nadmin = Admin()\n\n\nclass UserAdmin(sqla.ModelView):\n    column_list = ['username']\n    can_edit = False\n\n    def on_model_change(self, form, model, is_created):\n        model.password = generate_password_hash(model.password)\n\n\ndef init_app(app):\n    admin.name = app.config.TITLE\n    admin.template_mode = \"bootstrap3\"\n    admin.init_app(app)\n    admin.add_view(sqla.ModelView(Product, db.session))\n    admin.add_view(UserAdmin(User, db.session))\n"
  },
  {
    "path": "pydaria/ext/appearance.py",
    "content": "from flask_bootstrap import Bootstrap\n\n\ndef init_app(app):\n    Bootstrap(app)\n"
  },
  {
    "path": "pydaria/ext/auth.py",
    "content": "from flask_simplelogin import SimpleLogin\nfrom werkzeug.security import check_password_hash, generate_password_hash\nfrom pydaria.ext.database import db\nfrom pydaria.models import User\n\n\ndef verify_login(user):\n    \"\"\"Valida o usuario e senha para efetuar o login\"\"\"\n    username = user.get('username')\n    password = user.get('password')\n    if not username or not password:\n        return False\n    existing_user = User.query.filter_by(username=username).first()\n    if not existing_user:\n        return False\n    if check_password_hash(existing_user.password, password):\n        return True\n    return False\n\n\ndef create_user(username, password):\n    \"\"\"Registra um novo usuario caso nao esteja cadastrado\"\"\"\n    if User.query.filter_by(username=username).first():\n        raise RuntimeError(f'{username} ja esta cadastrado')\n    user = User(username=username, password=generate_password_hash(password))\n    db.session.add(user)\n    db.session.commit()\n    return user\n\n\ndef init_app(app):\n    SimpleLogin(app, login_checker=verify_login)\n"
  },
  {
    "path": "pydaria/ext/commands.py",
    "content": "import click\nfrom pydaria.ext.database import db\nfrom pydaria.ext.auth import create_user\nfrom pydaria.models import Product\n\n\ndef create_db():\n    \"\"\"Creates database\"\"\"\n    db.create_all()\n\n\ndef drop_db():\n    \"\"\"Cleans database\"\"\"\n    db.drop_all()\n\n\ndef populate_db():\n    \"\"\"Populate db with sample data\"\"\"\n    data = [\n        Product(\n            id=1, name=\"Ciabatta\", price=\"10\", description=\"Italian Bread\"\n        ),\n        Product(id=2, name=\"Baguete\", price=\"15\", description=\"French Bread\"),\n        Product(id=3, name=\"Pretzel\", price=\"20\", description=\"German Bread\"),\n    ]\n    db.session.bulk_save_objects(data)\n    db.session.commit()\n    return Product.query.all()\n\n\ndef init_app(app):\n    # add multiple commands in a bulk\n    for command in [create_db, drop_db, populate_db]:\n        app.cli.add_command(app.cli.command()(command))\n\n    # add a single command\n    @app.cli.command()\n    @click.option('--username', '-u')\n    @click.option('--password', '-p')\n    def add_user(username, password):\n        \"\"\"Adds a new user to the database\"\"\"\n        return create_user(username, password)\n"
  },
  {
    "path": "pydaria/ext/configuration.py",
    "content": "from importlib import import_module\n\nfrom dynaconf import FlaskDynaconf\n\n\ndef load_extensions(app):\n    for extension in app.config.EXTENSIONS:\n        # Split data in form `extension.path:factory_function`\n        module_name, factory = extension.split(\":\")\n        # Dynamically import extension module.\n        ext = import_module(module_name)\n        # Invoke factory passing app.\n        getattr(ext, factory)(app)\n\n\ndef init_app(app, **config):\n    FlaskDynaconf(app, **config)\n"
  },
  {
    "path": "pydaria/ext/database.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n\n\ndef init_app(app):\n    db.init_app(app)\n"
  },
  {
    "path": "pydaria/models.py",
    "content": "from pydaria.ext.database import db\nfrom sqlalchemy_serializer import SerializerMixin\n\n\nclass Product(db.Model, SerializerMixin):\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(140))\n    price = db.Column(db.Numeric())\n    description = db.Column(db.Text)\n\n\nclass User(db.Model, SerializerMixin):\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(140))\n    password = db.Column(db.String(512))\n"
  },
  {
    "path": "pydaria/tests/__init__.py",
    "content": ""
  },
  {
    "path": "pydaria/tests/conftest.py",
    "content": "import pytest\n\nfrom pydaria.app import create_app, minimal_app\nfrom pydaria.ext.commands import populate_db\nfrom pydaria.ext.database import db\n\n\n@pytest.fixture(scope=\"session\")\ndef min_app():\n    app = minimal_app(FORCE_ENV_FOR_DYNACONF=\"testing\")\n    return app\n\n\n@pytest.fixture(scope=\"session\")\ndef app():\n    app = create_app(FORCE_ENV_FOR_DYNACONF=\"testing\")\n    with app.app_context():\n        db.create_all(app=app)\n        yield app\n        db.drop_all(app=app)\n\n\n@pytest.fixture(scope=\"session\")\ndef products(app):\n    with app.app_context():\n        return populate_db()\n"
  },
  {
    "path": "pydaria/tests/test_api.py",
    "content": "from decimal import Decimal\n\n\ndef test_products_get_all(client, products):  # Arrange\n    \"\"\"Test get all products\"\"\"\n    # Act\n    response = client.get(\"/api/v1/product/\")\n    # Assert\n    assert response.status_code == 200\n    data = response.json[\"products\"]\n    assert len(data) == 3\n    for product in products:\n        assert product.id in [item[\"id\"] for item in data]\n        assert product.name in [item[\"name\"] for item in data]\n        assert product.price in [Decimal(item[\"price\"]) for item in data]\n\n\ndef test_products_get_one(client, products):  # Arrange\n    \"\"\"Test get all products\"\"\"\n    for product in products:\n        # Act\n        response = client.get(f\"/api/v1/product/{product.id}\")\n        data = response.json\n        # Assert\n        assert response.status_code == 200\n        assert data[\"name\"] == product.name\n        assert Decimal(data[\"price\"]) == product.price\n        assert data[\"description\"] == product.description\n"
  },
  {
    "path": "requirements.txt",
    "content": "dynaconf==2.2.1\nFlask-Admin==1.5.4\nFlask-Bootstrap==3.3.7.1\nFlask-RESTful==0.3.7\nflask-shell-ipython==0.4.1\nflask-simplelogin==0.0.7\nFlask-SQLAlchemy==2.4.1\nFlask==1.1.1\nSQLAlchemy-serializer==1.3.4.2\n"
  },
  {
    "path": "requirements_dev.txt",
    "content": "flask-debugtoolbar\nflake8\nblack\nisort\nipdb\n"
  },
  {
    "path": "requirements_test.txt",
    "content": "pytest-flask\n"
  },
  {
    "path": "settings.toml",
    "content": "[default]\nDEBUG = false\nFLASK_ADMIN_NAME = \"Pydaria Admin\"\nFLASK_ADMIN_TEMPLATE_MODE = \"bootstrap3\"\nFLASK_ADMIN_SWATCH = 'cerulean'\nSQLALCHEMY_DATABASE_URI = 'sqlite:///development.db'\nTITLE = \"Pydaria\"\nSECRET_KEY = \"jadkfbsdkjbfbh\"\nPASSWORD_SCHEMES = ['pbkdf2_sha512', 'md5_crypt']\nEXTENSIONS = [\n    \"pydaria.ext.appearance:init_app\",\n    \"pydaria.ext.database:init_app\",\n    \"pydaria.ext.auth:init_app\",\n    \"pydaria.ext.admin:init_app\",\n    \"pydaria.ext.commands:init_app\",\n    \"pydaria.blueprints.webui:init_app\",\n    \"pydaria.blueprints.restapi:init_app\",\n]\n\n\n[development]\nEXTENSIONS = [\n    \"flask_debugtoolbar:DebugToolbarExtension\",\n    \"dynaconf_merge_unique\"  # to reuse extensions list from [default]\n]\nTEMPLATES_AUTO_RELOAD = true\nDEBUG = true\nDEBUG_TOOLBAR_ENABLED = true\nDEBUG_TB_INTERCEPT_REDIRECTS = false\nDEBUG_TB_PROFILER_ENABLED = true\nDEBUG_TB_TEMPLATE_EDITOR_ENABLED = true\nDEBUG_TB_PANELS = [\n    \"flask_debugtoolbar.panels.versions.VersionDebugPanel\",\n    \"flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel\",\n    \"flask_debugtoolbar.panels.timer.TimerDebugPanel\",\n    \"flask_debugtoolbar.panels.headers.HeaderDebugPanel\",\n    \"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel\",\n    \"flask_debugtoolbar.panels.template.TemplateDebugPanel\",\n    \"flask_debugtoolbar.panels.route_list.RouteListDebugPanel\",\n    \"flask_debugtoolbar.panels.logger.LoggingPanel\",\n    \"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel\",\n    \"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel\"\n]\n\n[testing]\nSQLALCHEMY_DATABASE_URI = 'sqlite:///testing.db'\n\n[production]\nSQLALCHEMY_DATABASE_URI = 'sqlite:///production.db'\n"
  }
]