├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── controllers │ ├── __init__.py │ └── user.py ├── main │ ├── __init__.py │ ├── amqp.py │ ├── api.py │ ├── celery.py │ ├── database.py │ ├── errors.py │ ├── logging.py │ └── settings.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ └── script.py.mako ├── models │ ├── __init__.py │ └── user.py ├── serializers │ ├── __init__.py │ └── user.py ├── services │ ├── __init__.py │ └── user.py └── utils │ └── __init__.py ├── manage.py ├── requirements.txt └── test └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | \.idea/ 2 | 3 | venv/ 4 | 5 | *.pyc 6 | __pycache__/ 7 | 8 | instance/ 9 | 10 | .pytest_cache/ 11 | .coverage 12 | htmlcov/ 13 | 14 | dist/ 15 | build/ 16 | *.egg-info/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | on: 3 | branch: master 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/Dockerfile -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bret Fisher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask API 2 | 3 | This is a Flask API that I will keep improving with new features and functionalities. 4 | This boilerplate can be used as a template for bigger projects. 5 | 6 | ## 🔴 I'll start to improve this project again after a couple of years 7 | 8 | ### Introduction 9 | 10 | The “micro” in microframework means Flask aims to keep the core simple but extensible. Flask won’t make many decisions for you, such as what database to use. 11 | 12 | By convention, templates and static files are stored in subdirectories within the application’s Python source tree, with the names templates and static respectively. 13 | 14 | ### Dependencies 15 | 16 | * [Python](https://www.python.org/) - Programming Language 17 | * [Flask](https://flask.palletsprojects.com/) - The framework used 18 | * [SQLAlchemy](https://docs.sqlalchemy.org/) - ORM 19 | * [Pydantic](https://pydantic-docs.helpmanual.io/) - Data validation 20 | * [Alembic](https://alembic.sqlalchemy.org/) - Database Migrations 21 | * [Pip](https://pypi.org/project/pip/) - Dependency Management 22 | * [RESTful](https://restfulapi.net/) - REST docs 23 | * [Representational State Transfer](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) - Article by Roy Fielding 24 | 25 | ### Virtual environments 26 | 27 | ``` 28 | $ sudo apt-get install python-virtualenv 29 | $ python3 -m venv venv 30 | $ . venv/bin/activate 31 | $ pip install Flask 32 | ``` 33 | 34 | Install all project dependencies using: 35 | 36 | ``` 37 | $ pip install -r requirements.txt 38 | ``` 39 | 40 | ### Running 41 | 42 | ``` 43 | $ export FLASK_APP=app.py 44 | $ export FLASK_ENV=development 45 | $ python -m flask run 46 | ``` 47 | 48 | This launches a very simple builtin server, which is good enough for testing but probably not what you want to use in production. 49 | 50 | If you enable debug support the server will reload itself on code changes, and it will also provide you with a helpful debugger if things go wrong. 51 | 52 | If you have the debugger disabled or trust the users on your network, you can make the server publicly available simply by adding --host=0.0.0.0 to the command line: 53 | 54 | ``` 55 | flask run --host=0.0.0.0 56 | ``` 57 | 58 | ### Running using Manager 59 | 60 | This app can be started using Flask Manager. It provides some useful commands and configurations, also, it can be customized with more functionalities. 61 | 62 | ``` 63 | python manage.py runserver 64 | ``` 65 | 66 | ### Alembic Migrations 67 | 68 | Use the following commands to create a new migration file and update the database with the last migrations version: 69 | 70 | ``` 71 | flask db revision --autogenerate -m "description here" 72 | flask db upgrade head 73 | ``` 74 | 75 | This project also uses the customized manager command to perform migrations. 76 | ``` 77 | python manage.py db revision --autogenerate -m "description here" 78 | python manage.py db upgrade head 79 | ``` 80 | 81 | To upgrade the database with the newest migrations version, use: 82 | 83 | ``` 84 | python manage.py db upgrade head 85 | ``` 86 | 87 | For more information, access [Auto generating migrations](https://alembic.sqlalchemy.org/en/latest/autogenerate.html). 88 | 89 | 90 | ## Contributing 91 | 92 | This API was developed based on: 93 | 94 | [Flask documentation](https://flask.palletsprojects.com/) 95 | 96 | [REST APIs with Flask and Python](https://www.udemy.com/rest-api-flask-and-python/) 97 | 98 | [The Ultimate Flask Course](https://www.udemy.com/the-ultimate-flask-course) 99 | 100 | 101 | ## License 102 | 103 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 104 | 105 | ## Certificate 106 | 107 | 108 | [Certificate](https://www.udemy.com/certificate/UC-CYMYZILZ/) 109 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from flask import Flask 4 | from app import main 5 | from app.main.api import api 6 | from app.main.database import db, migration 7 | from app.main.logging import LOGGING_CONFIG 8 | 9 | # Flask App Initialization 10 | app = Flask(__name__) 11 | app.config.from_object(main.settings[os.environ.get('APPLICATION_ENV', 'default')]) 12 | 13 | # Logs Initialization 14 | console = logging.getLogger('console') 15 | 16 | # Database ORM Initialization 17 | from app import models 18 | db.init_app(app) 19 | 20 | # Database Migrations Initialization 21 | migration.init_app(app, db) 22 | 23 | # Flask API Initialization 24 | api.init_app(app) 25 | -------------------------------------------------------------------------------- /app/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/controllers/__init__.py -------------------------------------------------------------------------------- /app/controllers/user.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | 3 | 4 | class User(Resource): 5 | def get(self, id): 6 | pass 7 | 8 | def put(self, id): 9 | pass 10 | 11 | def patch(self, id): 12 | pass 13 | 14 | def delete(self, id): 15 | pass 16 | 17 | 18 | class UserList(Resource): 19 | 20 | def get(self): 21 | pass 22 | 23 | def post(self): 24 | pass 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from app.main.settings import settings 2 | -------------------------------------------------------------------------------- /app/main/amqp.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/main/amqp.py -------------------------------------------------------------------------------- /app/main/api.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Api 2 | from app.controllers.user import UserList, User 3 | from app.main.errors import errors 4 | 5 | # Flask API Configuration 6 | api = Api( 7 | catch_all_404s=True, 8 | errors=errors, 9 | prefix='/api' 10 | ) 11 | 12 | api.add_resource(UserList, '/users') 13 | api.add_resource(User, '/users//') 14 | 15 | -------------------------------------------------------------------------------- /app/main/celery.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/main/celery.py -------------------------------------------------------------------------------- /app/main/database.py: -------------------------------------------------------------------------------- 1 | from flask_migrate import Migrate 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | # Database ORM Configuration 5 | db = SQLAlchemy() 6 | 7 | # Database Migrations Configuration 8 | migration = Migrate(directory='./app/migrations') -------------------------------------------------------------------------------- /app/main/errors.py: -------------------------------------------------------------------------------- 1 | errors = { 2 | 'UserAlreadyExistsError': { 3 | 'message': "A user with that username already exists.", 4 | 'status': 409, 5 | }, 6 | 'ResourceDoesNotExist': { 7 | 'message': "A resource with that ID no longer exists.", 8 | 'status': 410, 9 | 'extra': "Any extra information you want.", 10 | }, 11 | } -------------------------------------------------------------------------------- /app/main/logging.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | 3 | LOGGING_CONFIG = { 4 | 'version': 1, 5 | 6 | 'loggers': { 7 | '': { 8 | 'level': 'DEBUG', 9 | 'handlers': ['console', 'mail'], 10 | }, 11 | 'console': { 12 | 'level': 'DEBUG', 13 | 'handlers': ['console'], 14 | } 15 | }, 16 | 17 | 'handlers': { 18 | 'console': { 19 | 'level': 'DEBUG', 20 | 'formatter': 'info', 21 | 'class': 'logging.StreamHandler', 22 | 'stream': 'ext://sys.stdout', 23 | }, 24 | 'mail': { 25 | 'level': 'ERROR', 26 | 'formatter': 'error', 27 | 'class': 'logging.handlers.SMTPHandler', 28 | 'mailhost': 'localhost', 29 | 'fromaddr': 'monitoring@domain.com', 30 | 'toaddrs': ['dev@domain.com', 'qa@domain.com'], 31 | 'subject': 'Critical error with application name' 32 | } 33 | }, 34 | 35 | 'formatters': { 36 | 'info': { 37 | 'format': '%(asctime)s | %(levelname)s | %(name)s (%(module)s) | %(lineno)s | %(message)s' 38 | }, 39 | 'error': { 40 | 'format': '%(asctime)s | %(levelname)s | %(name)s (%(module)s) | %(lineno)s | %(message)s' 41 | }, 42 | }, 43 | 44 | } 45 | 46 | # Loggers 47 | logging.config.dictConfig(LOGGING_CONFIG) 48 | logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) 49 | logging.getLogger('sqlalchemy.dialects.postgresql').setLevel(logging.INFO) 50 | -------------------------------------------------------------------------------- /app/main/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | 5 | class Config: 6 | 7 | # project root directory 8 | BASE_DIR = os.path.join(os.pardir, os.path.dirname(__file__)) 9 | SECRET_KEY = os.environ.get("SECRET_KEY") 10 | 11 | # Flask Configuration 12 | # -------------------------------------------------------------------- 13 | DEBUG = False 14 | TESTING = False 15 | PORT = 8000 16 | 17 | # log file path 18 | # -------------------------------------------------------------------- 19 | enable_access_log = False 20 | log_socket_host = "127.0.0.1" 21 | log_socket_port = 514 22 | 23 | # redis main 24 | # -------------------------------------------------------------------- 25 | REDIS_HOST = "redis" # docker network 26 | REDIS_PORT = 6379 27 | REDIS_DB = 0 28 | REDIS_PASSWD = '' 29 | 30 | # sqlalchemy database main 31 | # -------------------------------------------------------------------- 32 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 33 | SQLALCHEMY_TRACK_MODIFICATIONS = True 34 | SQLALCHEMY_ENGINE_OPTIONS = { 35 | 'executemany_mode': 'batch', 36 | 'client_encoding': 'utf8', 37 | 'case_sensitive': False, 38 | 'echo': True, 39 | 'echo_pool': True 40 | } 41 | 42 | # SMTP server main 43 | # -------------------------------------------------------------------- 44 | SERVER_EMAIL = 'Damiano ' 45 | DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', SERVER_EMAIL) 46 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 47 | EMAIL_PORT = os.environ.get('EMAIL_PORT') 48 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 49 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 50 | 51 | 52 | class DevelopmentConfig(Config): 53 | 54 | ENV = os.environ.get("FLASK_ENV", "development") 55 | DEBUG = True 56 | ASSETS_DEBUG = True 57 | 58 | 59 | class TestingConfig(Config): 60 | 61 | ENV = os.environ.get("FLASK_ENV", "testing") 62 | DEBUG = True 63 | TESTING = True 64 | 65 | 66 | class ProductionConfig(Config): 67 | 68 | ENV = os.environ.get("FLASK_ENV", "production") 69 | DEBUG = False 70 | USE_RELOADER = False 71 | 72 | 73 | settings = { 74 | 'development': DevelopmentConfig, 75 | 'testing': TestingConfig, 76 | 'production': ProductionConfig, 77 | 'default': DevelopmentConfig, 78 | } -------------------------------------------------------------------------------- /app/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/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', 27 | str(current_app.extensions['migrate'].db.engine.url).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 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from app.models.user import User 2 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy_utils import EmailType 3 | 4 | from app import db 5 | 6 | 7 | class User(db.Model): 8 | """ 9 | This is a base user Model 10 | """ 11 | __tablename__ = 'users' 12 | 13 | id = db.Column(Integer, primary_key=True) 14 | fullname = db.Column(String(100), nullable=False) 15 | username = db.Column(String(20), nullable=False, unique=True) 16 | password = db.Column(String(50), nullable=False) 17 | email = db.Column(EmailType(), nullable=False, unique=True) 18 | 19 | def __init__(self, fullname, username, password, email): 20 | self.fullname = fullname 21 | self.username = username 22 | self.password = password 23 | self.email = email 24 | 25 | def __repr__(self): 26 | return "" % (self.fullname, self.username) 27 | -------------------------------------------------------------------------------- /app/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/serializers/__init__.py -------------------------------------------------------------------------------- /app/serializers/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr 2 | 3 | 4 | class User(BaseModel): 5 | id: int 6 | fullname: constr(max_length=100) # https://pydantic-docs.helpmanual.io/usage/types/#constrained-types 7 | username: constr(max_length=20) 8 | password: constr(max_length=50) 9 | 10 | class Config: 11 | orm_mode = True 12 | -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/services/__init__.py -------------------------------------------------------------------------------- /app/services/user.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/services/user.py -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/app/utils/__init__.py -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask_migrate import MigrateCommand 2 | from flask_script import Manager 3 | from app import app 4 | 5 | manager = Manager(app) 6 | 7 | # Database migrations command 8 | manager.add_command('db', MigrateCommand) 9 | 10 | if __name__ == '__main__': 11 | manager.run() 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.9.4 2 | amqp==5.1.1 3 | Flask==2.3.2 4 | Flask-Admin==1.6.0 5 | Flask-Mail==0.9.1 6 | Flask-Migrate==4.0.4 7 | Flask-RESTful==0.3.9 8 | Flask-Script==2.0.6 9 | Flask-SQLAlchemy==3.0.3 10 | psycopg2-binary==2.9.5 11 | pydantic==1.10.5 12 | SQLAlchemy-i18n==1.1.0 13 | SQLAlchemy-Utils==0.40.0 14 | Werkzeug==3.0.1 15 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damianoalves/flask-service/e8594ba711da3d7a8a48482ab6576fca3abfa403/test/__init__.py --------------------------------------------------------------------------------