import logging import os from pathlib import Path from typing import Dict, Any, List import uvicorn from config_file import read_yaml APP_VERSION = "1.0" SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) CONFIGS_DIR = os.path.join(SCRIPT_PATH, "configs") CONFIG_FILE_NAME = os.path.join(CONFIGS_DIR, "config.yaml") TEMPLATES_DIR = os.path.join(SCRIPT_PATH, "templates") STATIC_DIR = os.path.join(SCRIPT_PATH, "static") TEST_DIR = os.path.join(SCRIPT_PATH, "test") SECRETS_NAME = 'secrets.yaml' CONFIG: Dict[str, Any] = {} LOG_FORMAT = '%(asctime)s [%(levelname)s] - %(name)s: %(funcName)s[%(lineno)d] - %(message)s' API_KEY = '' IN_MEMORY_LOGS = False IN_MEMORY_LOGS_LEN = 100 HOST = '0.0.0.0' PORT: int = 8000 IS_DEBUG = False TG_TOKEN = "" TG_CHAT = "" USE_MATRIX = False MX_SERVER = "" MX_ROOM = "" MX_TOKEN = "" ACCOUNT = "" SETTLEMENT = "" STREET = "" HOUSE = "" BUILDING_PART_NUMBER = "" APARTMENT = "" PROCESS_START_HOUR = 6 PROCESS_STOP_HOUR = 23 PROCESSING_MINUTES: List[int] = [5, 15, 30, 45, 55] HTTP_TIMEOUT = 30 # seconds, for all outgoing requests def read_app_config(): global CONFIG if not CONFIG: config_path = Path(CONFIG_FILE_NAME) if not config_path.exists(): LOG.error(f"{CONFIG_FILE_NAME} not found!") raise FileNotFoundError(f"{CONFIG_FILE_NAME} not found") CONFIG = read_yaml(CONFIG_FILE_NAME, SECRETS_NAME) def get_section(cfg: Dict[str, Any], *path: str) -> Dict[str, Any]: """Returns nested section of config or empty dict if it is absent or not a mapping""" section = cfg for key in path: section = section.get(key) if isinstance(section, dict) else None if not isinstance(section, dict): return {} return section def as_bool(value: Any) -> bool: if isinstance(value, bool): return value return str(value).strip().lower() in ('true', 'yes', 'on', '1') def as_int(value: Any, default: int, min_value: int = None, max_value: int = None) -> int: try: res = int(value) except (TypeError, ValueError): LOG.warning(f'Wrong integer value in config: {value!r}, default {default} is used') return default if (min_value is not None and res < min_value) or (max_value is not None and res > max_value): LOG.warning(f'Value {res} is out of range [{min_value}, {max_value}], default {default} is used') return default return res def as_str(value: Any) -> str: return '' if value is None else str(value) def parse_config(): global API_KEY, IN_MEMORY_LOGS, IN_MEMORY_LOGS_LEN, HOST, PORT, IS_DEBUG, \ TG_TOKEN, TG_CHAT, \ USE_MATRIX, MX_SERVER, MX_ROOM, MX_TOKEN, \ ACCOUNT, SETTLEMENT, STREET, HOUSE, BUILDING_PART_NUMBER, APARTMENT, \ PROCESS_START_HOUR, PROCESS_STOP_HOUR app_section = get_section(CONFIG, 'application') logs_section = get_section(app_section, 'logs_ep') process_section = get_section(app_section, 'process') tg_section = get_section(CONFIG, 'notifications', 'tg') mx_section = get_section(CONFIG, 'notifications', 'matrix') data_section = get_section(CONFIG, 'data') HOST = as_str(app_section.get('host')) or '0.0.0.0' PORT = as_int(app_section.get('port', 8000), 8000, 1, 65535) API_KEY = as_str(app_section.get('api-key')) IN_MEMORY_LOGS = as_bool(logs_section.get('enabled', False)) IN_MEMORY_LOGS_LEN = as_int(logs_section.get('max_records', 100), 100, 1) IS_DEBUG = as_bool(app_section.get('debug', False)) TG_TOKEN = as_str(tg_section.get('token')) TG_CHAT = as_str(tg_section.get('chat_id')) USE_MATRIX = as_bool(mx_section.get('use_matrix', False)) MX_SERVER = as_str(mx_section.get('mx_server')).rstrip('/') MX_ROOM = as_str(mx_section.get('mx_room_id')) MX_TOKEN = as_str(mx_section.get('mx_access_token')) ACCOUNT = as_str(data_section.get('account')) SETTLEMENT = as_str(data_section.get('settlement')) STREET = as_str(data_section.get('street')) HOUSE = as_str(data_section.get('house')) BUILDING_PART_NUMBER = as_str(data_section.get('building_part_number')) APARTMENT = as_str(data_section.get('apartment')) PROCESS_START_HOUR = as_int(process_section.get('start_hour', 6), 6, 0, 23) PROCESS_STOP_HOUR = as_int(process_section.get('stop_hour', 23), 23, 1, 24) if PROCESS_START_HOUR >= PROCESS_STOP_HOUR: LOG.warning(f'start_hour {PROCESS_START_HOUR} >= stop_hour {PROCESS_STOP_HOUR}, stop_hour 24 is used') PROCESS_STOP_HOUR = 24 if not TG_TOKEN or not TG_CHAT: LOG.warning('Telegram token or chat_id is not configured') if USE_MATRIX and not (MX_SERVER and MX_ROOM and MX_TOKEN): LOG.warning('Matrix is enabled but not fully configured') def get_uvicorn_config(): if HOST and PORT: cfg = { "host": f"{HOST}", "port": PORT, "access_log": True, "log_level": "info", "log_config": os.path.join(CONFIGS_DIR, "log.yaml") } else: cfg = { "host": "127.0.0.1", "port": 8000, "reload": True, "access_log": False, "log_level": "info", "log_config": os.path.join(CONFIGS_DIR, "log.yaml") } LOG.debug(f'got uvicorn_config: {cfg}') return cfg def log_config(): # Not used due to not working cfg = uvicorn.config.LOGGING_CONFIG cfg["formatters"]["access"]["fmt"] = LOG_FORMAT return cfg LOG_LIST: List[str] = [] class ListHandler(logging.Handler): def emit(self, record): if not IN_MEMORY_LOGS: return try: LOG_LIST.append(self.format(record)) if len(LOG_LIST) > IN_MEMORY_LOGS_LEN: del LOG_LIST[:len(LOG_LIST) - IN_MEMORY_LOGS_LEN] except Exception: self.handleError(record) ### LOG = logging.getLogger(__name__) handler = ListHandler() handler.setFormatter(logging.Formatter(LOG_FORMAT)) LOG.addHandler(handler) def init_config(): read_app_config() parse_config() if __name__ == '__main__': init_config()