Files
static/lib/common.py
KWON 5cae6e22c7 refactor: config.yaml 제거 및 환경변수 전용 설정으로 전환
- config.yaml 파일 삭제 (모든 설정을 .env로 이관)
- conf/db.py: 환경변수에서 직접 DB 설정 로드
- lib/common.py: load_config()를 환경변수 기반으로 완전히 재작성
- .env 파일에 모든 설정값 추가 (API, GA4, POS, 예측 가중치 등)
- YAML 의존성 제거, 환경변수만으로 전체 시스템 설정 가능
- 12-factor app 원칙 준수 (설정을 환경변수로 관리)
2025-12-26 17:45:38 +09:00

169 lines
5.9 KiB
Python

# common.py
import os
import yaml
import logging
import time
import glob
from functools import wraps
from typing import Any, Callable
# 로거 설정
def setup_logging(name: str, level: str = 'INFO') -> logging.Logger:
"""
로거 설정 (일관된 포맷 적용)
Args:
name: 로거 이름
level: 로그 레벨 (INFO, DEBUG, WARNING, ERROR)
Returns:
Logger 인스턴스
"""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter(
'[%(asctime)s] %(name)s - %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
return logger
def get_logger(name: str) -> logging.Logger:
"""기존 호환성 유지"""
return setup_logging(name)
def load_config(config_path: str = None) -> dict:
"""
환경변수에서 설정 로드 (config.yaml 대체)
Args:
config_path: 하위 호환성을 위해 유지 (사용 안 함)
Returns:
설정 딕셔너리 (환경변수 기반)
"""
config = {
'database': {
'host': os.getenv('DB_HOST', 'localhost'),
'user': os.getenv('DB_USER', 'firstgarden'),
'password': os.getenv('DB_PASSWORD', 'Fg9576861!'),
'name': os.getenv('DB_NAME', 'firstgarden')
},
'table_prefix': os.getenv('TABLE_PREFIX', 'fg_manager_static_'),
'DATA_API': {
'serviceKey': os.getenv('DATA_API_SERVICE_KEY', ''),
'startDt': os.getenv('DATA_API_START_DATE', '20170101'),
'endDt': os.getenv('DATA_API_END_DATE', '20250701'),
'air': {
'station_name': os.getenv('AIR_STATION_NAMES', '운정').split(',')
},
'weather': {
'stnIds': [int(x) for x in os.getenv('WEATHER_STN_IDS', '99').split(',')]
}
},
'ga4': {
'token': os.getenv('GA4_API_TOKEN', ''),
'property_id': int(os.getenv('GA4_PROPERTY_ID', '384052726')),
'service_account_file': os.getenv('GA4_SERVICE_ACCOUNT_FILE', './conf/service-account-credentials.json'),
'startDt': os.getenv('GA4_START_DATE', '20170101'),
'endDt': os.getenv('GA4_END_DATE', '20990731'),
'max_rows_per_request': int(os.getenv('GA4_MAX_ROWS_PER_REQUEST', '10000'))
},
'POS': {
'VISITOR_CA': os.getenv('VISITOR_CATEGORIES', '입장료,티켓,기업제휴').split(',')
},
'FORECAST_WEIGHT': {
'visitor_forecast_multiplier': float(os.getenv('FORECAST_VISITOR_MULTIPLIER', '0.5')),
'minTa': float(os.getenv('FORECAST_WEIGHT_MIN_TEMP', '1.0')),
'maxTa': float(os.getenv('FORECAST_WEIGHT_MAX_TEMP', '1.0')),
'sumRn': float(os.getenv('FORECAST_WEIGHT_PRECIPITATION', '10.0')),
'avgRhm': float(os.getenv('FORECAST_WEIGHT_HUMIDITY', '1.0')),
'pm25': float(os.getenv('FORECAST_WEIGHT_PM25', '1.0')),
'is_holiday': int(os.getenv('FORECAST_WEIGHT_HOLIDAY', '20'))
},
'max_workers': int(os.getenv('MAX_WORKERS', '4')),
'debug': os.getenv('DEBUG', 'false').lower() == 'true',
'force_update': os.getenv('FORCE_UPDATE', 'false').lower() == 'true',
'upsolution': {
'id': os.getenv('UPSOLUTION_ID', 'firstgarden'),
'code': os.getenv('UPSOLUTION_CODE', '1112'),
'pw': os.getenv('UPSOLUTION_PW', '9999')
}
}
return config
def retry_on_exception(max_retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
"""
지정된 횟수만큼 재시도하는 데코레이터
Args:
max_retries: 최대 재시도 횟수
delay: 재시도 간격 (초)
backoff: 재시도마다 지연 시간 배수
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
logger = logging.getLogger(func.__module__)
last_exception = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
if attempt < max_retries - 1:
wait_time = delay * (backoff ** attempt)
logger.warning(
f"{func.__name__} 재시도 {attempt + 1}/{max_retries} "
f"({wait_time:.1f}초 대기): {e}"
)
time.sleep(wait_time)
else:
logger.error(
f"{func.__name__} 모든 재시도 실패: {e}"
)
raise last_exception
return wrapper
return decorator
def wait_download_complete(download_dir: str, ext: str, timeout: int = 60) -> str:
"""
파일 다운로드 완료 대기
Args:
download_dir: 다운로드 디렉토리
ext: 파일 확장자 (예: 'xlsx', 'csv')
timeout: 대기 시간 (초)
Returns:
다운로드된 파일 경로
Raises:
TimeoutError: 지정 시간 내 파일이 나타나지 않을 때
"""
logger = logging.getLogger(__name__)
ext = ext.lstrip('.')
for i in range(timeout):
files = glob.glob(os.path.join(download_dir, f"*.{ext}"))
if files:
logger.info(f"다운로드 완료: {files[0]}")
return files[0]
if i > 0 and i % 10 == 0:
logger.debug(f"다운로드 대기 중... ({i}초 경과)")
time.sleep(1)
raise TimeoutError(
f"파일 다운로드 대기 시간 초과 ({timeout}초): {download_dir}/*.{ext}"
)