16. Flask Web Framework Basics
16. Flask Web Framework Basics¶
Learning Objectives¶
- Understand Flask's core concepts and structure
- Master routing, templates, and form handling
- Implement database integration and RESTful APIs
- Structure large-scale applications using blueprints
1. Introduction to Flask¶
1.1 What is Flask?¶
Flask is a micro web framework written in Python.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Flask Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββ βββββββββββ βββββββββββ β
β β Client ββββββΆβ Flask ββββββΆβ Responseβ β
β β(Browser)β β App β β (HTML) β β
β βββββββββββ ββββββ¬βββββ βββββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββ βββββββββββ βββββββββββ β
β β Routing β βTemplatesβ βDatabase β β
β β (URL) β β (Jinja2)β β(SQLAlchemy)β β
β βββββββββββ βββββββββββ βββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.2 Installation and Setup¶
# κ°μνκ²½ μμ±
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Flask μ€μΉ
pip install flask
# μΆκ° ν¨ν€μ§ (κΆμ₯)
pip install flask-sqlalchemy flask-migrate flask-wtf python-dotenv
1.3 Minimal Flask Application¶
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
# μ€ν
python app.py
# λλ
flask run --debug
# λΈλΌμ°μ μμ http://127.0.0.1:5000 μ μ
2. Routing¶
2.1 Basic Routes¶
from flask import Flask
app = Flask(__name__)
# κΈ°λ³Έ λΌμ°νΈ
@app.route('/')
def index():
return 'Home Page'
# μ μ κ²½λ‘
@app.route('/about')
def about():
return 'About Page'
@app.route('/contact')
def contact():
return 'Contact Page'
2.2 Dynamic URLs¶
# λ¬Έμμ΄ λ³μ
@app.route('/user/<username>')
def show_user(username):
return f'User: {username}'
# μ μ λ³μ
@app.route('/post/<int:post_id>')
def show_post(post_id):
return f'Post #{post_id}'
# κ²½λ‘ λ³μ (μ¬λμ ν¬ν¨)
@app.route('/path/<path:subpath>')
def show_path(subpath):
return f'Path: {subpath}'
# μ¬λ¬ λ³μ
@app.route('/user/<username>/post/<int:post_id>')
def user_post(username, post_id):
return f'{username}\'s Post #{post_id}'
2.3 HTTP Methods¶
from flask import request
# GET (κΈ°λ³Έκ°)
@app.route('/search')
def search():
query = request.args.get('q', '')
return f'Search: {query}'
# POST
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# λ‘κ·ΈμΈ μ²λ¦¬
return f'Logging in as {username}'
return '''
<form method="post">
<input name="username" placeholder="Username">
<input name="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
'''
# RESTful μ€νμΌ
@app.route('/api/users', methods=['GET', 'POST'])
def users():
if request.method == 'GET':
return {'users': ['Alice', 'Bob']}
elif request.method == 'POST':
data = request.json
return {'created': data['name']}, 201
@app.route('/api/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
def user(user_id):
if request.method == 'GET':
return {'id': user_id, 'name': 'Alice'}
elif request.method == 'PUT':
data = request.json
return {'id': user_id, 'name': data['name']}
elif request.method == 'DELETE':
return '', 204
2.4 URL Building¶
from flask import url_for, redirect
@app.route('/admin')
def admin():
return 'Admin Page'
@app.route('/user/<username>')
def profile(username):
return f'Profile: {username}'
@app.route('/redirect-example')
def redirect_example():
# url_forλ‘ URL μμ±
home_url = url_for('index') # '/'
admin_url = url_for('admin') # '/admin'
profile_url = url_for('profile', username='alice') # '/user/alice'
# 리λ€μ΄λ νΈ
return redirect(url_for('profile', username='guest'))
3. Templates (Jinja2)¶
3.1 Basic Templates¶
νλ‘μ νΈ κ΅¬μ‘°:
my_app/
βββ app.py
βββ templates/
β βββ base.html
β βββ index.html
β βββ user.html
βββ static/
βββ css/
β βββ style.css
βββ js/
βββ main.js
# app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html', title='Home')
@app.route('/user/<name>')
def user(name):
posts = [
{'title': 'First Post', 'date': '2024-01-01'},
{'title': 'Second Post', 'date': '2024-01-15'},
]
return render_template('user.html', username=name, posts=posts)
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - My App</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2024 My App</p>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
<!-- templates/index.html -->
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h1>Welcome to {{ title }}</h1>
<p>This is the home page.</p>
{% endblock %}
<!-- templates/user.html -->
{% extends 'base.html' %}
{% block title %}{{ username }}'s Profile{% endblock %}
{% block content %}
<h1>{{ username }}'s Profile</h1>
{% if posts %}
<h2>Posts</h2>
<ul>
{% for post in posts %}
<li>
<strong>{{ post.title }}</strong>
<span>{{ post.date }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}
{% endblock %}
3.2 Jinja2 Syntax¶
<!-- λ³μ μΆλ ₯ -->
<p>{{ variable }}</p>
<p>{{ user.name }}</p>
<p>{{ items[0] }}</p>
<!-- νν° -->
<p>{{ name|upper }}</p>
<p>{{ text|truncate(100) }}</p>
<p>{{ date|datetime('%Y-%m-%d') }}</p>
<p>{{ html_content|safe }}</p>
<!-- 쑰건문 -->
{% if user.is_admin %}
<p>Admin Panel</p>
{% elif user.is_staff %}
<p>Staff Panel</p>
{% else %}
<p>User Panel</p>
{% endif %}
<!-- λ°λ³΅λ¬Έ -->
{% for item in items %}
<p>{{ loop.index }}. {{ item }}</p>
{% endfor %}
<!-- loop λ³μ -->
{% for user in users %}
{{ loop.index }} {# 1λΆν° μμνλ μΈλ±μ€ #}
{{ loop.index0 }} {# 0λΆν° μμνλ μΈλ±μ€ #}
{{ loop.first }} {# 첫 λ²μ§Έ λ°λ³΅μ΄λ©΄ True #}
{{ loop.last }} {# λ§μ§λ§ λ°λ³΅μ΄λ©΄ True #}
{{ loop.length }} {# μ 체 νλͺ© μ #}
{% endfor %}
<!-- λ§€ν¬λ‘ (μ¬μ¬μ© κ°λ₯ν ν
νλ¦Ώ ν¨μ) -->
{% macro input(name, type='text', value='') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{{ input('username') }}
{{ input('password', type='password') }}
<!-- λ§€ν¬λ‘ κ°μ Έμ€κΈ° -->
{% from 'macros.html' import input %}
<!-- λΈλ‘ -->
{% block sidebar %}
<aside>Default Sidebar</aside>
{% endblock %}
<!-- λΆλͺ¨ λΈλ‘ λ΄μ© ν¬ν¨ -->
{% block content %}
{{ super() }}
<p>Additional content</p>
{% endblock %}
3.3 Custom Filters¶
# app.py
from flask import Flask
from datetime import datetime
app = Flask(__name__)
@app.template_filter('datetime')
def format_datetime(value, format='%Y-%m-%d %H:%M'):
if isinstance(value, str):
value = datetime.fromisoformat(value)
return value.strftime(format)
@app.template_filter('money')
def format_money(value):
return f'{value:,.0f}μ'
# ν
νλ¦Ώμμ μ¬μ©: {{ post.created_at|datetime('%Yλ
%mμ %dμΌ') }}
# ν
νλ¦Ώμμ μ¬μ©: {{ price|money }}
4. Form Handling¶
4.1 Basic Form Handling¶
from flask import Flask, render_template, request, redirect, url_for, flash
app = Flask(__name__)
app.secret_key = 'your-secret-key' # flash λ©μμ§μ©
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# κ°λ¨ν μ ν¨μ± κ²μ¬
if not name or not email:
flash('μ΄λ¦κ³Ό μ΄λ©μΌμ νμμ
λλ€.', 'error')
return redirect(url_for('contact'))
# λ°μ΄ν° μ²λ¦¬ (μ: μ΄λ©μΌ μ μ‘, DB μ μ₯)
flash('λ©μμ§κ° μ μ‘λμμ΅λλ€.', 'success')
return redirect(url_for('index'))
return render_template('contact.html')
<!-- templates/contact.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Contact Us</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="message">Message:</label>
<textarea id="message" name="message" rows="5"></textarea>
</div>
<button type="submit">Send</button>
</form>
{% endblock %}
4.2 Form Handling with Flask-WTF¶
pip install flask-wtf email-validator
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SelectField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
class LoginForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(message='μ¬μ©μλͺ
μ μ
λ ₯νμΈμ.'),
Length(min=3, max=20, message='3-20μ μ¬μ΄λ‘ μ
λ ₯νμΈμ.')
])
password = PasswordField('Password', validators=[
DataRequired(message='λΉλ°λ²νΈλ₯Ό μ
λ ₯νμΈμ.')
])
remember = BooleanField('Remember Me')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=20)
])
email = StringField('Email', validators=[
DataRequired(),
Email(message='μ ν¨ν μ΄λ©μΌ μ£Όμλ₯Ό μ
λ ₯νμΈμ.')
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8, message='λΉλ°λ²νΈλ μ΅μ 8μ μ΄μμ΄μ΄μΌ ν©λλ€.')
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='λΉλ°λ²νΈκ° μΌμΉνμ§ μμ΅λλ€.')
])
def validate_username(self, field):
# 컀μ€ν
μ ν¨μ± κ²μ¬
if field.data.lower() in ['admin', 'root', 'administrator']:
raise ValidationError('μ¬μ©ν μ μλ μ¬μ©μλͺ
μ
λλ€.')
class ContactForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
category = SelectField('Category', choices=[
('general', 'μΌλ° λ¬Έμ'),
('support', 'κΈ°μ μ§μ'),
('feedback', 'νΌλλ°±')
])
message = TextAreaField('Message', validators=[
DataRequired(),
Length(min=10, max=1000)
])
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from forms import LoginForm, RegistrationForm
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# form.username.data, form.password.data μ¬μ©
username = form.username.data
# λ‘κ·ΈμΈ λ‘μ§...
flash(f'{username}λ, νμν©λλ€!', 'success')
return redirect(url_for('index'))
return render_template('login.html', form=form)
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# νμκ°μ
λ‘μ§...
flash('νμκ°μ
μ΄ μλ£λμμ΅λλ€.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
<!-- templates/login.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Login</h1>
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
{% endblock %}
4.3 File Upload¶
import os
from flask import Flask, request, redirect, url_for, flash
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB μ ν
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('νμΌμ΄ μμ΅λλ€.')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('μ νλ νμΌμ΄ μμ΅λλ€.')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
flash(f'{filename} μ
λ‘λ μλ£!')
return redirect(url_for('index'))
else:
flash('νμ©λμ§ μλ νμΌ νμμ
λλ€.')
return redirect(request.url)
return '''
<form method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
'''
5. Database (Flask-SQLAlchemy)¶
5.1 Configuration and Model Definition¶
# app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# λͺ¨λΈ μ μ
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# κ΄κ³: 1:N
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return f'<User {self.username}>'
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# μΈλ ν€
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# κ΄κ³: N:M (νκ·Έ)
tags = db.relationship('Tag', secondary='post_tags', backref='posts')
def __repr__(self):
return f'<Post {self.title}>'
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
# λ€λλ€ κ΄κ³λ₯Ό μν μ°κ²° ν
μ΄λΈ
post_tags = db.Table('post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)
# λ°μ΄ν°λ² μ΄μ€ μμ±
with app.app_context():
db.create_all()
5.2 CRUD Operations¶
# Create
user = User(username='alice', email='alice@example.com')
db.session.add(user)
db.session.commit()
# μ¬λ¬ κ° μΆκ°
users = [
User(username='bob', email='bob@example.com'),
User(username='charlie', email='charlie@example.com')
]
db.session.add_all(users)
db.session.commit()
# Read
# μ 체 μ‘°ν
all_users = User.query.all()
# IDλ‘ μ‘°ν
user = User.query.get(1) # deprecated in SQLAlchemy 2.0
user = db.session.get(User, 1) # SQLAlchemy 2.0+
# 쑰건 μ‘°ν
user = User.query.filter_by(username='alice').first()
users = User.query.filter(User.email.like('%@example.com')).all()
# μ λ ¬
users = User.query.order_by(User.created_at.desc()).all()
# νμ΄μ§λ€μ΄μ
page = User.query.paginate(page=1, per_page=10)
# page.items, page.has_next, page.has_prev, page.pages
# Update
user = User.query.filter_by(username='alice').first()
user.email = 'newemail@example.com'
db.session.commit()
# λλ bulk update
User.query.filter_by(username='alice').update({'email': 'new@example.com'})
db.session.commit()
# Delete
user = User.query.filter_by(username='alice').first()
db.session.delete(user)
db.session.commit()
# λλ bulk delete
User.query.filter(User.created_at < some_date).delete()
db.session.commit()
5.3 Using in Routes¶
from flask import Flask, render_template, request, redirect, url_for, flash, abort
@app.route('/users')
def user_list():
page = request.args.get('page', 1, type=int)
users = User.query.order_by(User.created_at.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('users/list.html', users=users)
@app.route('/users/<int:user_id>')
def user_detail(user_id):
user = db.session.get(User, user_id)
if user is None:
abort(404)
return render_template('users/detail.html', user=user)
@app.route('/users/create', methods=['GET', 'POST'])
def user_create():
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
# μ€λ³΅ νμΈ
if User.query.filter_by(username=username).first():
flash('μ΄λ―Έ μ‘΄μ¬νλ μ¬μ©μλͺ
μ
λλ€.', 'error')
return redirect(url_for('user_create'))
user = User(username=username, email=email)
db.session.add(user)
db.session.commit()
flash('μ¬μ©μκ° μμ±λμμ΅λλ€.', 'success')
return redirect(url_for('user_detail', user_id=user.id))
return render_template('users/create.html')
@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
def user_edit(user_id):
user = db.session.get(User, user_id)
if user is None:
abort(404)
if request.method == 'POST':
user.email = request.form['email']
db.session.commit()
flash('μ¬μ©μ μ λ³΄κ° μμ λμμ΅λλ€.', 'success')
return redirect(url_for('user_detail', user_id=user.id))
return render_template('users/edit.html', user=user)
@app.route('/users/<int:user_id>/delete', methods=['POST'])
def user_delete(user_id):
user = db.session.get(User, user_id)
if user is None:
abort(404)
db.session.delete(user)
db.session.commit()
flash('μ¬μ©μκ° μμ λμμ΅λλ€.', 'success')
return redirect(url_for('user_list'))
5.4 Migrations (Flask-Migrate)¶
pip install flask-migrate
# app.py
from flask_migrate import Migrate
app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# λ§μ΄κ·Έλ μ΄μ
μ΄κΈ°ν
flask db init
# λ§μ΄κ·Έλ μ΄μ
μμ±
flask db migrate -m "Initial migration"
# λ§μ΄κ·Έλ μ΄μ
μ μ©
flask db upgrade
# λ‘€λ°±
flask db downgrade
6. Sessions and Cookies¶
6.1 Using Sessions¶
from flask import Flask, session, redirect, url_for, request
app = Flask(__name__)
app.secret_key = 'your-secret-key'
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
# μΈμ¦ λ‘μ§...
session['user_id'] = user.id
session['username'] = user.username
session.permanent = True # μΈμ
μ μ§ (κΈ°λ³Έ 31μΌ)
return redirect(url_for('dashboard'))
@app.route('/logout')
def logout():
session.clear()
# λλ νΉμ ν€λ§ μμ
# session.pop('user_id', None)
return redirect(url_for('index'))
@app.route('/dashboard')
def dashboard():
if 'user_id' not in session:
return redirect(url_for('login'))
return f'Welcome, {session["username"]}!'
6.2 Using Cookies¶
from flask import make_response, request
@app.route('/set-cookie')
def set_cookie():
response = make_response('Cookie set!')
response.set_cookie(
'theme',
'dark',
max_age=60*60*24*30, # 30μΌ
httponly=True,
secure=True, # HTTPSμμλ§
samesite='Lax'
)
return response
@app.route('/get-cookie')
def get_cookie():
theme = request.cookies.get('theme', 'light')
return f'Current theme: {theme}'
@app.route('/delete-cookie')
def delete_cookie():
response = make_response('Cookie deleted!')
response.delete_cookie('theme')
return response
7. Blueprints¶
7.1 Blueprint Structure¶
my_app/
βββ app/
β βββ __init__.py
β βββ models.py
β βββ auth/
β β βββ __init__.py
β β βββ routes.py
β β βββ forms.py
β β βββ templates/
β β βββ auth/
β β βββ login.html
β β βββ register.html
β βββ main/
β β βββ __init__.py
β β βββ routes.py
β β βββ templates/
β β βββ main/
β β βββ index.html
β βββ api/
β β βββ __init__.py
β β βββ routes.py
β βββ templates/
β β βββ base.html
β βββ static/
β βββ css/
β βββ js/
βββ config.py
βββ requirements.txt
βββ run.py
7.2 Defining Blueprints¶
# app/auth/__init__.py
from flask import Blueprint
auth_bp = Blueprint('auth', __name__,
template_folder='templates',
url_prefix='/auth')
from . import routes
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from . import auth_bp
from .forms import LoginForm, RegistrationForm
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# λ‘κ·ΈμΈ λ‘μ§
return redirect(url_for('main.index'))
return render_template('auth/login.html', form=form)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# νμκ°μ
λ‘μ§
flash('νμκ°μ
μ΄ μλ£λμμ΅λλ€.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth_bp.route('/logout')
def logout():
# λ‘κ·Έμμ λ‘μ§
return redirect(url_for('main.index'))
# app/main/__init__.py
from flask import Blueprint
main_bp = Blueprint('main', __name__,
template_folder='templates')
from . import routes
# app/main/routes.py
from flask import render_template
from . import main_bp
@main_bp.route('/')
def index():
return render_template('main/index.html')
@main_bp.route('/about')
def about():
return render_template('main/about.html')
7.3 Application Factory¶
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# νμ₯ μ΄κΈ°ν
db.init_app(app)
migrate.init_app(app, db)
# λΈλ£¨νλ¦°νΈ λ±λ‘
from app.auth import auth_bp
from app.main import main_bp
from app.api import api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(api_bp, url_prefix='/api')
# μλ¬ νΈλ€λ¬ λ±λ‘
from app.errors import register_error_handlers
register_error_handlers(app)
return app
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
class ProductionConfig(Config):
DEBUG = False
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
# run.py
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
8. RESTful API¶
8.1 JSON Responses¶
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
# μν λ°μ΄ν°
books = [
{'id': 1, 'title': 'Flask Web Development', 'author': 'Miguel Grinberg'},
{'id': 2, 'title': 'Python Crash Course', 'author': 'Eric Matthes'},
]
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify({'books': books})
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404)
return jsonify(book)
@app.route('/api/books', methods=['POST'])
def create_book():
if not request.json or 'title' not in request.json:
abort(400)
book = {
'id': books[-1]['id'] + 1 if books else 1,
'title': request.json['title'],
'author': request.json.get('author', '')
}
books.append(book)
return jsonify(book), 201
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404)
if not request.json:
abort(400)
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
return jsonify(book)
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404)
books.remove(book)
return '', 204
# μλ¬ νΈλ€λ¬
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request'}), 400
8.2 API Authentication (JWT)¶
pip install flask-jwt-extended
from flask import Flask, jsonify, request
from flask_jwt_extended import (
JWTManager, create_access_token, create_refresh_token,
jwt_required, get_jwt_identity, get_jwt
)
from datetime import timedelta
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-jwt-secret'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30)
jwt = JWTManager(app)
# ν ν° λΈλ리μ€νΈ (λ‘κ·Έμμλ ν ν°)
blacklist = set()
@jwt.token_in_blocklist_loader
def check_if_token_in_blocklist(jwt_header, jwt_payload):
return jwt_payload['jti'] in blacklist
@app.route('/api/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
# μ¬μ©μ μΈμ¦ λ‘μ§
user = authenticate(username, password)
if not user:
return jsonify({'error': 'Invalid credentials'}), 401
access_token = create_access_token(identity=user.id)
refresh_token = create_refresh_token(identity=user.id)
return jsonify({
'access_token': access_token,
'refresh_token': refresh_token
})
@app.route('/api/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
identity = get_jwt_identity()
access_token = create_access_token(identity=identity)
return jsonify({'access_token': access_token})
@app.route('/api/logout', methods=['POST'])
@jwt_required()
def logout():
jti = get_jwt()['jti']
blacklist.add(jti)
return jsonify({'message': 'Logged out'})
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
current_user_id = get_jwt_identity()
return jsonify({'user_id': current_user_id})
8.3 API Documentation (Flask-RESTX)¶
pip install flask-restx
from flask import Flask
from flask_restx import Api, Resource, fields
app = Flask(__name__)
api = Api(app, version='1.0', title='Book API',
description='A simple Book API')
ns = api.namespace('books', description='Book operations')
book_model = api.model('Book', {
'id': fields.Integer(readonly=True, description='Unique identifier'),
'title': fields.String(required=True, description='Book title'),
'author': fields.String(required=True, description='Book author'),
})
books = []
@ns.route('/')
class BookList(Resource):
@ns.doc('list_books')
@ns.marshal_list_with(book_model)
def get(self):
'''List all books'''
return books
@ns.doc('create_book')
@ns.expect(book_model)
@ns.marshal_with(book_model, code=201)
def post(self):
'''Create a new book'''
book = api.payload
book['id'] = len(books) + 1
books.append(book)
return book, 201
@ns.route('/<int:id>')
@ns.response(404, 'Book not found')
@ns.param('id', 'The book identifier')
class Book(Resource):
@ns.doc('get_book')
@ns.marshal_with(book_model)
def get(self, id):
'''Get a book by ID'''
book = next((b for b in books if b['id'] == id), None)
if book is None:
api.abort(404, 'Book not found')
return book
@ns.doc('delete_book')
@ns.response(204, 'Book deleted')
def delete(self, id):
'''Delete a book'''
global books
books = [b for b in books if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
# Swagger UI: http://localhost:5000/
9. Error Handling¶
9.1 Error Handlers¶
from flask import Flask, render_template, jsonify, request
app = Flask(__name__)
@app.errorhandler(404)
def not_found(error):
if request.path.startswith('/api/'):
return jsonify({'error': 'Not found'}), 404
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback() # νΈλμμ
λ‘€λ°±
if request.path.startswith('/api/'):
return jsonify({'error': 'Internal server error'}), 500
return render_template('errors/500.html'), 500
@app.errorhandler(403)
def forbidden(error):
if request.path.startswith('/api/'):
return jsonify({'error': 'Forbidden'}), 403
return render_template('errors/403.html'), 403
# 컀μ€ν
μμΈ
class ValidationError(Exception):
def __init__(self, message, status_code=400):
super().__init__()
self.message = message
self.status_code = status_code
@app.errorhandler(ValidationError)
def handle_validation_error(error):
response = jsonify({'error': error.message})
response.status_code = error.status_code
return response
# μ¬μ©
@app.route('/api/validate', methods=['POST'])
def validate():
if 'name' not in request.json:
raise ValidationError('Name is required')
return jsonify({'valid': True})
9.2 Logging¶
import logging
from logging.handlers import RotatingFileHandler
import os
def setup_logging(app):
if not app.debug:
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler(
'logs/app.log',
maxBytes=10240000, # 10MB
backupCount=10
)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
# μ¬μ©
@app.route('/api/action')
def action():
app.logger.info('Action performed by user')
try:
# λ‘μ§
pass
except Exception as e:
app.logger.error(f'Error occurred: {e}')
raise
10. Testing¶
10.1 Testing with pytest¶
# tests/conftest.py
import pytest
from app import create_app, db
@pytest.fixture
def app():
app = create_app('config.TestingConfig')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
# tests/test_routes.py
def test_index(client):
response = client.get('/')
assert response.status_code == 200
assert b'Welcome' in response.data
def test_login_page(client):
response = client.get('/auth/login')
assert response.status_code == 200
def test_login(client):
response = client.post('/auth/login', data={
'username': 'testuser',
'password': 'testpass'
}, follow_redirects=True)
assert response.status_code == 200
def test_api_get_books(client):
response = client.get('/api/books')
assert response.status_code == 200
assert response.content_type == 'application/json'
def test_api_create_book(client):
response = client.post('/api/books',
json={'title': 'Test Book', 'author': 'Test Author'}
)
assert response.status_code == 201
data = response.get_json()
assert data['title'] == 'Test Book'
10.2 Running Tests¶
# ν
μ€νΈ μ€ν
pytest
# μμΈ μΆλ ₯
pytest -v
# 컀λ²λ¦¬μ§
pip install pytest-cov
pytest --cov=app --cov-report=html
11. Deployment¶
11.1 Production Configuration¶
# config.py
import os
class ProductionConfig:
SECRET_KEY = os.environ['SECRET_KEY']
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 보μ μ€μ
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
11.2 Running with Gunicorn¶
pip install gunicorn
# μ€ν
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
# λλ gunicorn.conf.py μ¬μ©
gunicorn -c gunicorn.conf.py "app:create_app()"
# gunicorn.conf.py
bind = '0.0.0.0:8000'
workers = 4
threads = 2
worker_class = 'gthread'
timeout = 120
keepalive = 5
errorlog = 'logs/gunicorn-error.log'
accesslog = 'logs/gunicorn-access.log'
loglevel = 'info'
11.3 Docker Deployment¶
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_APP=run.py
ENV FLASK_ENV=production
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=postgresql://user:pass@db:5432/app
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
12. Practical Example: Blog Application¶
For a complete blog application example with full structure and key files, refer to the examples/Web_Development/flask_blog/ folder.
Practice Problems¶
Exercise 1: Basic CRUD App¶
Create a simple Todo list application: - View list of todos - Add new todo - Mark as completed - Delete
Exercise 2: User Authentication¶
Add user authentication to the above app: - Registration/login/logout - Each user can only see their own todos
Exercise 3: REST API¶
Create a Todo list API: - CRUD endpoints - JWT authentication - Swagger documentation