├── crm ├── .flaskenv ├── migrations │ ├── README │ ├── script.py.mako │ ├── alembic.ini │ ├── versions │ │ └── 24a44c0cd92f_customers_table.py │ └── env.py ├── crm.py ├── app │ ├── api │ │ ├── __init__.py │ │ ├── errors.py │ │ └── customers.py │ ├── __init__.py │ └── models.py └── config.py ├── swagger.png ├── requirements.txt ├── .gitignore └── README.md /crm/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=crm.py 2 | -------------------------------------------------------------------------------- /crm/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rg3915/crm-flask/master/swagger.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-Migrate==2.5.2 2 | Flask-SQLAlchemy==2.4.1 3 | Flask==1.1.1 4 | httpie==1.0.3 5 | python-dotenv==0.10.3 6 | flask-restplus==0.13.0 7 | -------------------------------------------------------------------------------- /crm/crm.py: -------------------------------------------------------------------------------- 1 | from app import create_app, db 2 | from app.models import Customer 3 | 4 | 5 | app = create_app() 6 | 7 | 8 | @app.shell_context_processor 9 | def make_shell_context(): 10 | return {'db': db, 'Customer': Customer} 11 | -------------------------------------------------------------------------------- /crm/app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_restplus import Api 2 | 3 | 4 | api = Api( 5 | version='1.0.0', 6 | title='Customers', 7 | doc='/doc/', 8 | description='CRM Api' 9 | ) 10 | 11 | 12 | from app.api import customers, errors 13 | -------------------------------------------------------------------------------- /crm/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | 5 | class Config(object): 6 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' 7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 8 | 'sqlite:///' + os.path.join(basedir, 'app.db') 9 | SQLALCHEMY_TRACK_MODIFICATIONS = False 10 | ADMINS = ['your-email@example.com'] 11 | POSTS_PER_PAGE = 25 12 | -------------------------------------------------------------------------------- /crm/app/api/errors.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | from werkzeug.http import HTTP_STATUS_CODES 3 | 4 | 5 | def bad_request(): 6 | return error_response(400, 'message') 7 | 8 | 9 | def error_response(status_code, message=None): 10 | payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')} 11 | if message: 12 | payload['message'] = message 13 | response = jsonify(payload) 14 | response.status_code = status_code 15 | return response 16 | -------------------------------------------------------------------------------- /crm/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /crm/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from config import Config 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | from flask import Blueprint 6 | 7 | 8 | app = Flask(__name__) 9 | app.config.from_object(Config) 10 | db = SQLAlchemy(app) 11 | migrate = Migrate(app, db) 12 | 13 | 14 | def create_app(config_class=Config): 15 | app = Flask(__name__) 16 | app.config.from_object(config_class) 17 | 18 | db.init_app(app) 19 | migrate.init_app(app, db) 20 | 21 | from app.api import api 22 | bp = Blueprint('api', __name__) 23 | api.init_app(bp) 24 | app.register_blueprint(bp, url_prefix='/api') 25 | 26 | return app 27 | 28 | 29 | from app import models 30 | -------------------------------------------------------------------------------- /crm/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /crm/migrations/versions/24a44c0cd92f_customers_table.py: -------------------------------------------------------------------------------- 1 | """customers table 2 | 3 | Revision ID: 24a44c0cd92f 4 | Revises: 5 | Create Date: 2019-10-12 02:16:22.459389 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '24a44c0cd92f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('customer', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=150), nullable=True), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_index(op.f('ix_customer_name'), 'customer', ['name'], unique=True) 27 | # ### end Alembic commands ### 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index(op.f('ix_customer_name'), table_name='customer') 33 | op.drop_table('customer') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /crm/app/models.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | 3 | 4 | class PaginatedAPIMixin(object): 5 | 6 | @staticmethod 7 | def to_collection_dict(query, page, per_page): 8 | resources = query.paginate(page, per_page, False) 9 | data = { 10 | 'items': [item.to_dict() for item in resources.items], 11 | '_meta': { 12 | 'page': page, 13 | 'per_page': per_page, 14 | 'total_pages': resources.pages, 15 | 'total_items': resources.total 16 | }, 17 | 'has_next': True if resources.has_next else False, 18 | 'has_prev': True if resources.has_prev else False 19 | } 20 | return data 21 | 22 | 23 | class Customer(PaginatedAPIMixin, db.Model): 24 | id = db.Column(db.Integer, primary_key=True) 25 | name = db.Column(db.String(150), index=True, unique=True) 26 | 27 | def to_dict(self, include_email=False): 28 | data = { 29 | 'id': self.id, 30 | 'name': self.name, 31 | } 32 | return data 33 | 34 | def from_dict(self, data): 35 | for field in ['name']: 36 | if field in data: 37 | setattr(self, field, data[field]) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | *env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | *.db 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # crm-flask 2 | 3 | Projeto sobre CRM em Flask. 4 | 5 | https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xxiii-application-programming-interfaces-apis 6 | 7 | ### doc 8 | 9 | Veja a doc em `/api/doc/`. 10 | 11 | ![swagger](swagger.png) 12 | 13 | 14 | | Método HTTP | URL | Notas | 15 | |-------------|------------------------|----------------------------| 16 | | GET | `/api/customers/` | Retorna um cliente. | 17 | | GET | `/api/customers/` | Retorna todos os clientes. | 18 | | POST | `/api/customers/` | Registra um novo cliente. | 19 | | PUT | `/api/customers/` | Modifica um cliente. | 20 | | DELETE | `/api/customers/` | Deleta um cliente. | 21 | 22 | 23 | 24 | ## Como rodar o projeto? 25 | 26 | * Clone esse repositório. 27 | * Crie um virtualenv com Python 3. 28 | * Ative o virtualenv. 29 | * Instale as dependências. 30 | * Rode as migrações. 31 | 32 | ``` 33 | git clone https://github.com/rg3915/crm-flask.git 34 | cd crm-flask 35 | python3 -m venv .venv 36 | source .venv/bin/activate 37 | pip install -r requirements.txt 38 | 39 | export FLASK_APP=crm 40 | export FLASK_ENV=development 41 | 42 | flask db init 43 | flask db migrate -m "customers table" 44 | flask db upgrade 45 | 46 | cd crm 47 | flask run 48 | ``` 49 | 50 | Em outro terminal, faça: 51 | 52 | ``` 53 | # Ative a virtualenv 54 | pip install httpie 55 | http POST http://localhost:5000/api/customers/ name='Abel' 56 | http POST http://localhost:5000/api/customers/ name='Regis' 57 | http GET http://localhost:5000/api/customers/ 58 | http PUT http://localhost:5000/api/customers/1 "name=John" 59 | http GET http://localhost:5000/api/customers/ 60 | http DELETE http://localhost:5000/api/customers/1 61 | 62 | names="Elliot Edward Angela Darlene Tyrell Joanna Phillip Whiterose Ollie Krista Gideon Shayla Terry Scott Fernando Leon Romero Trenton Mobley Cisco Dominique Ray Irving Sharon Susan Sutherland Hot Grant Bill Rat Ron Frank" 63 | 64 | for i in $names 65 | do http POST http://localhost:5000/api/customers/ name=$i 66 | done 67 | 68 | 69 | http GET http://localhost:5000/api/customers/?page=1\&per_page=10 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /crm/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /crm/app/api/customers.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_restplus import Resource, abort, fields, reqparse 3 | from app import db 4 | from app.api import api 5 | from app.models import Customer 6 | 7 | 8 | customer_model = api.model( 9 | 'Customer', 10 | { 11 | 'id': fields.String(readonly=True), 12 | 'name': fields.String(required=True, example='John'), 13 | } 14 | ) 15 | 16 | pagination = api.model( 17 | 'Page Model', { 18 | 'page': fields.Integer( 19 | description='Number of this page of results' 20 | ), 21 | 'per_page': fields.Integer( 22 | description='Number of items per page of results' 23 | ), 24 | 'total_pages': fields.Integer( 25 | description='Total number of pages of results' 26 | ), 27 | 'total_items': fields.Integer(description='Total number of results'), 28 | 'next_page': fields.String(), 29 | 'prev_page': fields.String() 30 | } 31 | ) 32 | 33 | customers_list = api.inherit( 34 | 'Page of customers', 35 | pagination, 36 | {'customers': fields.List(fields.Nested(customer_model))} 37 | ) 38 | 39 | ns_customers = api.namespace('customers', description='Customers') 40 | 41 | parser = reqparse.RequestParser() 42 | parser.add_argument('page', 1, type=int, location='args', required=False) 43 | parser.add_argument('per_page', 10, choices=[5, 10, 25, 50, 100], type=int, 44 | location='args', required=False) 45 | 46 | 47 | @ns_customers.route('/') 48 | class CustomerService(Resource): 49 | 50 | @api.marshal_with(customer_model) 51 | def get(self, id): 52 | return Customer.query.get_or_404(id).to_dict() 53 | 54 | @api.expect(customer_model) 55 | @api.marshal_with(customer_model) 56 | def put(self, id): 57 | customer = Customer.query.get_or_404(id) 58 | data = request.get_json() or {} 59 | if 'customer' in data and data['name'] != customer.name and \ 60 | Customer.query.filter_by(name=data['name']).first(): 61 | return abort(400, 'please use a different name') 62 | customer.from_dict(data) 63 | db.session.commit() 64 | return customer.to_dict() 65 | 66 | @api.response(204, 'Customer deleted') 67 | def delete(self, id): 68 | customer = Customer.query.get_or_404(id) 69 | db.session.delete(customer) 70 | db.session.commit() 71 | return 'Success', 204 72 | 73 | 74 | @ns_customers.route('/') 75 | class CustomersService(Resource): 76 | 77 | @api.expect(parser, validate=True) 78 | @api.marshal_list_with(customers_list, skip_none=True, code=200) 79 | def get(self): 80 | args = parser.parse_args() 81 | per_page = min(args['per_page'], 100) 82 | page = args['page'] 83 | query = Customer.to_collection_dict(Customer.query, page, per_page) 84 | data = { 85 | 'customers': query['items'], 86 | 'page': page, 87 | 'per_page': per_page, 88 | 'total_pages': query['_meta']['total_pages'], 89 | 'total_items': query['_meta']['total_items'], 90 | } 91 | if query['has_next']: 92 | data.update( 93 | { 94 | 'next_page': api.url_for( 95 | CustomersService, 96 | page=page + 1, 97 | per_page=per_page 98 | ) 99 | } 100 | ) 101 | if query['has_prev']: 102 | data.update( 103 | { 104 | 'prev_page': api.url_for( 105 | CustomersService, 106 | page=page - 1, 107 | per_page=per_page 108 | ) 109 | } 110 | ) 111 | return data 112 | 113 | @api.expect(customer_model) 114 | @api.marshal_with(customer_model, code=201) 115 | def post(self): 116 | data = request.get_json() or {} 117 | if 'name' not in data: 118 | return abort(400, 'must include name fields') 119 | if Customer.query.filter_by(name=data['name']).first(): 120 | return abort(400, 'please use a different name') 121 | customer = Customer() 122 | customer.from_dict(data) 123 | db.session.add(customer) 124 | db.session.commit() 125 | return customer.to_dict(), 201 126 | --------------------------------------------------------------------------------