5 분 소요


Github Link

  • 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…

댓글남기기