[FEAT] (프로젝트 구성): 초기 프로젝트 구성
v0.1 (2025-11-13) - Flask, Gunicorn, gevent, MariaDB, Redis, Swagger, logging 기본 구성 완료;
This commit is contained in:
37
.env.example
Normal file
37
.env.example
Normal file
@@ -0,0 +1,37 @@
|
||||
# 환경 설정
|
||||
DEBUG=True
|
||||
|
||||
# 서버 설정
|
||||
HOST=0.0.0.0
|
||||
PORT=5000
|
||||
|
||||
# 데이터베이스 설정
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
DB_POOL_SIZE=10
|
||||
DB_MAX_OVERFLOW=20
|
||||
DB_POOL_RECYCLE=3600
|
||||
|
||||
# JWT 설정
|
||||
JWT_SECRET_KEY=
|
||||
JWT_ACCESS_TOKEN_HOURS=24
|
||||
JWT_REFRESH_TOKEN_DAYS=30
|
||||
|
||||
# CORS 설정
|
||||
CORS_ORIGINS=*
|
||||
|
||||
# Redis 설정
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# 로깅 설정
|
||||
LOG_DIR=logs
|
||||
LOG_LEVEL_TEXT=DEBUG
|
||||
LOG_PREFIX=
|
||||
MAX_LOG_SIZE=10485760
|
||||
LOG_BACKUP_COUNT=5
|
||||
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Flask
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# NuriQ
|
||||
|
||||
Flask 기반 실시간 퀴즈 플랫폼 백엔드 API (Gunicorn + gevent)
|
||||
85
src/app.py
Normal file
85
src/app.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Flask 애플리케이션 메인 파일
|
||||
Gunicorn + gevent 구조
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# non-blocking으로 변경
|
||||
from gevent import monkey
|
||||
monkey.patch_all()
|
||||
|
||||
# 프로젝트 루트 경로 추가
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from flask import Flask, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
from extensions import db, migrate, jwt, redis_cli, custom_logger
|
||||
from blueprints import register_blueprints
|
||||
|
||||
import settings
|
||||
import constants
|
||||
|
||||
def create_app(config: Optional[Dict[str, Any]] = None) -> Flask:
|
||||
"""Flask 애플리케이션 팩토리"""
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config.from_object(settings)
|
||||
|
||||
if config:
|
||||
app.config.update(config)
|
||||
|
||||
# 로깅 설정
|
||||
logger = custom_logger(f"{settings.LOG_PREFIX}main_app")
|
||||
|
||||
# Extensions 초기화
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
jwt.init_app(app)
|
||||
|
||||
# Redis 초기화
|
||||
redis_connected = redis_cli.start()
|
||||
if not redis_connected:
|
||||
logger.warning("Redis 연결에 실패하였습니다. Redis 없이 실행됩니다.")
|
||||
|
||||
# CORS 등록
|
||||
CORS(app, origins=settings.CORS_ORIGINS)
|
||||
|
||||
# 에러 핸들러 등록
|
||||
|
||||
# 블루프린트 등록
|
||||
register_blueprints(app)
|
||||
|
||||
@app.route('/api/v1/health')
|
||||
def health_check():
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'database': 'connected' if db.engine else 'disconnected',
|
||||
'redis': 'connected' if redis_cli.is_connected() else 'disconnected'
|
||||
})
|
||||
|
||||
return app
|
||||
|
||||
def main() -> None:
|
||||
"""애플리케이션 실행"""
|
||||
app = create_app()
|
||||
|
||||
# 로그 정보
|
||||
|
||||
app.run(
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
debug=settings.DEBUG,
|
||||
use_reloader=False
|
||||
)
|
||||
|
||||
# Gunicorn이 import할 app 인스턴스
|
||||
app = create_app()
|
||||
|
||||
if __name__ =='__main__':
|
||||
main()
|
||||
|
||||
14
src/blueprints/__init__.py
Normal file
14
src/blueprints/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Blueprints 패키지
|
||||
Flask Blueprint 관리
|
||||
"""
|
||||
|
||||
from flask import Flask
|
||||
|
||||
def register_blueprints(app: Flask) -> None:
|
||||
|
||||
from utils.swagger_config import api_blueprint
|
||||
app.register_blueprint(api_blueprint, url_prefix='/api/v1')
|
||||
|
||||
|
||||
__all__ = ['register_blueprints']
|
||||
6
src/constants.py
Normal file
6
src/constants.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# API INFO
|
||||
API_VERSION = "v0.1"
|
||||
API_DATE = 2025-11-13
|
||||
|
||||
# BASE DATE FORMAT
|
||||
DATE_FORMAT = "%Y-%m-%d %H:%M:%S %z"
|
||||
28
src/extensions.py
Normal file
28
src/extensions.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
모든 외부 모듈, 서비스의 초기화
|
||||
"""
|
||||
|
||||
import settings
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_jwt_extended import JWTManager
|
||||
|
||||
from utils.logger_manager import *
|
||||
|
||||
from modules.redis.redis_facade import redis_cli
|
||||
|
||||
logger = custom_logger(f"{settings.LOG_PREFIX})extenstions")
|
||||
|
||||
try:
|
||||
|
||||
logging.info("Extensions 초기화 시작 .. ")
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
jwt = JWTManager()
|
||||
|
||||
logging.info("Extensions 초기화 완료 .. ")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f" Fail Start Extensions.py => {str(e)}")
|
||||
53
src/gunicorn_config.py
Normal file
53
src/gunicorn_config.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Gunicorn 설정 파일
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 프로젝트 루트 경로 추가
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import settings
|
||||
|
||||
# Worker 설정
|
||||
workers = settings.GUNICORN_WORKERS
|
||||
worker_class = settings.GUNICORN_WORKER_CLASS
|
||||
worker_connections = settings.GUNICORN_WORKER_CONNECTIONS
|
||||
threads = settings.GUNICORN_THREADS
|
||||
|
||||
# 서버 바인딩
|
||||
bind = f'{settings.HOST}:{settings.PORT}'
|
||||
|
||||
# 타임아웃 설정
|
||||
timeout = settings.GUNICORN_TIMEOUT
|
||||
keepalive = settings.GUNICORN_KEEPALIVE
|
||||
graceful_timeout = settings.GUNICORN_GRACEFUL_TIMEOUT
|
||||
|
||||
# 재시작 설정 (메모리 누수 방지)
|
||||
max_requests = settings.GUNICORN_MAX_REQUESTS
|
||||
max_requests_jitter = settings.GUNICORN_MAX_REQUESTS_JITTER
|
||||
|
||||
# 로깅 설정
|
||||
accesslog = settings.GUNICORN_ACCESSLOG
|
||||
errorlog = settings.GUNICORN_ERRORLOG
|
||||
loglevel = settings.GUNICORN_LOGLEVEL
|
||||
|
||||
# 프로세스 네임
|
||||
proc_name = "nuriq_server"
|
||||
|
||||
# hook 함수
|
||||
def on_starting(server):
|
||||
"""
|
||||
Gunicorn 마스터 프로세스가 시작될 때 한번만 실행
|
||||
워커가 생성되기 전 실행, 전역 초기화 작업
|
||||
"""
|
||||
server.log.info("[Gunicorn] 마스터 프로세스 시작 중 ...")
|
||||
|
||||
# Flask 앱 컨텍스트 내에서 초기화 작업 수행
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
|
||||
def post_worker_init(worker):
|
||||
"""각 워커 프로세스 초기화된 후 사용"""
|
||||
worker.log.info(f"Worker {worker.pid} 초기화 완료")
|
||||
0
src/models/__init__.py
Normal file
0
src/models/__init__.py
Normal file
0
src/modules/__init__.py
Normal file
0
src/modules/__init__.py
Normal file
66
src/modules/redis/redis_facade.py
Normal file
66
src/modules/redis/redis_facade.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
Redis 클라이언트 전체 관리 모듈
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
import redis
|
||||
import settings
|
||||
import logging
|
||||
|
||||
class RedisFacadeOpr():
|
||||
"""Redis 연결 및 작업을 관리하는 Facade 클래스"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Redis 설정 초기화"""
|
||||
self.redis_config = {
|
||||
'host': settings.REDIS_HOST,
|
||||
'port': settings.REDIS_PORT,
|
||||
'password': settings.REDIS_PASSWORD,
|
||||
'decode_responses': True, # auto-decode response
|
||||
'db': settings.REDIS_DB
|
||||
}
|
||||
|
||||
self.redis_client: Optional[redis.StrictRedis] = None
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Redis 연결 시작
|
||||
|
||||
Returns:
|
||||
bool: 연결 성공 시 True, 실패 시 False
|
||||
"""
|
||||
try:
|
||||
# Redis Client 연결
|
||||
self.redis_client = redis.StrictRedis(**self.redis_config)
|
||||
# 연결 테스트
|
||||
self.redis_client.ping()
|
||||
logging.info("Redis connection established successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to connect to Redis: {str(e)}")
|
||||
self.redis_client = None
|
||||
return False
|
||||
|
||||
def reset_database(self) -> None:
|
||||
"""Redis 데이터베이스 초기화"""
|
||||
if self.redis_client:
|
||||
self.redis_client.flushdb()
|
||||
logging.info("Redis database flushed")
|
||||
else:
|
||||
logging.warning("Cannot reset database: Redis client not connected")
|
||||
|
||||
def refresh_init_database(self) -> None:
|
||||
"""개별 데이터 초기화"""
|
||||
pass
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Redis 연결 상태 확인"""
|
||||
if not self.redis_client:
|
||||
return False
|
||||
try:
|
||||
self.redis_client.ping()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
redis_cli = RedisFacadeOpr()
|
||||
0
src/repositories/__init__.py
Normal file
0
src/repositories/__init__.py
Normal file
35
src/requirements.txt
Normal file
35
src/requirements.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# Flask Core
|
||||
Flask
|
||||
Flask-RESTful
|
||||
Flask-SQLAlchemy
|
||||
Flask-Migrate
|
||||
Flask-CORS
|
||||
Flask-JWT-Extended
|
||||
flask_restx
|
||||
|
||||
# Database
|
||||
PyMySQL
|
||||
SQLAlchemy
|
||||
|
||||
# HTTP Client
|
||||
requests
|
||||
|
||||
# Background Tasks
|
||||
redis
|
||||
|
||||
# Environment
|
||||
python-dotenv
|
||||
|
||||
# Utilities
|
||||
psutil
|
||||
|
||||
# Password Hashing
|
||||
bcrypt
|
||||
|
||||
# Validation
|
||||
marshmallow
|
||||
pydantic
|
||||
|
||||
# WSGI Server
|
||||
gunicorn
|
||||
gevent
|
||||
67
src/run_server.sh
Normal file
67
src/run_server.sh
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
##############################################################
|
||||
# NuriQ 서버 실행 스크립트
|
||||
#
|
||||
# 사용법:
|
||||
# ./run_server.sh [모드]
|
||||
#
|
||||
# 모드:
|
||||
# dev - 개발 모드 (Flask built-in server)
|
||||
# prod - 운영 모드 (Gunicorn + gevent)
|
||||
##############################################################
|
||||
|
||||
set -e #에러 발생시 스크립트 중단
|
||||
|
||||
# 색상 정리
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 현재 스크립트 디렉토리로 이동
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 모드 설정 (기본값: prod)
|
||||
MODE="${1:-prod}"
|
||||
|
||||
echo -e "${BLUE}========================================="
|
||||
echo -e "${BLUE} NuriQ Server Start${NC} "
|
||||
echo -e "${BLUE}========================================="
|
||||
|
||||
# 가상환경 활성화 확인
|
||||
if [ -z "$VIRTUAL_ENV" ]; then
|
||||
echo -e "${YELLOW} Virtual environment not activated${NC}"
|
||||
echo -e "${YELLOW} Consider running: source .venv/bin/activate${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# 모드에 따른 처리
|
||||
case "$MODE" in
|
||||
dev)
|
||||
echo -e "${GREEN} 개발 모드 시작 중 ...${NC}"
|
||||
echo -e "${GREEN} Flask built-in server 사용 중 ...${NC}"
|
||||
echo ""
|
||||
export DEBUG=True
|
||||
gunicorn -c ./gunicorn_config.py app:app
|
||||
;;
|
||||
|
||||
prod)
|
||||
echo -e "${GREEN} 운영 모드 시작 중 ...${NC}"
|
||||
echo -e "${YELLOW} Gunicorn + gevent 사용 중 ...${NC}"
|
||||
echo ""
|
||||
gunicorn -c ./gunicorn_config.py \
|
||||
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
||||
app:app
|
||||
;;
|
||||
*)
|
||||
echo -e "{$RED} 알 수 없는 모드: $MODE${NC}"
|
||||
echo ""
|
||||
echo "사용 가능한 모드"
|
||||
echo " dev - 개발 모드 (Flask built-in server)"
|
||||
echo " prod - 운영 모드 (Gunicorn + gevent)"
|
||||
echo ""
|
||||
echo "사용: $0 [mode]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
0
src/services/__init__.py
Normal file
0
src/services/__init__.py
Normal file
80
src/settings.py
Normal file
80
src/settings.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
애플리케이션 설정 모듈
|
||||
환경 변수 및 전역 설정 관리
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from dotenv import load_dotenv
|
||||
from urllib.parse import quote_plus
|
||||
import multiprocessing
|
||||
|
||||
# .env 파일 로드
|
||||
load_dotenv()
|
||||
|
||||
# 환경설정
|
||||
DEBUG = os.getenv('DEBUG', False)
|
||||
|
||||
# 서버
|
||||
HOST = os.getenv('HOST', '0.0.0.0')
|
||||
PORT = int(os.getenv('PORT', '5000'))
|
||||
|
||||
# 데이터베이스 설정
|
||||
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = int(os.getenv('DB_PORT', 5000))
|
||||
DB_USER = os.getenv('DB_USER', 'root')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||
DB_NAME = os.getenv('DB_NAME', 'test')
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SQLALCHEMY_ECHO = DEBUG
|
||||
SQLALCHEMY_POOL_SIZE = int(os.getenv('DB_POOL_SIZE', 10))
|
||||
SQLALCHEMY_MAX_OVERFLOW = int(os.getenv('DB_MAX_OVERFLOW', 20))
|
||||
SQLALCHEMY_POOL_RECYCLE = int(os.getenv('DB_POOL_RECYCLE', 3600))
|
||||
|
||||
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-this-in-production')
|
||||
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=int(os.getenv('JWT_ACCESS_TOKEN_HOURS', 24)))
|
||||
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=int(os.getenv('JWT_REFRESH_TOKEN_DAYS', 30)))
|
||||
JWT_TOKEN_LOCATION = ['headers']
|
||||
JWT_HEADER_NAME = 'Authorization'
|
||||
JWT_HEADER_TYPE = 'Bearer'
|
||||
JWT_ENCODE_JTI = True # JWT ID 포함 (블랙리스트 추적용)
|
||||
|
||||
# CORS 설정
|
||||
CORS_ORIGINS = os.getenv('CORS_ORIGINS', '*').split(',')
|
||||
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization']
|
||||
CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
|
||||
|
||||
# Redis 설정
|
||||
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
||||
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||
REDIS_DB = int(os.getenv('REDIS_DB', 0))
|
||||
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)
|
||||
|
||||
# 로깅 설정
|
||||
LOG_DIR = os.getenv('LOG_DIR', 'logs')
|
||||
LOG_PREFIX = os.getenv('LOG_PREFIX', 'default.')
|
||||
LOG_LEVEL_TEXT = os.getenv('LOG_LEVEL_TEXT', 'INFO' if not DEBUG else 'DEBUG')
|
||||
MAX_LOG_SIZE = int(os.getenv('MAX_LOG_SIZE', 10 * 1024 * 1024)) # 10MB
|
||||
LOG_BACKUP_COUNT = int(os.getenv('LOG_BACKUP_COUNT', 5))
|
||||
|
||||
# Worker 설정
|
||||
GUNICORN_WORKERS = int(os.getenv('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))
|
||||
GUNICORN_WORKER_CLASS = os.getenv('GUNICORN_WORKER_CLASS', 'gevent') # gevent for async I/O
|
||||
GUNICORN_WORKER_CONNECTIONS = int(os.getenv('GUNICORN_WORKER_CONNECTIONS', 1000))
|
||||
GUNICORN_THREADS = int(os.getenv('GUNICORN_THREADS', 1)) # gevent 사용 시 무시됨
|
||||
|
||||
# 타임아웃 설정
|
||||
GUNICORN_TIMEOUT = int(os.getenv('GUNICORN_TIMEOUT', 30)) # 요청 타임아웃 (초)
|
||||
GUNICORN_KEEPALIVE = int(os.getenv('GUNICORN_KEEPALIVE', 5)) # Keep-Alive 연결 유지 시간
|
||||
GUNICORN_GRACEFUL_TIMEOUT = int(os.getenv('GUNICORN_GRACEFUL_TIMEOUT', 30)) # Graceful shutdown 대기 시간
|
||||
|
||||
# 재시작 설정 (메모리 누수 방지)
|
||||
GUNICORN_MAX_REQUESTS = int(os.getenv('GUNICORN_MAX_REQUESTS', 1000))
|
||||
GUNICORN_MAX_REQUESTS_JITTER = int(os.getenv('GUNICORN_MAX_REQUESTS_JITTER', 50))
|
||||
|
||||
# 로깅 설정
|
||||
GUNICORN_ACCESSLOG = os.getenv('GUNICORN_ACCESSLOG', '-') # '-' = stdout
|
||||
GUNICORN_ERRORLOG = os.getenv('GUNICORN_ERRORLOG', '-')
|
||||
GUNICORN_LOGLEVEL = os.getenv('GUNICORN_LOGLEVEL', 'info')
|
||||
0
src/test/__init__.py
Normal file
0
src/test/__init__.py
Normal file
100
src/utils/logger_manager.py
Normal file
100
src/utils/logger_manager.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import constants
|
||||
|
||||
import sys, time
|
||||
import builtins, logging
|
||||
import settings
|
||||
|
||||
# base logging format
|
||||
log_format = "[%(asctime)s] [%(levelname)s] (%(name)s) %(message)s"
|
||||
|
||||
# date-time format
|
||||
date_format = constants.DATE_FORMAT
|
||||
|
||||
# formatter for logger
|
||||
formatter = logging.Formatter(log_format, date_format)
|
||||
|
||||
# class for redirect STD to Logging
|
||||
class StdLogRedirect:
|
||||
|
||||
def __init__(self, stream, formatter, level=logging.ERROR, name="stderr"):
|
||||
self.stream = stream
|
||||
self.formatter = formatter
|
||||
self.level = level
|
||||
self.name = name
|
||||
self._buf = ""
|
||||
|
||||
def _emit_line(self, line: str):
|
||||
rec = logging.LogRecord(
|
||||
name=self.name,
|
||||
level=self.level,
|
||||
pathname="<stderr>",
|
||||
lineno=0,
|
||||
msg=line,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
sys.stdout.write(self.formatter.format(rec) + "\n")
|
||||
|
||||
def write(self, s):
|
||||
if isinstance(s, bytes):
|
||||
s = s.decode(errors="replace")
|
||||
self._buf += s
|
||||
while True:
|
||||
idx = self._bug.find("\n")
|
||||
if idx == -1:
|
||||
break
|
||||
line = self._buf[:idx+1:]
|
||||
self._buf = self._buf[idx+1:]
|
||||
if line:
|
||||
self._emit_line(line)
|
||||
|
||||
def flush(self):
|
||||
if self._buf:
|
||||
self._emit_line(self._buf.rstrip("\r"))
|
||||
self._buf = ""
|
||||
self.stream.flush()
|
||||
|
||||
def isatty(self):
|
||||
return self.stream.isatty()
|
||||
|
||||
# redirect STDERR to STDOUT using usual Logging format
|
||||
sys.stderr = StdLogRedirect(sys.stderr, formatter, level=logging.ERROR, name="external.stderr")
|
||||
|
||||
# redirect print to Logging
|
||||
_builtin_print = print
|
||||
def print_as_log(*args, level=logging.NOTSET, name="unknown.print", **kwargs):
|
||||
record = logging.LogRecord(
|
||||
name=name,
|
||||
level=level,
|
||||
pathname=__file__,
|
||||
lineno=0,
|
||||
msg=" ".join(str(a) for a in args),
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
_builtin_print(formatter.format(record), **kwargs)
|
||||
builtins.print = print_as_log
|
||||
|
||||
# logging basic config
|
||||
logging.basicConfig(
|
||||
level=settings.LOG_LEVEL_TEXT,
|
||||
format=log_format,
|
||||
datefmt=date_format,
|
||||
stream=sys.stdout
|
||||
)
|
||||
|
||||
# create own logger class to prevent init custom loggers by other libs
|
||||
class GatekeeperLogger(logging.Logger):
|
||||
def assHandler(self, h):
|
||||
# only 'root', '__main__' ans own loggers will be accepted
|
||||
if self.name == "root" or self.name.startswith(('LOG_PREFIX'), "__main__"):
|
||||
return super().addHandler(h)
|
||||
|
||||
logging.setLoggerClass(GatekeeperLogger)
|
||||
|
||||
def custom_logger(log_prefix: str):
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(log_prefix)
|
||||
logger.setLevel(settings.LOG_LEVEL_TEXT)
|
||||
return logger
|
||||
34
src/utils/swagger_config.py
Normal file
34
src/utils/swagger_config.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Swagger 설정
|
||||
"""
|
||||
|
||||
# Third-Party Library Imports
|
||||
from flask import Blueprint, jsonify
|
||||
from flask_restx import Api
|
||||
from flask_jwt_extended.exceptions import JWTExtendedException, RevokedTokenError
|
||||
|
||||
import constants
|
||||
|
||||
# Swagger API Blueprint 및 설정
|
||||
api_blueprint = Blueprint('api', __name__)
|
||||
|
||||
api = Api(
|
||||
api_blueprint,
|
||||
title='NuriQ API',
|
||||
version=constants.API_VERSION,
|
||||
description='NuriQ REST API 문서',
|
||||
doc='/docs',
|
||||
authorizations={
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'in': 'header',
|
||||
'name': 'Authorization',
|
||||
'description': 'JWT 토큰 입력 (예: Bearer <token>)'
|
||||
}
|
||||
},
|
||||
security='Bearer',
|
||||
validate=True
|
||||
)
|
||||
|
||||
# 네임 스페이스 정의 및 API에 추가
|
||||
__all__ = ['api_blueprint', 'api']
|
||||
Reference in New Issue
Block a user