├── .gitignore ├── LICENSE ├── README.md ├── cookiecutter.json └── {{cookiecutter.project_name}} ├── .dockerignore ├── .gitignore ├── .gitlab-ci-sample.yml ├── Makefile ├── app ├── __init__.py ├── config.py ├── controllers │ ├── __init__.py │ ├── aluno.py │ └── campus.py ├── dao │ ├── __init__.py │ ├── aluno.py │ └── campus.py ├── docs │ └── api-blueprint-sample.apib ├── errors │ └── __init__.py ├── models │ ├── __init__.py │ └── db.py ├── resources │ ├── __init__.py │ ├── aluno.py │ ├── campus.py │ └── docs.py └── templates │ └── apidocs │ └── index.html ├── docker-compose.yml ├── requirements.txt ├── run.sh ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── test_campus.py └── test_factory.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | /*/__pycache__/ 4 | 5 | instance/ 6 | 7 | .pytest_cache/ 8 | .coverage 9 | htmlcov/ 10 | 11 | dist/ 12 | build/ 13 | *.egg-info/ 14 | 15 | .idea/ 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Skeleton API 2 | 3 | Um simples esqueleto de API construido com Flask. 4 | 5 | ## About 6 | 7 | Este é um esqueleto de API construido com Flask e utilizado pelo Núcleo de Tecnologia e Inovação do Grupo Ceuma (NTI) para a construção de aplicações backend. 8 | 9 | ## Gerar esqueleto utilizando `cookiecutter` 10 | 11 | O `cookiecutter` é um utilitário em linha de comando utilizando para gerar templates de projetos criados utilizando a ferramenta. 12 | Para gerar o projeto a partir deste template, você deverá ter o pacote `cookiecutter` instalado no seu python. 13 | 14 | 1. Instale o `cookiecutter` com o comando `pip install cookiecutter` (aconselhamos o uso de um ambiente virtual python). 15 | 2. Execute o comando `cookiecutter https://github.com/devsceuma/flask-skeleton-api` para prosseguir com a geração do template 16 | no diretório atual da execução. 17 | 3. Uma vez baixado o projeto, o `cookiecutter` será responsável por gerar o template e para isso irá pedir algumas informações, 18 | entre elas "project_name" que é o nome da pasta do projeto e "app_name" que será o nome da aplicação e prefixo utilizado pela API 19 | em suas rotas. 20 | 21 | ## Documentação da API 22 | 23 | Para acessar a documentação da API, acesse a seguinte rota: 24 | 25 | ``` 26 | http://localhost:5000/app-name/apidocs/ 27 | ``` 28 | 29 | A API possui um arquivo de documentação *default* utilizando a especificação do *[Blueprint](https://apiblueprint.org/)*. 30 | O arquivo está em: `./app/docs/api-blueprint-sample.apib`. 31 | 32 | Preferimos deixar a responsabilidade da renderização do template HTML para o desenvolvedor. 33 | Toda vez que houver atualizações na especificação de endpoints da sua API, será de responsabilidade do desenvolvedor realizar a atualização e renderização do documento estático. 34 | Para isso, basta utilizar as ferramentas existentes e sugeridas pelo *[Blueprint](https://apiblueprint.org/)*. 35 | 36 | Afim de facilitar o processo de gerar o HTML, descrevemos ele a seguir. 37 | 38 | ### 1. Instale o *Render* 39 | 40 | Uma das ferramentas sugeridas pelo *Blueprint* é o [Aglio](https://github.com/danielgtaylor/aglio). Usaremos ele: 41 | 42 | ```npm install -g aglio``` 43 | 44 | ### 2. Gere a documentação. 45 | 46 | Para isso, entre na raíz do projeto e execute o seguinte comando: 47 | 48 | ``` 49 | aglio -i ./app/docs/api-blueprint-sample.apib --theme-full-width --no-theme-condense -o ./app/templates/apidocs/index.html 50 | ``` 51 | 52 | O Output será um arquivo ```index.html``` dentro de ```./app/templates/apidocs/index.html```. 53 | 54 | *p.s: O arquivo base para esta documentação foi retirado de: [Definindo APIs com o API Blueprint](https://eltonminetto.net/post/2017-06-29-definindo-apis-com-api-blueprint/)*. 55 | 56 | ## Como usar isto 57 | 58 | ### Usando Flask 59 | 60 | Antes de iniciar a sua aplicação, você deve informar ao seu terminal qual a aplicação `flask` que será iniciada. 61 | Para isto, você deverar *setar* uma variável de ambiente de nome `FLASK_APP` e, no nosso caso, valor igual a `app`. 62 | Neste caso, o valor `app` refere-se ao módulo python onde está contida a aplicação. 63 | 64 | > No Linux: 65 | ``` 66 | export FLASK_APP=app 67 | ``` 68 | 69 | > No Windows: 70 | ``` 71 | set FLASK_APP=app 72 | ``` 73 | 74 | Para ativar o modo de debug uma outra variável de ambiente deverá ser setada, desta vez com nome 75 | `FLASK_ENV` e valor `development`. Esta informação informa ao `flask` que ele 76 | deverá iniciar a aplicação em modo de desenvolvimento, com o *stacktrace* de erros 77 | e outras funcionalidades. 78 | 79 | > No Linux: 80 | ``` 81 | export FLASK_ENV=development 82 | ``` 83 | 84 | > No Windows: 85 | ``` 86 | set FLASK_ENV=development 87 | ``` 88 | 89 | ### Usando localmente com Cookiecutter 90 | 91 | O cookiecutter permite que o usuário crie projetos a partir de templates, como este Skeleton por exemplo. 92 | 93 | 1. Para isso você precisa instalar o cookiecutter conforme a [documentação](https://cookiecutter.readthedocs.io/en/latest/index.html) sugere. 94 | De preferência crie um *virtual-env* para isto. 95 | 96 | `pip install cookiecutter` 97 | 98 | 2. Depois gere o novo projeto a partir deste repositório. 99 | 100 | `cookiecutter gh:devsceuma/flask-skeleton-api` 101 | 102 | Para ver mais formas de uso, visite a sessao de *usage* do [cookiecutter](https://cookiecutter.readthedocs.io/en/latest/usage.html). 103 | 104 | #### Virtualenv 105 | 106 | É aconselhável que você esteja utilizando uma virtualenv para a execução do projeto. 107 | No [virtualenvwrapper][1] você encontra a documentação de um utilitário para utilização de ambientes 108 | virtuais com o python. Porém, se você não deseja instalar o utilitário, pode utilizar 109 | apenas o [virtualenv][2] a fim de isolar a instalação dos pacotes do python contido em sua máquina. 110 | 111 | ### Usando Docker 112 | 113 | Para subir a aplicação usando docker, basta executar o seguinte comando na raíz do projeto: 114 | 115 | ```docker-compose up -d --build``` 116 | 117 | O arquivo ```docker-compose.yml``` usa o [ceumanti/docker-python-odbc](https://hub.docker.com/r/ceumanti/docker-python-odbc), 118 | uma imagem preparada para com configuração de conexão com SQLServer usando a versão 3.6.5 do 119 | Python. A imagem é extensível e qualquer pessoa pode criar outras imagens a partir dela. 120 | Recomendamos que sejam criadas imagens a partir desta, por conta do processo de construção usando Pyenv, que é custosa. 121 | 122 | No processo de contrução e *build* da imagem o *script* `run.sh` será executado. Este *script*, que se 123 | encontra na raíz do projeto, é responsável por efetuar o download e instalação dos pacotes 124 | python, além da execução do servidor `waitress-serve` para rodar a aplicação. A aplicação 125 | dockerizada está pronta para produção (vide utilização de servidor preparado para tal propósito) 126 | e o modo de desenvolvimento está desativado. 127 | 128 | ### Integração com SQL Server 129 | 130 | Esta é uma seção especial destinada a esclarecer alguns processos necessários à utilização 131 | da aplicação em conjunto com o SQL Server. 132 | 133 | #### Driver ODBC 134 | 135 | > Linux: 136 | 137 | Para baixar o driver, você deverá a página 138 | [*ODBC Driver for SQL Server - Linux*](https://docs.microsoft.com/pt-br/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-2017) 139 | e seguir o tutorial. 140 | 141 | > Windows: 142 | 143 | Para baixar o driver, você deverá a página 144 | [*ODBC Driver for SQL Server - Windows*](https://docs.microsoft.com/pt-br/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-2017) 145 | e seguir o tutorial. 146 | 147 | #### Pacote `pyodbc` 148 | 149 | Para prosseguir com a instalação, certifique-se de que todo o processo anterior foi corretamente 150 | efetuado. Após isso, o comando 151 | 152 | ``` 153 | pip install pyodbc 154 | ``` 155 | 156 | deverá instalar o pacote `pyodbc` que 157 | será utilizado em conjunto com o driver para acesso à base. 158 | 159 | ## Maintainers and Contributors 160 | 161 | Este projeto é mantido por [@devsceuma](https://github.com/devsceuma). 162 | 163 | ### Maintainers 164 | 165 | [Atmos Maciel](https://github.com/atmosmps)
166 | [Igor Cavalcanti](https://github.com/cavalcantigor) 167 | 168 | 169 | ### Como contribuir 170 | 171 | Qualquer pessoa pode contribuir com este projeto, basta fazer um fork do repositório e submeter Pull Requests :) 172 | 173 | ## [License](./LICENSE) 174 | 175 | Este projeto está sob uma Licença [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0) 176 | 177 | 178 | [1]: "Virtualenvwrapper" 179 | [2]: "Virtualenv" -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "NTI CEUMA", 3 | "email": "nti@ceuma.br", 4 | "project_name": "project-name", 5 | "app_name": "app-name", 6 | "version": "v0.0.1" 7 | } 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .gitignore 4 | .git/ 5 | requeriments.txt 6 | Dockerfile 7 | docker-compose.yml 8 | *.pyc 9 | __pycache__/ 10 | ./app/__pycache__/* 11 | ./app/*/__pycache__/* 12 | ./docker/ 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | /*/__pycache__/ 4 | 5 | instance/ 6 | 7 | .pytest_cache/ 8 | .coverage 9 | htmlcov/ 10 | 11 | dist/ 12 | build/ 13 | *.egg-info/ 14 | 15 | .idea/ 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.gitlab-ci-sample.yml: -------------------------------------------------------------------------------- 1 | image: ceumanti/docker-python-odbc:latest 2 | 3 | #Stages: 4 | # - config 5 | # - build 6 | # - test 7 | # - deploy 8 | 9 | # Development: where the actual coding takes place 10 | # Staging: where the application is reviewed and tested 11 | # Production: where the final version of the application is hosted 12 | 13 | stages: 14 | - config 15 | - build 16 | - test 17 | - deploy 18 | 19 | config: 20 | stage: config 21 | script: 22 | - echo "Instalando pacotes de requirements.txt necessarios a aplicacao {{cookiecutter.app_name}}..." 23 | - pip install -r requirements.txt 24 | 25 | build: 26 | stage: build 27 | script: 28 | - echo "Iniciando aplicacao..." 29 | - waitress-serve --call --listen=0.0.0.0:5000 app:create_app 30 | 31 | test: 32 | stage: test 33 | script: 34 | - pytest 35 | 36 | deploy: 37 | stage: deploy 38 | script: 39 | - docker-compose down 40 | - docker-compose up -d --build 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/Makefile: -------------------------------------------------------------------------------- 1 | container=app 2 | 3 | up: 4 | docker-compose up -d 5 | 6 | up-debug: 7 | docker-compose up 8 | 9 | build: 10 | docker-compose rm -vsf 11 | docker-compose down -v --remove-orphans 12 | docker-compose build 13 | docker-compose up -d 14 | 15 | down: 16 | docker-compose down 17 | 18 | run: 19 | docker-compose run {container} /bin/bash 20 | 21 | jumpin: 22 | docker-compose run {container} bash 23 | 24 | test: 25 | docker-compose run {container} pytest ./tests/ 26 | 27 | test-file: 28 | docker-compose run {container} pytest ./tests/ --group $(FILE) 29 | 30 | tail-logs: 31 | docker-compose logs -f {container} 32 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from flask import Flask, request, current_app 3 | from datetime import datetime 4 | from logging import StreamHandler 5 | from flask_cors import CORS 6 | from .models.db import db 7 | from .config import DevelopmentConfig 8 | 9 | 10 | __author__ = "{{cookiecutter.author}}" 11 | __email__ = "{{cookiecutter.email}}" 12 | __version__ = "{{cookiecutter.version}}" 13 | 14 | 15 | def log_request(): 16 | try: 17 | # caso mude o WSGI, entao isso aqui devera mudar tambem 18 | logger = logging.getLogger('waitress') 19 | logger.setLevel(logging.INFO) 20 | logger.info("\t{asctime} \t {level} \t {ip} \t {method} \t {url}".format(asctime=datetime.now(). 21 | strftime("%d-%m-%Y %H:%M:%S"), 22 | level="INFO", 23 | ip=request.remote_addr, 24 | method=str(request.method), 25 | url=request.path)) 26 | except Exception as e: 27 | current_app.logger.error(e) 28 | 29 | 30 | def create_app(test_config=None): 31 | 32 | # cria e configura a aplicacao 33 | app = Flask(__name__, instance_relative_config=True) 34 | 35 | # configura log da aplicacao 36 | handler = StreamHandler() 37 | handler.setLevel(logging.ERROR) 38 | handler.setFormatter(formatter) 39 | app.logger.addHandler(handler) 40 | 41 | # modificando prefixo da url 42 | app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix='/{{cookiecutter.app_name}}') 43 | 44 | if test_config is None: 45 | # carrega uma instancia de configuracao 46 | app.config.from_object(DevelopmentConfig) 47 | else: 48 | # carrega a instancia test_config passada por parametro 49 | app.config.from_mapping(test_config) 50 | 51 | # registra as blueprints de resources 52 | from .resources.campus import bp as bp_campus 53 | from .resources.aluno import bp as bp_aluno 54 | from .resources.docs import bp as bp_docs 55 | app.register_blueprint(bp_campus) 56 | app.register_blueprint(bp_aluno) 57 | app.register_blueprint(bp_docs) 58 | 59 | db.init_app(app) 60 | CORS(app) 61 | 62 | # registrando log antes da requisicao 63 | app.before_request(log_request) 64 | 65 | return app 66 | 67 | 68 | class PrefixMiddleware(object): 69 | 70 | def __init__(self, app, prefix=''): 71 | self.app = app 72 | self.prefix = prefix 73 | 74 | def __call__(self, environ, start_response): 75 | 76 | if environ['PATH_INFO'].startswith(self.prefix): 77 | environ['PATH_INFO'] = environ['PATH_INFO'][len(self.prefix):] 78 | environ['SCRIPT_NAME'] = self.prefix 79 | return self.app(environ, start_response) 80 | else: 81 | start_response('404', [('Content-Type', 'text/plain')]) 82 | return ["Esta URL nao pertence a aplicacao. Por favor, insira o prefixo '/{{cookiecutter.app_name}}'." 83 | .encode()] 84 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/config.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import os 3 | 4 | 5 | class Config(object): 6 | DEBUG = False 7 | TESTING = False 8 | SQLALCHEMY_TRACK_MODIFICATIONS = False 9 | SQLALCHEMY_RECORD_QUERIES = True 10 | FLASK_SLOW_DB_QUERY_TIME = 0.2 11 | JSON_AS_ASCII = False 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | params_conn = 'Driver={ODBC Driver 17 for SQL Server};' \ 16 | 'Server=MY_SERVER;' \ 17 | 'Database=MY_DATABASE;' \ 18 | 'APP={{cookiecutter.app_name}};' \ 19 | 'UID=MY_USER;' \ 20 | 'PWD=MY_PASSWORD;' 21 | SQLALCHEMY_DATABASE_URI = "mssql+pyodbc:///?odbc_connect=%s" % urllib.parse.quote_plus(params_conn) 22 | DEBUG = True 23 | 24 | 25 | class TesteConfig(Config): 26 | DEBUG = True 27 | TESTING = True 28 | pass 29 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime, decimal 2 | 3 | 4 | def alchemy_encoder(obj): 5 | """ 6 | Enconder de objetos para o formato JSON para tratar casos de tipos especiais de dados. Esta função é comum 7 | a todos os controllers. 8 | 9 | :param obj: objeto a ser encondificado. 10 | :return: objeto codificado pronto para dump. 11 | """ 12 | if isinstance(obj, datetime.date): 13 | return obj.isoformat() 14 | elif isinstance(obj, decimal.Decimal): 15 | return float(obj) 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/controllers/aluno.py: -------------------------------------------------------------------------------- 1 | from ..errors import UsoInvalido, ErroInterno, TipoErro 2 | from ..dao import aluno as aluno_dao 3 | from . import alchemy_encoder 4 | import simplejson 5 | 6 | 7 | def recuperar_aluno(cpd=None): 8 | """ 9 | Função que recupera os alunos e trata a resposta para o formato JSON e então retorna para a View Function. 10 | 11 | :param cpd: código do aluno. 12 | :return: um objeto do tipo JSON pronto para ser enviado como resposta pela view function. 13 | """ 14 | try: 15 | 16 | resultado = aluno_dao.recupera_aluno(cpd) 17 | 18 | # transforma o resultado da consulta em JSON efetuando um dump para JSON utilizando um encoder proprio 19 | resposta = simplejson.dumps([dict(aluno) for aluno in resultado], default=alchemy_encoder, ensure_ascii=False) 20 | 21 | return resposta 22 | 23 | except ErroInterno as e: 24 | raise e 25 | except Exception: 26 | raise ErroInterno(TipoErro.ERRO_INTERNO.name, payload="Ocorreu um erro ao recuperar aluno(s).") 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/controllers/campus.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | from ..errors import ErroInterno, TipoErro 3 | from ..dao import campus as campus_dao 4 | from . import alchemy_encoder 5 | 6 | 7 | def recuperar_campus(): 8 | """ 9 | Função que recupera os alunos e trata a resposta para o formato JSON e então retorna para a View Function. 10 | 11 | :return: uma lista de objetos contendo informacoes dos campi. 12 | :exception ErroInterno 13 | """ 14 | try: 15 | resultado = campus_dao.recupera_campus() 16 | 17 | # transforma o resultado da consulta em JSON efetuando um dump para JSON utilizando um encoder proprio 18 | resposta = simplejson.dumps([dict(aluno) for aluno in resultado], default=alchemy_encoder, ensure_ascii=False) 19 | 20 | return resposta 21 | 22 | except ErroInterno as e: 23 | raise e 24 | except Exception: 25 | raise ErroInterno(TipoErro.ERRO_INTERNO.name, payload="Erro ao recuperar campi disponíveis.") 26 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexonlab/flask-skeleton-api/3a0cc71a3d027c7c4139b31bb4b19ad7bb5566fd/{{cookiecutter.project_name}}/app/dao/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/dao/aluno.py: -------------------------------------------------------------------------------- 1 | from ..models.db import db 2 | 3 | 4 | def recupera_aluno(cpd=None): 5 | try: 6 | sql = db.select([ 7 | db.text("CPD, NOME") 8 | ]).select_from( 9 | db.text("ALUNO") 10 | ) 11 | 12 | if cpd is not None: 13 | sql = sql.where(db.text("CPD = :cpd")) 14 | resultado = db.engine.execute(sql, cpd=cpd).fetchall() 15 | else: 16 | resultado = db.engine.execute(sql).fetchall() 17 | 18 | return resultado 19 | except Exception as e: 20 | raise e 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/dao/campus.py: -------------------------------------------------------------------------------- 1 | from ..models.db import db 2 | 3 | 4 | def recupera_campus(): 5 | """ 6 | Busca todos os campi disponíveis. 7 | 8 | :return: uma lista com todos os campi. 9 | :exception Exception: Lança uma exceção genérica caso ocorra algum erro. 10 | """ 11 | try: 12 | sql = db.select([ 13 | db.text("CODIGO, DESCRICAO") 14 | ]).select_from( 15 | db.text("CAMPUS") 16 | ) 17 | 18 | resultado = db.engine.execute(sql).fetchall() 19 | 20 | return resultado 21 | except Exception as e: 22 | raise e 23 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/docs/api-blueprint-sample.apib: -------------------------------------------------------------------------------- 1 | 2 | FORMAT: 1A 3 | HOST: http://localhost:5000/{{cookiecutter.app_name}} 4 | 5 | # Descrição da API 6 | 7 | Descrição da API. 8 | 9 | # Group API 10 | 11 | ## Sobre [/] 12 | 13 | Aqui podemos descrever detalhes que são comuns a todos os serviços como formatos, headers, tipos de erros, etc 14 | 15 | # Group Aluno 16 | 17 | ## Lista de Alunos [/aluno] 18 | 19 | ### Listar alunos [GET] 20 | 21 | + Response 200 (application/json) 22 | 23 | + Body 24 | 25 | { 26 | [ 27 | { 28 | "cpd": 1, 29 | "nome": "Nome do Aluno 1" 30 | }, 31 | { 32 | "cpd": 2, 33 | "nome": "Nome do Aluno 2" 34 | } 35 | ] 36 | } 37 | 38 | + Response 404 (application/json) 39 | 40 | + Body 41 | 42 | { 43 | "erro": "TIPO_ERRO", 44 | "status_code": 404, 45 | "mensagem": "Mensagem de erro." 46 | } 47 | 48 | 49 | ## Aluno [/aluno/{cpd}] 50 | 51 | + Parameters 52 | + cpd: 12345 (number, required) - CPD do aluno 53 | 54 | ### Aluno [GET] 55 | 56 | + Response 200 (application/json) 57 | 58 | Location: /aluno/12345 59 | 60 | + Body 61 | 62 | { 63 | "cpd": 12345, 64 | "nome": "Nome do Aluno" 65 | } 66 | 67 | + Response 404 (application/json) 68 | 69 | + Body 70 | 71 | { 72 | "erro": "TIPO_ERRO", 73 | "status_code": 404, 74 | "mensagem": "Mensagem de erro." 75 | } 76 | 77 | + Response 500 (application/json) 78 | 79 | + Body 80 | 81 | { 82 | "erro": "TIPO_ERRO", 83 | "status_code": 500, 84 | "mensagem": "Mensagem de erro." 85 | } 86 | 87 | ## Campus [/campus/] 88 | 89 | ### Lista Campus [GET] 90 | 91 | + Response 200 (application/json) 92 | 93 | Location: /campus/ 94 | 95 | + Body 96 | 97 | { 98 | "id": 1, 99 | "descricao": "Descricao do Campus" 100 | } 101 | 102 | + Response 404 (application/json) 103 | 104 | + Body 105 | 106 | { 107 | "erro": "TIPO_ERRO", 108 | "status_code": 404, 109 | "mensagem": "Mensagem de erro." 110 | } 111 | 112 | + Response 500 (application/json) 113 | 114 | + Body 115 | 116 | { 117 | "erro": "TIPO_ERRO", 118 | "status_code": 500, 119 | "mensagem": "Mensagem de erro." 120 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ErroInterno(Exception): 5 | status_code = 500 6 | 7 | def __init__(self, message, status_code=None, payload=None): 8 | Exception.__init__(self) 9 | self.message = message 10 | if status_code is not None: 11 | self.status_code = status_code 12 | self.payload = payload 13 | 14 | def to_dict(self): 15 | rv = dict([('erro', self.message), ('status_code', self.status_code), ('mensagem', self.payload)]) 16 | return rv 17 | 18 | 19 | class UsoInvalido(Exception): 20 | status_code = 400 21 | 22 | def __init__(self, message, status_code=None, payload=None): 23 | Exception.__init__(self) 24 | self.message = message 25 | if status_code is not None: 26 | self.status_code = status_code 27 | self.payload = payload 28 | 29 | def to_dict(self): 30 | rv = dict([('erro', self.message), ('status_code', self.status_code), ('mensagem', self.payload)]) 31 | return rv 32 | 33 | 34 | class TipoErro(Enum): 35 | INSCRICAO_DUPLICADA = 1 36 | ERRO_INTERNO = 2 37 | ERRO_JSON = 3 38 | ERRO_VALIDACAO = 4 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexonlab/flask-skeleton-api/3a0cc71a3d027c7c4139b31bb4b19ad7bb5566fd/{{cookiecutter.project_name}}/app/models/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | 4 | def generic_handler(error): 5 | """ 6 | Handler genérico de erros. Espera um objeto que contenha uma funcao [to_dict] e um atributo [code] a fim de 7 | preparar a resposta do erro no formato JSON. 8 | 9 | :param error: objeto a ser tratado pelo handler. 10 | :return: um objeto JSON a ser enviado como resposta para o requisitante. 11 | """ 12 | response = jsonify(error.to_dict()) 13 | response.status_code = error.status_code 14 | return response 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/resources/aluno.py: -------------------------------------------------------------------------------- 1 | from flask import (make_response, Blueprint, current_app) 2 | from ..controllers import aluno as aluno_controller 3 | from ..errors import ErroInterno, UsoInvalido, TipoErro 4 | from . import generic_handler 5 | 6 | 7 | bp = Blueprint('aluno', __name__, url_prefix='/aluno') 8 | bp.register_error_handler(ErroInterno, generic_handler) 9 | bp.register_error_handler(UsoInvalido, generic_handler) 10 | 11 | 12 | @bp.route('/', methods=('GET',)) 13 | @bp.route('/', methods=('GET',)) 14 | def get_aluno(cpd=None): 15 | """ 16 | View function para recuperar os alunos. Duas rotas são mapeadas, uma com um CPD e outra sem. Caso seja passado, 17 | um aluno especifico é retornado. 18 | CPD = Código de Processamento de Dados. 19 | 20 | :param cpd: código do aluno. 21 | :return: 200 - uma lista de alunos ou aluno buscado. 22 | 404 - erro na requisição. 23 | 500 - erro interno. 24 | """ 25 | try: 26 | # aqui o controller trata a resposta e manda o JSON no formato correto. 27 | resposta = aluno_controller.recuperar_aluno(cpd) 28 | 29 | # criando a resposta da requisicao 30 | response = make_response(resposta, 200) 31 | response.headers['Content-Type'] = 'application/json' 32 | 33 | # enviando a resposta para o requisitante 34 | return response 35 | except UsoInvalido as e: 36 | current_app.logger.error(e) 37 | raise e 38 | except ErroInterno as e: 39 | current_app.logger.error(e) 40 | raise e 41 | except Exception as e: 42 | current_app.logger.error(e) 43 | raise ErroInterno(TipoErro.ERRO_INTERNO.name, payload="Erro ao recuperar campi.") 44 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/resources/campus.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | from flask import (make_response, Blueprint, current_app) 3 | from ..controllers import campus as campus_controller 4 | from ..errors import ErroInterno, UsoInvalido, TipoErro 5 | from . import generic_handler 6 | 7 | 8 | bp = Blueprint('campus', __name__, url_prefix='/campus') 9 | bp.register_error_handler(ErroInterno, generic_handler) 10 | bp.register_error_handler(UsoInvalido, generic_handler) 11 | 12 | 13 | @bp.route('/', methods=('GET',)) 14 | def get_campus(): 15 | """ 16 | View function para recuperar os campi disponíveis. 17 | 18 | :return: 200 - uma lista de campi disponíveis. 19 | 404 - erro na requisição. 20 | 500 - erro interno. 21 | """ 22 | try: 23 | resultado = campus_controller.recuperar_campus() 24 | 25 | response = make_response(resultado, 200) 26 | response.headers['Content-Type'] = 'application/json' 27 | 28 | return response 29 | except UsoInvalido as e: 30 | current_app.logger.error(e) 31 | raise e 32 | except ErroInterno as e: 33 | current_app.logger.error(e) 34 | raise e 35 | except Exception as e: 36 | current_app.logger.error(e) 37 | raise ErroInterno(TipoErro.ERRO_INTERNO.name, payload="Erro ao recuperar campi.") 38 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/resources/docs.py: -------------------------------------------------------------------------------- 1 | from flask import (Blueprint, current_app, render_template) 2 | from ..errors import ErroInterno, UsoInvalido, TipoErro 3 | from . import generic_handler 4 | 5 | 6 | bp = Blueprint('docs', __name__, url_prefix='/apidocs') 7 | bp.register_error_handler(ErroInterno, generic_handler) 8 | bp.register_error_handler(UsoInvalido, generic_handler) 9 | 10 | 11 | @bp.route('/', methods=('GET',)) 12 | def apidocs(): 13 | """ 14 | View function para retornar a documentacao de API. 15 | """ 16 | try: 17 | return render_template('/apidocs/index.html') 18 | except UsoInvalido as e: 19 | current_app.logger.error(e) 20 | raise e 21 | except ErroInterno as e: 22 | current_app.logger.error(e) 23 | raise e 24 | except Exception as e: 25 | current_app.logger.error(e) 26 | raise ErroInterno(TipoErro.ERRO_INTERNO.name, payload="Erro ao recuperar campi.") 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/app/templates/apidocs/index.html: -------------------------------------------------------------------------------- 1 | Descrição da API Back to top

Descrição da API

Descrição da API.

2 |

API

Sobre

Aqui podemos descrever detalhes que são comuns a todos os serviços como formatos, headers, tipos de erros, etc

3 |

Aluno

Lista de Alunos

Listar alunos
GET/aluno

Example URI

GET http://localhost:5000/{{cookiecutter.app_name}}/aluno
Response  200
HideShow
Headers
Content-Type: application/json
Body
{
  4 |   [
  5 |     {
  6 |       "cpd": 1,
  7 |       "nome": "Nome do Aluno 1"
  8 |     },
  9 |     {
 10 |       "cpd": 2,
 11 |       "nome": "Nome do Aluno 2"
 12 |     }
 13 |   ]
 14 | }
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
 15 |   "erro": "TIPO_ERRO",
 16 |   "status_code": 404,
 17 |   "mensagem": "Mensagem de erro."
 18 | }

Aluno

Aluno
GET/aluno/{cpd}

Example URI

GET http://localhost:5000/{{cookiecutter.app_name}}/aluno/12345
URI Parameters
HideShow
cpd
number (required) Example: 12345

CPD do aluno

19 |
Response  200
HideShow

Location: /aluno/12345

20 |
Headers
Content-Type: application/json
Body
{
 21 |   "cpd": 12345,
 22 |   "nome": "Nome do Aluno"
 23 | }
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
 24 |   "erro": "TIPO_ERRO",
 25 |   "status_code": 404,
 26 |   "mensagem": "Mensagem de erro."
 27 | }
Response  500
HideShow
Headers
Content-Type: application/json
Body
{
 28 |   "erro": "TIPO_ERRO",
 29 |   "status_code": 500,
 30 |   "mensagem": "Mensagem de erro."
 31 | }

Campus

Lista Campus
GET/campus/

Example URI

GET http://localhost:5000/{{cookiecutter.app_name}}/campus/
Response  200
HideShow

Location: /campus/

32 |
Headers
Content-Type: application/json
Body
{
 33 |   "id": 1,
 34 |   "descricao": "Descricao do Campus"
 35 | }
Response  404
HideShow
Headers
Content-Type: application/json
Body
{
 36 |   "erro": "TIPO_ERRO",
 37 |   "status_code": 404,
 38 |   "mensagem": "Mensagem de erro."
 39 | }
Response  500
HideShow
Headers
Content-Type: application/json
Body
{
 40 |   "erro": "TIPO_ERRO",
 41 |   "status_code": 500,
 42 |   "mensagem": "Mensagem de erro."
 43 | }

Generated by aglio on 21 Feb 2019

-------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | app: 4 | image: ceumanti/docker-python-odbc:latest 5 | container_name: {{cookiecutter.app_name}} 6 | restart: on-failure:10 7 | volumes: 8 | - .:/application 9 | command: bash -c "chmod +x run.sh && ./run.sh" 10 | ports: 11 | - "5000:5000" 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==3.0.2 2 | atomicwrites==1.2.1 3 | attrs==18.2.0 4 | click==6.7 5 | coverage==4.5.1 6 | Flask==1.0.2 7 | Flask-Cors==3.0.6 8 | Flask-RESTful==0.3.6 9 | Flask-SQLAlchemy==2.3.2 10 | itsdangerous==0.24 11 | Jinja2>=2.10 12 | jsonschema==2.6.0 13 | MarkupSafe==1.0 14 | marshmallow==2.15.6 15 | mistune==0.8.3 16 | more-itertools==4.3.0 17 | pluggy==0.7.1 18 | py==1.6.0 19 | pyodbc==4.0.24 20 | pytest==3.8.0 21 | pytz==2018.5 22 | PyYAML==5.4 23 | simplejson==3.16.0 24 | six==1.11.0 25 | SQLAlchemy>=1.3.3 26 | style==1.1.0 27 | urllib3>=1.24.1 28 | waitress>=1.1.0 29 | Werkzeug>=0.15.3 30 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "=> Instalando pacotes de requirements.txt necessarios a aplicacao {{cookiecutter.app_name}}..." 3 | pip install -r requirements.txt 4 | echo "" 5 | echo "=> O waitress-server deu inicio ao processo de deploy da aplicação..." 6 | waitress-serve --call --listen=0.0.0.0:5000 app:create_app 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | testpaths = tests 3 | 4 | [coverage:run] 5 | branch = True 6 | source = app -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nexonlab/flask-skeleton-api/3a0cc71a3d027c7c4139b31bb4b19ad7bb5566fd/{{cookiecutter.project_name}}/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from app import create_app 4 | from app.config import TesteConfig 5 | 6 | 7 | @pytest.fixture 8 | def app(): 9 | 10 | app = create_app() 11 | app.config.from_object(TesteConfig) 12 | 13 | yield app 14 | 15 | 16 | @pytest.fixture 17 | def client(app): 18 | return app.test_client() 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/test_campus.py: -------------------------------------------------------------------------------- 1 | # testa se get_campus esta sendo executado 2 | def test_get_campus(client): 3 | response = client.get('/{{cookiecutter.app_name}}/campus/') 4 | assert response is not None 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/test_factory.py: -------------------------------------------------------------------------------- 1 | # testa se uma aplicacao em modo de teste esta sendo construida 2 | def test_config(app): 3 | assert app.testing 4 | --------------------------------------------------------------------------------