Commit da7dfa3a by 이태호

first commit

parents
data/temp/*
data/upload/*
data/logs/*
**/node_modules
**/__pycache__
web/static/js/**/*.js
*.pyc
.idea
*.pot
## Flask 프로젝트 기본 설정
### 기본 정보
- Flask 2.0.2 이상
- nodejs 11.x 이상
- 기본 모듈과 패키지 구성, 설정 파일 구성 포함
- 폴더 구조
### 초기 설치 필요 패키지
```bash
# 필요 패키지 설치
$> pip install -r requirements.txt
# nvm, node(11버전 이상), npm 설치(Ubuntu18.04 기준)
$> curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
$> nvm install 10
$. nvm use 10
```
### 프로젝트 폴더 상세
```bash
./
├── README.md # this file
├── config # 설정이 포함된 경로
├── data # 각종 데이터 저장에 사용되는 폴더
├── mods # 라이브러리 모듈이 사용되는 폴더
├── requirements.txt # Python 모듈 의존성 정의
├── runweb.py # 웹서비스 실행, 기본 8888 포트로 구동 프로그램
├── test # 테스트 코드 폴더
└── web # 웹 관련 모듈 폴더
```
### 웹프로젝트 폴더 상세
```bash
./web
├── __init__.py # 기본 설정 내용
├── jss # javascript 소스 경로
├── main # 컨트롤러와 서비스 모듈 위치
├── static # image, js, css 등 정적 파일 위치
└── templates # view 템플릿 위치
```
### JS 라이브러리 관련
- jss 폴더에 소스가 있고 webpack 을 이용하여 빌드하면 web/static/js 에 deploy 됨
- `npm install`, `npm run prod`, `npm run debug` 등을 이용하여 사용
```
### 사용 방법
- 빌드 방법이 있긴하지만 절대로 이 프로젝트에 바로 사용하지 않는다.
- 다른 웹 기반 프로젝트 생성 시 최초에 본 프로젝트를 머지해서 사용한다.
- pull request 와 비슷한 방법으로 사용한다.
```bash
# 신규 프로젝트를 git 에서 생성, 예시 프로젝트명 : gogosing
# 생성한 프로젝트 clone
$> git clone https://...../gogosing
$> cd gogosing
# 신규 프로젝트의 remote에 레파지토리 등록, 예시 Alias : flask1st
$> git remote add flask1st https://gitlab.synap.co.kr/innodev/flask1st.git
# flask1st 를 pull 하여 최신 버전 유지
$> git pull flask1st
# flask1st 의 내용을 신규 프로젝트에 merge
$> git merge flask1st/master
# 신규 프로젝트에 생성된 템플릿 추가
$> git push origin
# 옵션) 필요 없는 경우 템플릿 리모트 제거
$> git remote remove flask1st
```
import os
import logging
G_APPNAME = 'EMPTY-APP'
G_VERSION = '0.0.1'
G_EMAIL = 'innodev@synapsoft.co.kr'
class Config(object):
DEBUG = False
TESTING = False
# Information
APPNAME = G_APPNAME
VERSION = G_VERSION
EMAIL = G_EMAIL
# Define the application directory
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
# Temporary Directory
TEMP_DIR = os.path.join(BASE_DIR, 'data/temp')
if os.path.exists(TEMP_DIR) is False:
os.mkdir(TEMP_DIR)
UPLOAD_DIR = os.path.join(BASE_DIR, 'data/upload')
if os.path.exists(UPLOAD_DIR) is False:
os.mkdir(UPLOAD_DIR)
# Define the database - we are working with
# TODO DB 사용시 DB 설정에 따라 아래 값을 수정합니다.
# DB_USER = 'flask'
# DB_PASSWORD = 'ekfrhsk'
# DB_ADDRESS = '127.0.0.1'
# DB_PORT = '3306'
# DB_DATABASE = 'flask1st'
SQLALCHEMY_DATABASE_URI = f'sqlite:///:memory:'
# SQLALCHEMY_DATABASE_URI = f'mysql+mysqlconnector://' \
# f'{DB_USER}:{DB_PASSWORD}@' \
# f'{DB_ADDRESS}:{DB_PORT}/' \
# f'{DB_DATABASE}?charset=utf8'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# SQLALCHEMY_ENGINE_OPTIONS 사용 방법 https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine
# SQLALCHEMY_ENGINE_OPTIONS = {'pool_size': 20,
# 'pool_recycle': -1,
# 'pool_timeout': 30,
# 'max_overflow': 10}
# Application threads. A common general assumption is
# using 2 per available processor cores - to handle
# incoming requests using one and performing background
# operations using the other.
THREADS_PER_PAGE = 4
# Enable protection agains *Cross-site Request Forgery (CSRF)*
CSRF_ENABLED = False
# Use a secure, unique and absolutely secret key for
# signing the data.
CSRF_SESSION_KEY = "SYNAP-INNO-SECRET"
# Secret key for signing cookies
SECRET_KEY = "SYNAP-INNO-COOKIES-SECRET"
# set logdir
LOG_DIR = os.path.join(BASE_DIR, 'data/logs')
if os.path.exists(LOG_DIR) is False:
os.mkdir(LOG_DIR)
LOG_FILE = '{}.log'.format(G_APPNAME)
LOG_LEVEL = logging.DEBUG
# for reverse proxy
ENABLE_PROXY_FIX = True
class ProductionConfig(Config):
LOG_LEVEL = logging.WARN
class DevelopmentConfig(Config):
DEBUG = True
LOG_FILE = '{}-dev.log'.format(G_APPNAME)
class TestingConfig(Config):
TESTING = True
from sqlalchemy import text
DB_ENGINE = None
def init_db_engine(db):
global DB_ENGINE
DB_ENGINE = db.engine
def db_conn():
"""
일반 디비 쿼리 커넥션 생성
사용방법
with db_conn() as conn:
dao.insert_any(conn, data)
"""
global DB_ENGINE
return ExConnection(DB_ENGINE)
def db_trans():
"""
트랜잭션 디비 쿼리 커넥션 생성
사용방법
with db_trans() as conn:
dao.insert_any(conn, data1)
dao.insert_any(conn, data2)
"""
global DB_ENGINE
return ExTransaction(DB_ENGINE)
class ExConnection(object):
def __init__(self, engine):
self.engine = engine
def __enter__(self):
self.conn = self.engine.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# TODO 에러처리
self.conn.close()
return exc_type is None
def execute(self, query):
cur = self.conn.execute(text(query))
return cur
class ExTransaction(ExConnection):
def __init__(self, engine):
super().__init__(engine)
self.trans = None
def __enter__(self):
super().__enter__()
self.trans = self.conn.begin()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# TODO 에러처리
if exc_type is None:
self.trans.commit()
else:
self.trans.rollback()
return super().__exit__(exc_type, exc_val, exc_tb)
# TODO DAO 샘플 코드. 참고하여 작성합니다.
def insert_sample_record(conn, data):
"""
:param conn: DB connection
:param data: 레코드 데이터
:return: INSERT된 row의 primary key
"""
# FIXME 작성시 테이블명(SAMPLE_TABLE), 필드명(SAMPLE_FIELD)을 실제 값에 맞춰 수정합니다.
query = f"INSERT INTO SAMPLE_TABLE(SAMPLE_FIELD) VALUES('{data}')"
return conn.execute(query)
-- write database scheme...
\ No newline at end of file
from mods.db import db_conn, db_trans
from mods.db.sample_dao import *
from mods.utils.logger import logger as log
# TODO DB 커넥션 샘플 코드. 참고하여 작성합니다.
def add_sample_data(data):
"""
:param data: 레코드 데이터
:return: 성공 여부
"""
try:
with db_conn() as conn:
insert_sample_record(conn, data)
except Exception as e:
# FIXME 에러 관련 내용으로 로그를 수정합니다.
log.exception(f'Exception was occured by {str(e)} while add sample.')
return False
return True
# TODO DB 트랜잭션 사용 샘플 코드. 참고하여 작성합니다
def add_sample_data_with_transaction(data_1, data_2):
try:
with db_trans() as conn:
insert_sample_record(conn, data_1)
insert_sample_record(conn, data_2)
except Exception as e:
# FIXME 에러 관련 내용으로 로그를 수정합니다.
log.exception(f'Exception was occured by {str(e)} while add sample with transaction.')
return False
return True
import os
import logging
from logging.handlers import RotatingFileHandler
# logger_name = 'default-logger'
# Default Logger Setting
format_string = '%(asctime)s - %(name)s - %(levelname)s : %(threadName)s(%(thread)s) ' \
'%(funcName)s@%(pathname)s:%(lineno)d %(message)s'
logging.basicConfig(format=format_string)
logger = None
def init_logger(name, log_level, log_dir=None, log_file=None):
"""
init logger
:param log_dir: directory for logging
:param log_file: log file name
:param log_level: log level (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
:return: logger object
"""
global logger
logger = logging.getLogger(name)
logger.setLevel(log_level)
if log_dir and log_file:
# global log_handler
log_handler = RotatingFileHandler(os.path.join(log_dir, log_file), maxBytes=1024*1024*20, backupCount=5)
log_handler.setLevel(log_level)
# create formatter
formatter = logging.Formatter(format_string)
log_handler.setFormatter(formatter)
logger.addHandler(log_handler)
from web import init_app
from config import appcfgs
if __name__ == '__main__':
# ------- Start Server ---------
app = init_app(appcfgs.DevelopmentConfig)
app.run(host='0.0.0.0', threaded=True, port=8888, debug=False)
##### Unit test 실행
```bash
#> python -m unittest test.runner
or
#> python test/runner.py
or
#> python -m unittest discover test
```
##### 의존성 설치
- assertTemplateUsed() 사용을 위해서는 다음 의존성 설치가 필요합니다.
coverage.sh 사용 이전에 아래 의존성 설치 진행 후 커버리지 테스트를 진행 할 수 있도록 합니다.
```bash
#> pip install blinker
```
설치하지 않으면 테스트코드 테스트 진행시 다음과 같은 에러가 발생합니다.
RuntimeError: Your version of Flask doesn't support signals. This requires Flask 0.6+ with the blinker module installed.
##### coverage.sh 사용방법
- 기본실행시 html 형태의 출력결과 생성
- xml 옵션 추가시 xml 결과 파일 생성(jenkins 연동시 사용)
```bash
#> ./coverage.sh
or
#> ./coverage.sh xml
```
##### 주의사항
- test 패키지명 생성할때 앞에 'test_' 붙여줘야 테스트할 패키지 참조할때 경로가 꼬이지 않음
- 테스트 실행시 unittest로 실행 (twist의 trial로 실행하면 안됨, ide 실행환경 주의)
#!/bin/bash
CPWD=$(dirname $(readlink -f $0))
coverage erase
if [ $? -ne 0 ];then
echo "You have to install coverage.py!!!!!, excute 'pip install coverage'!!!!"
exit 1
fi
TEST_DIR=$CPWD
PYTHONPATH=$CPWD:$CPWD/../:$TEST_DIR:$PYTHONPATH
export PYTHONPATH=$PYTHONPATH
MODSRC_DIR=$CPWD/../mods
WEBSRC_DIR=$CPWD/../web
coverage run --source=$MODSRC_DIR,$WEBSRC_DIR--concurrency=thread --branch --append "$TEST_DIR"/runner.py
if [ $? -ne 0 ];then
echo "Test failed!!!!"
exit 1
fi
coverage report -m
if [ $? -ne 0 ];then
echo "An error occured while generating report!!!!"
exit 1
fi
if [ "$1" == xml ];then
rm -f coverage.xml
coverage xml -o coverage.xml
else
rm -rf htmlcov
coverage html
fi
import os
import unittest
loader = unittest.TestLoader()
start_dir = os.path.dirname(os.path.realpath(__file__))
print("TEST Directory - ", start_dir)
suite = loader.discover(start_dir)
runner = unittest.TextTestRunner()
runner.run(suite)
import unittest
from unittest import TestCase, mock
from web import init_app
from config import appcfgs
from mods.service import sample_service
# TODO 샘플 테스트 코드. 참고하여 작성합니다.
class TestSampleService(TestCase):
app = None
@classmethod
def setUpClass(cls):
"""
테스트 클래스가 시작되기 이전에 한 번 호출됨
:return:
"""
TestSampleService.app = init_app(appcfgs.TestingConfig)
sample_service.log = mock.MagicMock()
def setUp(self):
"""
각 테스트 케이스가 시작되기 전에 호출됨
:return:
"""
self.app = TestSampleService.app.test_client()
@mock.patch('mods.service.sample_service.insert_sample_record')
@mock.patch('mods.service.sample_service.db_conn')
def test_add_sample_success(self, mocked_db_conn, mocked_insert_sample_record):
# Given
test_sample_data = 'TEST'
mocked_db_conn.return_value = mock.MagicMock()
mocked_insert_sample_record.return_value = mock.MagicMock()
# When
actual = sample_service.add_sample_data(test_sample_data)
# Then
expected = True
self.assertEqual(expected, actual)
@mock.patch('mods.service.sample_service.insert_sample_record')
@mock.patch('mods.service.sample_service.db_conn')
def test_add_sample_fail(self, mocked_db_conn, mocked_insert_sample_record):
# Given
test_sample_data = 'TEST'
mocked_db_conn.return_value = mock.MagicMock()
mocked_insert_sample_record.side_effect = Exception()
# When
actual = sample_service.add_sample_data(test_sample_data)
# Then
expected = False
self.assertEqual(expected, actual)
if __name__ == '__main__':
unittest.main()
from flask import Flask, g
from flask.json import JSONEncoder
from flask_sqlalchemy import SQLAlchemy
# from werkzeug.contrib.fixers import ProxyFix
# ------- import mods ------------------
from mods.db import init_db_engine
from mods.utils.logger import init_logger
# ------- regist custom json encoder ------------------
import json
from enum import Enum
class DFJsonEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Enum):
return str(obj)
else:
try:
iterable = iter(obj)
except TypeError:
pass
else:
return list(iterable)
return JSONEncoder.default(self, obj)
# ------- Flask Application ------------------
app = Flask(__name__)
already_init = False
def init_app(config_object):
global already_init
if already_init:
print('[Warning] App initialization has been requested multiple times.')
return
else:
already_init = True
app.config.from_object(config_object)
app.json_encoder = DFJsonEncoder
# for Reverse Proxy # TODO Werkzeug 버전이 올라감에 따라 적용 방식이 바뀜
# if app.config.get('ENABLE_PROXY_FIX'):
# app.wsgi_app = ProxyFix(app.wsgi_app)
# ------- database engine ------------------
if not app.config['TESTING']:
init_db_engine(SQLAlchemy(app))
# ------- Logging ------------------
if not app.config['TESTING'] and not app.config['DEBUG']:
init_logger(app.config['APPNAME'], app.config['LOG_LEVEL'], app.config['LOG_DIR'], app.config['LOG_FILE'])
else:
init_logger(app.config['APPNAME'], app.config['LOG_LEVEL'])
# ------- common callback and handler ------------------
return app
# ------- register controller ------------------
from web.main import controller
{
"name": "flask1st",
"version": "0.0.1",
"description": "Flask 1st - Synapsoft",
"main": "main.js",
"scripts": {
"prod": "webpack --mode=production",
"dev": "webpack --mode=development",
"watch": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "synapsoft innodev team",
"devDependencies": {
"@webpack-cli/generators": "^2.4.1",
"prettier": "^2.4.1",
"style-loader": "^3.3.1",
"url-loader": "4.1.1",
"css-loader": "^6.5.1",
"glob-parent": "=5.1.2",
"ansi-regex": "=5.0.1",
"webpack": "^5.67.0",
"webpack-cli": "^4.9.2",
"mini-css-extract-plugin": "2.6.0"
},
"dependencies": {
"admin-lte": "=3.0.5",
"bootstrap": "=4.6.0",
"jquery": "=3.6.0",
"popper.js": "=1.16.1",
"acorn": "=8.0.0",
"tempusdominus-core": "=5.19.0"
}
}
$(document).ready(function() {
// get file list
});
require('bootstrap/dist/css/bootstrap.css');
$(document).ready(function() {
// get file list
});
const path = require("path");
const webpack = require("webpack");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'development',
entry: {
main: ["./src/main/main.js", "jquery", "bootstrap", "admin-lte"],
index: ["./src/index", "jquery", "bootstrap"],
},
output: {
path: path.join(__dirname, "../static/bundle/"),
filename: "[name].js",
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery", // jquery 모듈을 불러온다.
jQuery: "jquery", // jquery 모듈을 불러온다.
"window.jQuery": "jquery" // angular.js 에서 jquery 를 사용시
}),
new MiniCssExtractPlugin({filename: "[name].css"}),
]
};
from web import app
from flask import Flask, render_template, redirect
@app.route('/')
def index():
return redirect('main')
@app.route('/main')
def main():
return render_template("main.html", msg="Hello World!")
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Default Template</title>
<link href="/static/bundle/main.css" rel="stylesheet">
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Default Template</a>
</div>
<ul class="nav navbar-nav">
<!--<li class="active"><a href="#">Home</a></li>-->
<!--<li><a href="#">Page 1</a></li>-->
<!--<li><a href="#">Page 2</a></li>-->
</ul>
<ul class="nav navbar-nav navbar-right">
<!--<li><a href="#"><span class="glyphicon glyphicon-user"></span>About</a></li>-->
</ul>
</div>
</nav>
<!-- Page Content -->
<div class="container">
<div class="row">
<div class="col-lg-3">
<div id='items' class="list-group">
<a href="#" class="list-group-item active">Category 1</a>
<a href="#" class="list-group-item">Category 2</a>
<a href="#" class="list-group-item">Category 3</a>
</div>
</div>
<!-- /.col-lg-3 -->
<div class="col-lg-9">
<div class="card mt-4">
<img class="card-img-top img-fluid" src="" alt="">
<div class="card-body">
<h3 class="card-title">Product Name</h3>
<h4>$24.99</h4>
<p class="card-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sapiente dicta fugit fugiat hic aliquam itaque facere, soluta. Totam id dolores, sint aperiam sequi pariatur praesentium animi perspiciatis molestias iure, ducimus!</p>
<span class="text-warning">&#9733; &#9733; &#9733; &#9733; &#9734;</span>
4.0 stars
</div>
</div>
<!-- /.card -->
<div class="card card-outline-secondary my-4">
<div class="card-header">
Product Reviews
</div>
<div class="card-body">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Omnis et enim aperiam inventore, similique necessitatibus neque non! Doloribus, modi sapiente laboriosam aperiam fugiat laborum. Sequi mollitia, necessitatibus quae sint natus.</p>
<small class="text-muted">Posted by Anonymous on 3/1/17</small>
<hr>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Omnis et enim aperiam inventore, similique necessitatibus neque non! Doloribus, modi sapiente laboriosam aperiam fugiat laborum. Sequi mollitia, necessitatibus quae sint natus.</p>
<small class="text-muted">Posted by Anonymous on 3/1/17</small>
<hr>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Omnis et enim aperiam inventore, similique necessitatibus neque non! Doloribus, modi sapiente laboriosam aperiam fugiat laborum. Sequi mollitia, necessitatibus quae sint natus.</p>
<small class="text-muted">Posted by Anonymous on 3/1/17</small>
<hr>
<a href="#" class="btn btn-success">Leave a Review</a>
</div>
</div>
<!-- /.card -->
</div>
<!-- /.col-lg-9 -->
</div>
</div>
<!-- /.container -->
<!-- Footer -->
<footer class="gray-dark">
<div class="container">
<p class="m-0 text-center text-white">Copyright &copy; Synapsoft.co.kr, 2019</p>
</div>
<!-- /.container -->
</footer>
<!-- script -->
<script src='/static/bundle/main.js'></script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment