Repository: codeshow/003-arquitetura-flask Branch: new Commit: ced8e41faff6 Files: 28 Total size: 16.0 KB Directory structure: gitextract_wj20g4fr/ ├── .github/ │ └── workflows/ │ └── install_lint_test.yml ├── .gitignore ├── Makefile ├── README.md ├── pydaria/ │ ├── __init__.py │ ├── app.py │ ├── blueprints/ │ │ ├── __init__.py │ │ ├── restapi/ │ │ │ ├── __init__.py │ │ │ └── resources.py │ │ └── webui/ │ │ ├── __init__.py │ │ ├── templates/ │ │ │ ├── index.html │ │ │ └── product.html │ │ └── views.py │ ├── ext/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── appearance.py │ │ ├── auth.py │ │ ├── commands.py │ │ ├── configuration.py │ │ └── database.py │ ├── models.py │ └── tests/ │ ├── __init__.py │ ├── conftest.py │ └── test_api.py ├── requirements.txt ├── requirements_dev.txt ├── requirements_test.txt └── settings.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/install_lint_test.yml ================================================ name: Python application on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_test.txt - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 pydaria/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest pydaria/tests -v ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 *.db # 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 # Environments .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ ================================================ FILE: Makefile ================================================ SHELL := /bin/bash .PHONY: all clean install test help: @$(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 '^$@$$' all: clean install test test: pytest pydaria/tests -v install: pip install --upgrade pip pip install -r requirements.txt pip install -r requirements_dev.txt pip install -r requirements_test.txt clean: @find . -name '*.pyc' -exec rm -rf {} \; @find . -name '__pycache__' -exec rm -rf {} \; @find . -name 'Thumbs.db' -exec rm -rf {} \; @find . -name '*~' -exec rm -rf {} \; rm -rf .cache rm -rf build rm -rf dist rm -rf *.egg-info rm -rf htmlcov rm -rf .tox/ rm -rf docs/_build ================================================ FILE: README.md ================================================ # Arquitetura Definitiva para Projetos Flask Tutorial em texto em: https://codeshow.com.br/arquitetura-web-python-flask/ Video: https://youtu.be/-qWySnuoaTM Slides: 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) Código parte da apresentação na https://pyjamas.live conference --- ## Clone ```bash git clone https://github.com/codeshow/003-arquitetura-flask.git ``` ## ou faça o download https://github.com/codeshow/003-arquitetura-flask/archive/new.zip ou ```bash wget https://github.com/codeshow/003-arquitetura-flask/archive/new.zip ``` ## Ambiente Python 3.6+ Ative a sua virtualenv ```bash pip install -r requirements.txt pip install -r requirements_dev.txt pip install -r requirements_test.txt ``` ## Testando ```bash pytest pydaria/tests ``` ## Executando ```bash flask create-db # rodar uma vez flask populate-db # rodar uma vez flask add-user -u admin -p 1234 # adiciona usuario admin flask run ``` Acesse: - Website: http://localhost:5000 - Admin: http://localhost:5000/admin/ - user: admin, senha: 1234 - API GET: - https://localhost:5000/api/v1/product/ - https://localhost:5000/api/v1/product/1 - https://localhost:5000/api/v1/product/2 - https://localhost:5000/api/v1/product/3 ## Structure ```bash . ├── Makefile ├── pydaria (MAIN PACKAGE) │   ├── app.py (APP FACTORIES) │   ├── blueprints (BLUEPRINT FACTORIES) │   │   ├── __init__.py │   │   ├── restapi (REST API) │   │   │   ├── __init__.py │   │   │   └── resources.py │   │   └── webui (FRONT END) │   │   ├── __init__.py │   │   ├── templates │   │   │   ├── index.html │   │   │   └── product.html │   │   └── views.py │   ├── ext (EXTENSION FACTORIES) │   │   ├── admin.py │   │   ├── appearance.py │   │   ├── auth.py │   │   ├── commands.py │   │   ├── configuration.py │   │   ├── database.py │   │   └── __init__.py │   ├── __init__.py │   ├── models.py (DATABASE MODELS) │   └── tests (TESTS) │   ├── conftest.py │   ├── __init__.py │   └── test_api.py ├── README.md ├── requirements_dev.txt ├── requirements_test.txt ├── requirements.txt └── settings.toml (SETTINGS) 7 directories, 26 files ``` ================================================ FILE: pydaria/__init__.py ================================================ ================================================ FILE: pydaria/app.py ================================================ from flask import Flask from pydaria.ext import configuration def minimal_app(**config): app = Flask(__name__) configuration.init_app(app, **config) return app def create_app(**config): app = minimal_app(**config) configuration.load_extensions(app) return app ================================================ FILE: pydaria/blueprints/__init__.py ================================================ ================================================ FILE: pydaria/blueprints/restapi/__init__.py ================================================ from flask import Blueprint from flask_restful import Api from .resources import ProductItemResource, ProductResource bp = Blueprint("restapi", __name__, url_prefix="/api/v1") api = Api(bp) def init_app(app): api.add_resource(ProductResource, "/product/") api.add_resource(ProductItemResource, "/product/") app.register_blueprint(bp) ================================================ FILE: pydaria/blueprints/restapi/resources.py ================================================ from flask import abort, jsonify from flask_restful import Resource from pydaria.models import Product class ProductResource(Resource): def get(self): products = Product.query.all() or abort(204) return jsonify( {"products": [product.to_dict() for product in products]} ) class ProductItemResource(Resource): def get(self, product_id): product = Product.query.filter_by(id=product_id).first() or abort(404) return jsonify(product.to_dict()) ================================================ FILE: pydaria/blueprints/webui/__init__.py ================================================ from flask import Blueprint from .views import index, product bp = Blueprint("webui", __name__, template_folder="templates") bp.add_url_rule("/", view_func=index) bp.add_url_rule( "/product/", view_func=product, endpoint="productview" ) def init_app(app): app.register_blueprint(bp) ================================================ FILE: pydaria/blueprints/webui/templates/index.html ================================================ {% extends "bootstrap/base.html" %} {% block title %}{{config.get('TITLE')}}{% endblock %} {% block navbar %} {% endblock %} {% block content %}

{{config.get('TITLE')}}

{% endblock %} ================================================ FILE: pydaria/blueprints/webui/templates/product.html ================================================ {% extends "index.html" %} {% block content %}

{{ product.name }}

R$ {{ "%0.2f" | format(product.price)}}

{{product.description}}

{% endblock %} ================================================ FILE: pydaria/blueprints/webui/views.py ================================================ from flask import abort, render_template from pydaria.models import Product def index(): products = Product.query.all() return render_template("index.html", products=products) def product(product_id): product = Product.query.filter_by(id=product_id).first() or abort( 404, "produto nao encontrado" ) return render_template("product.html", product=product) ================================================ FILE: pydaria/ext/__init__.py ================================================ ================================================ FILE: pydaria/ext/admin.py ================================================ from flask_admin import Admin from flask_admin.base import AdminIndexView from flask_admin.contrib import sqla from flask_simplelogin import login_required from werkzeug.security import generate_password_hash from pydaria.ext.database import db from pydaria.models import Product, User # Proteger o admin com login via Monkey Patch AdminIndexView._handle_view = login_required(AdminIndexView._handle_view) sqla.ModelView._handle_view = login_required(sqla.ModelView._handle_view) admin = Admin() class UserAdmin(sqla.ModelView): column_list = ['username'] can_edit = False def on_model_change(self, form, model, is_created): model.password = generate_password_hash(model.password) def init_app(app): admin.name = app.config.TITLE admin.template_mode = "bootstrap3" admin.init_app(app) admin.add_view(sqla.ModelView(Product, db.session)) admin.add_view(UserAdmin(User, db.session)) ================================================ FILE: pydaria/ext/appearance.py ================================================ from flask_bootstrap import Bootstrap def init_app(app): Bootstrap(app) ================================================ FILE: pydaria/ext/auth.py ================================================ from flask_simplelogin import SimpleLogin from werkzeug.security import check_password_hash, generate_password_hash from pydaria.ext.database import db from pydaria.models import User def verify_login(user): """Valida o usuario e senha para efetuar o login""" username = user.get('username') password = user.get('password') if not username or not password: return False existing_user = User.query.filter_by(username=username).first() if not existing_user: return False if check_password_hash(existing_user.password, password): return True return False def create_user(username, password): """Registra um novo usuario caso nao esteja cadastrado""" if User.query.filter_by(username=username).first(): raise RuntimeError(f'{username} ja esta cadastrado') user = User(username=username, password=generate_password_hash(password)) db.session.add(user) db.session.commit() return user def init_app(app): SimpleLogin(app, login_checker=verify_login) ================================================ FILE: pydaria/ext/commands.py ================================================ import click from pydaria.ext.database import db from pydaria.ext.auth import create_user from pydaria.models import Product def create_db(): """Creates database""" db.create_all() def drop_db(): """Cleans database""" db.drop_all() def populate_db(): """Populate db with sample data""" data = [ Product( id=1, name="Ciabatta", price="10", description="Italian Bread" ), Product(id=2, name="Baguete", price="15", description="French Bread"), Product(id=3, name="Pretzel", price="20", description="German Bread"), ] db.session.bulk_save_objects(data) db.session.commit() return Product.query.all() def init_app(app): # add multiple commands in a bulk for command in [create_db, drop_db, populate_db]: app.cli.add_command(app.cli.command()(command)) # add a single command @app.cli.command() @click.option('--username', '-u') @click.option('--password', '-p') def add_user(username, password): """Adds a new user to the database""" return create_user(username, password) ================================================ FILE: pydaria/ext/configuration.py ================================================ from importlib import import_module from dynaconf import FlaskDynaconf def load_extensions(app): for extension in app.config.EXTENSIONS: # Split data in form `extension.path:factory_function` module_name, factory = extension.split(":") # Dynamically import extension module. ext = import_module(module_name) # Invoke factory passing app. getattr(ext, factory)(app) def init_app(app, **config): FlaskDynaconf(app, **config) ================================================ FILE: pydaria/ext/database.py ================================================ from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() def init_app(app): db.init_app(app) ================================================ FILE: pydaria/models.py ================================================ from pydaria.ext.database import db from sqlalchemy_serializer import SerializerMixin class Product(db.Model, SerializerMixin): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(140)) price = db.Column(db.Numeric()) description = db.Column(db.Text) class User(db.Model, SerializerMixin): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(140)) password = db.Column(db.String(512)) ================================================ FILE: pydaria/tests/__init__.py ================================================ ================================================ FILE: pydaria/tests/conftest.py ================================================ import pytest from pydaria.app import create_app, minimal_app from pydaria.ext.commands import populate_db from pydaria.ext.database import db @pytest.fixture(scope="session") def min_app(): app = minimal_app(FORCE_ENV_FOR_DYNACONF="testing") return app @pytest.fixture(scope="session") def app(): app = create_app(FORCE_ENV_FOR_DYNACONF="testing") with app.app_context(): db.create_all(app=app) yield app db.drop_all(app=app) @pytest.fixture(scope="session") def products(app): with app.app_context(): return populate_db() ================================================ FILE: pydaria/tests/test_api.py ================================================ from decimal import Decimal def test_products_get_all(client, products): # Arrange """Test get all products""" # Act response = client.get("/api/v1/product/") # Assert assert response.status_code == 200 data = response.json["products"] assert len(data) == 3 for product in products: assert product.id in [item["id"] for item in data] assert product.name in [item["name"] for item in data] assert product.price in [Decimal(item["price"]) for item in data] def test_products_get_one(client, products): # Arrange """Test get all products""" for product in products: # Act response = client.get(f"/api/v1/product/{product.id}") data = response.json # Assert assert response.status_code == 200 assert data["name"] == product.name assert Decimal(data["price"]) == product.price assert data["description"] == product.description ================================================ FILE: requirements.txt ================================================ dynaconf==2.2.1 Flask-Admin==1.5.4 Flask-Bootstrap==3.3.7.1 Flask-RESTful==0.3.7 flask-shell-ipython==0.4.1 flask-simplelogin==0.0.7 Flask-SQLAlchemy==2.4.1 Flask==1.1.1 SQLAlchemy-serializer==1.3.4.2 ================================================ FILE: requirements_dev.txt ================================================ flask-debugtoolbar flake8 black isort ipdb ================================================ FILE: requirements_test.txt ================================================ pytest-flask ================================================ FILE: settings.toml ================================================ [default] DEBUG = false FLASK_ADMIN_NAME = "Pydaria Admin" FLASK_ADMIN_TEMPLATE_MODE = "bootstrap3" FLASK_ADMIN_SWATCH = 'cerulean' SQLALCHEMY_DATABASE_URI = 'sqlite:///development.db' TITLE = "Pydaria" SECRET_KEY = "jadkfbsdkjbfbh" PASSWORD_SCHEMES = ['pbkdf2_sha512', 'md5_crypt'] EXTENSIONS = [ "pydaria.ext.appearance:init_app", "pydaria.ext.database:init_app", "pydaria.ext.auth:init_app", "pydaria.ext.admin:init_app", "pydaria.ext.commands:init_app", "pydaria.blueprints.webui:init_app", "pydaria.blueprints.restapi:init_app", ] [development] EXTENSIONS = [ "flask_debugtoolbar:DebugToolbarExtension", "dynaconf_merge_unique" # to reuse extensions list from [default] ] TEMPLATES_AUTO_RELOAD = true DEBUG = true DEBUG_TOOLBAR_ENABLED = true DEBUG_TB_INTERCEPT_REDIRECTS = false DEBUG_TB_PROFILER_ENABLED = true DEBUG_TB_TEMPLATE_EDITOR_ENABLED = true DEBUG_TB_PANELS = [ "flask_debugtoolbar.panels.versions.VersionDebugPanel", "flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel", "flask_debugtoolbar.panels.timer.TimerDebugPanel", "flask_debugtoolbar.panels.headers.HeaderDebugPanel", "flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel", "flask_debugtoolbar.panels.template.TemplateDebugPanel", "flask_debugtoolbar.panels.route_list.RouteListDebugPanel", "flask_debugtoolbar.panels.logger.LoggingPanel", "flask_debugtoolbar.panels.profiler.ProfilerDebugPanel", "flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel" ] [testing] SQLALCHEMY_DATABASE_URI = 'sqlite:///testing.db' [production] SQLALCHEMY_DATABASE_URI = 'sqlite:///production.db'