Flask 구조를 잡아 확장성있게 관리하기 Building Scalable Flask Project Structure
- Previously, I built projects using Spring and Django. It took a long time before actually starting to work because of all the setup, configuration, and understanding of supported services.
- Flask allows you to start projects minimally, so I thought there was relatively less to set up and learn when getting started.
- As I worked on Flask in my professional work and side projects, I found that while it was helpful for quickly building minimal applications, it was somewhat difficult to structure projects like Django or Spring.
- I decided to organize my thoughts because I wanted to create “my own Flask structure” to use as a template.
Virtual Environment
- From what I know, people are divided between pipenv and virtualenv based on preferences and popular opinion.
- I mainly used virtualenv, but I wanted to try pipenv. It feels more intuitive.
install pipenv
When using pipenv, instead of creating requirements.txt with pip freeze, dependencies are automatically saved to a Pipfile. It’s so convenient.
$ pip3 install pipenv # If not already installed!
$ pipenv shell # <-- Automatically creates and activates a virtual environment
(venv_name) $ pipenv install [package_name] # <-- Install within the virtual environment
What I installed - Pipfile
After installation, packages are saved to the Pipfile in the same directory.
Here are my install commands and Pipfile:
(venv_name) $ pipenv install flask flask-script python-dotenv
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "*"
flask-script = "*"
python-dotenv = "*"
[dev-packages]
autopep8 = "*"
[requires]
python_version = "3.9"
File Structure
- I don’t do this for every project, but generally I follow this pattern
- I’ve also been considering various approaches:
- Putting routers and providers inside the app package
- Dividing by domain within the app package, each with their own router and provider
- Simply creating app.py with routes and providers at the same level
Structure
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── app
│ └── __init__.py
├── config
│ ├── __init__.py
│ └── flask_config.py
├── manage.py
├── model
│ ├── __init__.py
│ └── test_model.py
├── provider
│ ├── __init__.py
│ ├── common_provider.py
│ ├── first
│ │ ├── __init__.py
│ │ └── first_provider.py
│ └── second
│ ├── __init__.py
│ └── second_provider.py
└── router
├── __init__.py
├── first_router
│ ├── __init__.py
│ └── first.py
└── second_router
├── __init__.py
└── second.py
- app/__init__.py: This is where the overall structure and configurations for the Flask app are injected.
- config/flask_config.py: Manages Flask configuration injection. Before injecting environment variables in app/init.py, it uses
.env (dot-env)to distinguish between production and development environments. - model: Where database models (ORM) are located (not covered here)
- provider: Handles the ‘service’ layer to be used by each API endpoint.
- router: Defines the API endpoints. The goal is to purely “ROUTE” - keep it as clean as possible.
Breaking Down Each Package
- I’ve written about the packages that I considered important enough to separate.
- Packages/modules not covered here will be briefly explained.
app/__init__.py
from flask import Flask, jsonify
from config import flask_config
from router import first_router
def register_router(flask_app: Flask):
# Register routers here
from router.first_router.first import first
from router.second_router.second import second
flask_app.register_blueprint(first)
flask_app.register_blueprint(second)
# Define functions to run with every request/response
@flask_app.before_request
def before_my_request():
print("before my request")
@flask_app.after_request
def after_my_request(res):
print("after my request", res.status_code)
return res
def create_app():
# App configuration
app = Flask(__name__)
app.config.from_object((get_flask_env()))
register_router(app)
return app
def get_flask_env():
# Divide config based on environment variables
if(flask_config.Config.ENV == "prod"):
return 'config.flask_config.prodConfig'
elif (flask_config.Config.ENV == "dev"):
return 'config.flask_config.devConfig'
- register_router(flask_app: Flask) - Creates a function that takes Flask as a parameter to manage blueprints. It registers the separated routers and gathers common functions to execute before processing requests and before returning responses.
- Literally “before_request” and “after_request”.
- create_app: Declares the app, loads the config file, and returns the app.
- get_flask_env: Applies configuration settings by distinguishing between production and development based on the Config’s environment variable logic.
manage.py
from flask_script import Server, Manager
from app import create_app
app = create_app()
manager = Manager(app)
manager.add_command(
"runserver",
Server(host='0.0.0.0', port=5000, use_debugger=True)
)
if __name__ == "__main__":
manager.run()
- I discovered flask-script and decided to use manage.py.
- I plan to cover flask-script in more detail later.
- It handles server execution and runtime settings. All other configuration is done in app/init.py, and manage.py just needs to import the completed app.
config/flask_config.py
import os
from dotenv import load_dotenv
load_dotenv(verbose=True)
class Config(object):
ENV = os.getenv('ENV')
CSRF_ENABLED = True
SECRET_KEY = os.getenv('SECRET_KEY')
SQLALCHEMY_TRACK_MODIFICATIONS = False
class devConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.environ["DB_USERNAME"] + ":" \
+ os.environ["DB_PASSWORD"] + "@" \
+ os.environ["DB_HOST"] + ":" \
+ os.environ["DB_PORT"] + "/" \
+ os.environ["DB_DATABASE"]
class prodConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://" + os.environ["DB_USERNAME"] + ":" \
+ os.environ["DB_PASSWORD"] + "@" \
+ os.environ["DB_HOST"] + ":" \
+ os.environ["DB_PORT"] + "/" \
+ os.environ["DB_DATABASE"]
- Uses dotenv to track the .env file and retrieve related environment variables.
- Currently there’s a base Config class, with devConfig and prodConfig inheriting from it.
- You could add more configurations depending on your deployment servers.
router/first_router (second_router is similar)
from flask import jsonify, request, Blueprint
first = Blueprint('first_route', __name__)
@first.route("/first", methods=['GET'])
def first_route():
msg = {
"page": "first",
"method": "GET"
}
return jsonify(msg)
- The router really just needs to use Blueprint effectively.
- Use Blueprint to properly divide and define API endpoints.
- One thing I consider important: router files should only contain routes and call a few providers that execute the logic.
- If you get lazy and declare multiple functions directly inside the router… it will eventually become painful.
provider
- The provider is the ‘service’ layer used by the routers above.
- It can be divided by domain or by commonly used functionality. It’s up to the developer…
- Think of it as creating ‘processed data’ to pass to the router with the API endpoint.
So Do I Have to Follow This Exactly?
Absolutely not. Feel free to adapt this based on your project.
The structure I’ve written because I thought it would be good is definitely not the only correct answer. I just like organizing things.
I want to make it even more compact…..!!!!!!
Database Connection
- I put the database connection in Config, but some people separate it out.
- I use model as the space for loading database models. I prefer creating a file for each table.
External Pipeline
- There are various things to consider for external pipelines.
- Pipeline configuration and connections could be grouped in Config or declared in a separate extConfig.
- The actual logic processing using the pipeline should be placed in the provider.
- I don’t want to plug it into the router and make things complicated…
댓글남기기