diff --git a/README.md b/README.md index 9585b57..3a0ee53 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ if logger_loader.config.extra.http_json_enabled: ) ``` -[**`app.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/app.py): +[**`main.py`**](https://github.com/bybatkhuu/module.python-logging/blob/main/examples/advanced/main.py): ```python from typing import Union @@ -338,28 +338,28 @@ cd ./examples/advanced # Install python dependencies for examples: pip install -r ./requirements.txt -uvicorn app:app --host=0.0.0.0 --port=8000 +uvicorn main:app --host=0.0.0.0 --port=8000 ``` **Output**: ```txt -[2023-09-01 12:37:38.569 +09:00 | TRACE | beans_logging._base:499]: Intercepted modules: ['watchfiles.watcher', 'asyncio', 'watchfiles', 'concurrent', 'dotenv', 'concurrent.futures', 'fastapi', 'dotenv.main', 'uvicorn', 'watchfiles.main']; Muted modules: ['uvicorn.error', 'uvicorn.access']; -[2023-09-01 12:37:38.579 +09:00 | INFO | uvicorn.server:76]: Started server process [22599] -[2023-09-01 12:37:38.579 +09:00 | INFO | uvicorn.lifespan.on:46]: Waiting for application startup. -[2023-09-01 12:37:38.579 +09:00 | INFO | app:21]: Preparing to startup... -[2023-09-01 12:37:38.580 +09:00 | OK | app:22]: Finished preparation to startup. -[2023-09-01 12:37:38.580 +09:00 | INFO | app:23]: API version: 0.0.1-000000 -[2023-09-01 12:37:38.580 +09:00 | INFO | uvicorn.lifespan.on:60]: Application startup complete. -[2023-09-01 12:37:38.582 +09:00 | INFO | uvicorn.server:218]: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) -[2023-09-01 12:37:48.487 +09:00 | DEBUG | anyio._backends._asyncio:807]: [0b9f972939054a58ba10e7a39a12bd21] 127.0.0.1 - "GET / HTTP/1.1" -[2023-09-01 12:37:48.488 +09:00 | OK | anyio._backends._asyncio:807]: [0b9f972939054a58ba10e7a39a12bd21] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 0.5ms -^C[2023-09-01 12:37:51.845 +09:00 | INFO | uvicorn.server:264]: Shutting down -[2023-09-01 12:37:51.949 +09:00 | INFO | uvicorn.lifespan.on:65]: Waiting for application shutdown. -[2023-09-01 12:37:51.951 +09:00 | INFO | app:26]: Praparing to shutdown... -[2023-09-01 12:37:51.952 +09:00 | OK | app:27]: Finished preparation to shutdown. -[2023-09-01 12:37:51.952 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete. -[2023-09-01 12:37:51.953 +09:00 | INFO | uvicorn.server:86]: Finished server process [22599] +[2023-09-01 14:55:11.724 +09:00 | TRACE | beans_logging._base:576]: Intercepted modules: ['watchfiles.watcher', 'dotenv', 'asyncio', 'dotenv.main', 'watchfiles.main', 'concurrent.futures', 'uvicorn', 'fastapi', 'concurrent', 'watchfiles']; Muted modules: ['uvicorn.access', 'uvicorn.error']; +[2023-09-01 14:55:11.740 +09:00 | INFO | uvicorn.server:76]: Started server process [17146] +[2023-09-01 14:55:11.740 +09:00 | INFO | uvicorn.lifespan.on:46]: Waiting for application startup. +[2023-09-01 14:55:11.741 +09:00 | INFO | main:21]: Preparing to startup... +[2023-09-01 14:55:11.741 +09:00 | OK | main:22]: Finished preparation to startup. +[2023-09-01 14:55:11.741 +09:00 | INFO | main:23]: API version: 0.0.1-000000 +[2023-09-01 14:55:11.741 +09:00 | INFO | uvicorn.lifespan.on:60]: Application startup complete. +[2023-09-01 14:55:11.745 +09:00 | INFO | uvicorn.server:218]: Uvicorn running on http://0.0.0.0:9000 (Press CTRL+C to quit) +[2023-09-01 14:55:17.417 +09:00 | DEBUG | anyio._backends._asyncio:833]: [f635ebbc3f2348db9dcff681be1bd52a] 127.0.0.1 - "GET / HTTP/1.1" +[2023-09-01 14:55:17.418 +09:00 | OK | anyio._backends._asyncio:833]: [f635ebbc3f2348db9dcff681be1bd52a] 127.0.0.1 - "GET / HTTP/1.1" 200 17B 0.7ms +^C[2023-09-01 14:55:18.729 +09:00 | INFO | uvicorn.server:264]: Shutting down +[2023-09-01 14:55:18.831 +09:00 | INFO | uvicorn.lifespan.on:65]: Waiting for application shutdown. +[2023-09-01 14:55:18.834 +09:00 | INFO | main:26]: Praparing to shutdown... +[2023-09-01 14:55:18.835 +09:00 | OK | main:27]: Finished preparation to shutdown. +[2023-09-01 14:55:18.837 +09:00 | INFO | uvicorn.lifespan.on:76]: Application shutdown complete. +[2023-09-01 14:55:18.837 +09:00 | INFO | uvicorn.server:86]: Finished server process [17146] ``` --- diff --git a/beans_logging/__init__.py b/beans_logging/__init__.py index bad4689..7bd9b5c 100644 --- a/beans_logging/__init__.py +++ b/beans_logging/__init__.py @@ -2,5 +2,5 @@ from ._base import Logger, logger, LoggerLoader from .schemas import LoggerConfigPM -from ._const import WarnEnum +from ._consts import WarnEnum from .__version__ import __version__ diff --git a/beans_logging/_base.py b/beans_logging/_base.py index 5373d74..cbbcf04 100644 --- a/beans_logging/_base.py +++ b/beans_logging/_base.py @@ -11,7 +11,12 @@ import yaml from loguru import logger from loguru._logger import Logger -from pydantic import validate_call +import pydantic + +if "2.0.0" <= pydantic.__version__: + from pydantic import validate_call +else: + from pydantic import validate_arguments as validate_call ## Internal modules from ._utils import create_dir, deep_merge @@ -168,7 +173,11 @@ def update_config(self, config: Union[LoggerConfigPM, Dict[str, Any]]): """ if isinstance(config, dict): - _config_dict = self.config.model_dump() + if "2.0.0" <= pydantic.__version__: + _config_dict = self.config.model_dump() + else: + _config_dict = self.config.dict() + _merged_dict = deep_merge(_config_dict, config) try: self.config = LoggerConfigPM(**_merged_dict) @@ -219,7 +228,11 @@ def _load_config_file(self): return _new_config_dict = _new_config_dict["logger"] - _config_dict = self.config.model_dump() + if "2.0.0" <= pydantic.__version__: + _config_dict = self.config.model_dump() + else: + _config_dict = self.config.dict() + _merged_dict = deep_merge(_config_dict, _new_config_dict) self.config = LoggerConfigPM(**_merged_dict) except Exception: @@ -240,7 +253,11 @@ def _load_config_file(self): return _new_config_dict = _new_config_dict["logger"] - _config_dict = self.config.model_dump() + if "2.0.0" <= pydantic.__version__: + _config_dict = self.config.model_dump() + else: + _config_dict = self.config.dict() + _merged_dict = deep_merge(_config_dict, _new_config_dict) self.config = LoggerConfigPM(**_merged_dict) except Exception: @@ -252,7 +269,9 @@ def _load_config_file(self): # try: # import toml - # with open(self.config_file_path, "r", encoding="utf-8") as _config_file: + # with open( + # self.config_file_path, "r", encoding="utf-8" + # ) as _config_file: # _new_config_dict = toml.load(_config_file) or {} # if "logger" not in _new_config_dict: # logger.warning( @@ -261,7 +280,11 @@ def _load_config_file(self): # return # _new_config_dict = _new_config_dict["logger"] - # _config_dict = self.config.model_dump() + # if "2.0.0" <= pydantic.__version__: + # _config_dict = self.config.model_dump() + # else: + # _config_dict = self.config.dict() + # _merged_dict = deep_merge(_config_dict, _new_config_dict) # self.config = LoggerConfigPM(**_merged_dict) # except Exception: diff --git a/beans_logging/_const.py b/beans_logging/_const.py deleted file mode 100644 index f8745e9..0000000 --- a/beans_logging/_const.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- - -from enum import Enum - - -class WarnEnum(str, Enum): - RAISE = "RAISE" - LOG = "LOG" - DEBUG = "DEBUG" - IGNORE = "IGNORE" diff --git a/beans_logging/_consts.py b/beans_logging/_consts.py new file mode 100644 index 0000000..20a72c6 --- /dev/null +++ b/beans_logging/_consts.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from enum import Enum + + +class WarnEnum(str, Enum): + ERROR = "ERROR" + ALWAYS = "ALWAYS" + DEBUG = "DEBUG" + IGNORE = "IGNORE" + + +class LogLevelEnum(str, Enum): + TRACE = "TRACE" + DEBUG = "DEBUG" + INFO = "INFO" + SUCCESS = "SUCCESS" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" diff --git a/beans_logging/_utils.py b/beans_logging/_utils.py index 9461522..fc2c17c 100644 --- a/beans_logging/_utils.py +++ b/beans_logging/_utils.py @@ -1,13 +1,19 @@ # -*- coding: utf-8 -*- import os +import sys import copy import errno from loguru import logger -from pydantic import validate_call +import pydantic -from ._const import WarnEnum +if "2.0.0" <= pydantic.__version__: + from pydantic import validate_call +else: + from pydantic import validate_arguments as validate_call + +from ._consts import WarnEnum @validate_call @@ -16,13 +22,13 @@ def create_dir(create_dir: str, warn_mode: WarnEnum = WarnEnum.DEBUG): Args: create_dir (str, required): Create directory path. - warn_mode (str, optional): Warning message mode, for example: 'LOG', 'DEBUG', 'QUIET'. Defaults to "QUIET". + warn_mode (str, optional): Warning message mode, for example: 'ERROR', 'ALWAYS', 'DEBUG', 'IGNORE'. Defaults to "DEBUG". """ if not os.path.isdir(create_dir): try: _message = f"Creaing '{create_dir}' directory..." - if warn_mode == WarnEnum.LOG: + if warn_mode == WarnEnum.ALWAYS: logger.info(_message) elif warn_mode == WarnEnum.DEBUG: logger.debug(_message) @@ -36,7 +42,7 @@ def create_dir(create_dir: str, warn_mode: WarnEnum = WarnEnum.DEBUG): raise _message = f"Successfully created '{create_dir}' directory." - if warn_mode == WarnEnum.LOG: + if warn_mode == WarnEnum.ALWAYS: logger.success(_message) elif warn_mode == WarnEnum.DEBUG: logger.debug(_message) @@ -67,3 +73,28 @@ def deep_merge(dict1: dict, dict2: dict) -> dict: _merged[_key] = copy.deepcopy(_val) return _merged + + +def get_default_logs_dir() -> str: + """Return default logs directory path (current working directory + 'logs'). + + Returns: + str: Default logs directory path. + """ + + return os.path.join(os.getcwd(), "logs") + + +def get_app_name() -> str: + """Return application name (sys.argv[0]). + + Returns: + str: Application name. + """ + + return ( + os.path.splitext(os.path.basename(sys.argv[0]))[0] + .strip() + .replace(" ", "-") + .lower() + ) diff --git a/beans_logging/fastapi/_handlers.py b/beans_logging/fastapi/_handlers.py index a8eae50..36c58da 100644 --- a/beans_logging/fastapi/_handlers.py +++ b/beans_logging/fastapi/_handlers.py @@ -2,7 +2,12 @@ from typing import Union, Callable -from pydantic import validate_call +import pydantic + +if "2.0.0" <= pydantic.__version__: + from pydantic import validate_call +else: + from pydantic import validate_arguments as validate_call from beans_logging import LoggerLoader diff --git a/beans_logging/schemas/__init__.py b/beans_logging/schemas/__init__.py new file mode 100644 index 0000000..4b15a18 --- /dev/null +++ b/beans_logging/schemas/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +import pydantic + +if "2.0.0" <= pydantic.__version__: + from .config import LoggerConfigPM +else: + from .config_v1 import LoggerConfigPM diff --git a/beans_logging/schemas.py b/beans_logging/schemas/config.py similarity index 86% rename from beans_logging/schemas.py rename to beans_logging/schemas/config.py index 89cc9fb..e751a81 100644 --- a/beans_logging/schemas.py +++ b/beans_logging/schemas/config.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -import os -import sys import datetime -from enum import Enum from typing import List from pydantic import ( @@ -15,34 +12,14 @@ ConfigDict, ) - -def _get_logs_dir() -> str: - return os.path.join(os.getcwd(), "logs") - - -def _get_app_name() -> str: - return ( - os.path.splitext(os.path.basename(sys.argv[0]))[0] - .strip() - .replace(" ", "-") - .lower() - ) +from .._consts import LogLevelEnum +from .._utils import get_default_logs_dir, get_app_name class ExtraBaseModel(BaseModel): model_config = ConfigDict(extra="allow") -class LevelEnum(str, Enum): - TRACE = "TRACE" - DEBUG = "DEBUG" - INFO = "INFO" - SUCCESS = "SUCCESS" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - - class StdHandlerPM(ExtraBaseModel): enabled: bool = Field(default=True) @@ -104,7 +81,7 @@ def _check_log_path(self) -> "JsonHandlersPM": class FilePM(ExtraBaseModel): logs_dir: constr(strip_whitespace=True) = Field( - default_factory=_get_logs_dir, min_length=2, max_length=1023 + default_factory=get_default_logs_dir, min_length=2, max_length=1023 ) rotate_size: int = Field( default=10_000_000, ge=1_000, lt=1_000_000_000 # 10MB = 10 * 1000 * 1000 @@ -143,11 +120,11 @@ class ExtraPM(ExtraBaseModel): class LoggerConfigPM(ExtraBaseModel): app_name: constr(strip_whitespace=True) = Field( - default_factory=_get_app_name, + default_factory=get_app_name, min_length=1, max_length=127, ) - level: LevelEnum = Field(default=LevelEnum.INFO) + level: LogLevelEnum = Field(default=LogLevelEnum.INFO) use_backtrace: bool = Field(default=True) use_diagnose: bool = Field(default=False) stream: StreamPM = Field(default_factory=StreamPM) diff --git a/beans_logging/schemas/config_v1.py b/beans_logging/schemas/config_v1.py new file mode 100644 index 0000000..0b8a907 --- /dev/null +++ b/beans_logging/schemas/config_v1.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +import datetime +from typing import List + +from pydantic import BaseModel, constr, Field, validator, root_validator + +from .._consts import LogLevelEnum +from .._utils import get_default_logs_dir, get_app_name + + +class ExtraBaseModel(BaseModel): + class Config: + extra = "allow" + + +class StdHandlerPM(ExtraBaseModel): + enabled: bool = Field(default=True) + + +class StreamPM(ExtraBaseModel): + use_color: bool = Field(default=True) + use_icon: bool = Field(default=False) + format_str: constr(strip_whitespace=True) = Field( + default="[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}", + min_length=3, + max_length=511, + ) + std_handler: StdHandlerPM = Field(default_factory=StdHandlerPM) + + +class LogHandlersPM(ExtraBaseModel): + enabled: bool = Field(default=False) + format_str: constr(strip_whitespace=True) = Field( + default="[{time:YYYY-MM-DD HH:mm:ss.SSS Z} | {level_short:<5} | {name}:{line}]: {message}", + min_length=4, + max_length=511, + ) + log_path: constr(strip_whitespace=True) = Field( + default="{app_name}.std.all.log", min_length=4, max_length=1023 + ) + err_path: constr(strip_whitespace=True) = Field( + default="{app_name}.std.err.log", min_length=4, max_length=1023 + ) + + @root_validator + def _check_log_path(cls, values): + _log_path, _err_path = values.get("log_path"), values.get("err_path") + if _log_path == _err_path: + raise ValueError( + f"`log_path` and `err_path` attributes are same: '{_log_path}', must be different!" + ) + + return values + + +class JsonHandlersPM(ExtraBaseModel): + enabled: bool = Field(default=False) + use_custom: bool = Field(default=False) + log_path: constr(strip_whitespace=True) = Field( + default="{app_name}.json.all.log", min_length=4, max_length=1023 + ) + err_path: constr(strip_whitespace=True) = Field( + default="{app_name}.json.err.log", min_length=4, max_length=1023 + ) + + @root_validator + def _check_log_path(cls, values): + _log_path, _err_path = values.get("log_path"), values.get("err_path") + if _log_path == _err_path: + raise ValueError( + f"`log_path` and `err_path` attributes are same: '{_log_path}', must be different!" + ) + + return values + + +class FilePM(ExtraBaseModel): + logs_dir: constr(strip_whitespace=True) = Field( + default_factory=get_default_logs_dir, min_length=2, max_length=1023 + ) + rotate_size: int = Field( + default=10_000_000, ge=1_000, lt=1_000_000_000 # 10MB = 10 * 1000 * 1000 + ) + rotate_time: datetime.time = Field(datetime.time(0, 0, 0)) + backup_count: int = Field(default=90, ge=1) + encoding: constr(strip_whitespace=True) = Field( + default="utf8", min_length=2, max_length=31 + ) + log_handlers: LogHandlersPM = Field(default_factory=LogHandlersPM) + json_handlers: JsonHandlersPM = Field(default_factory=JsonHandlersPM) + + @validator("rotate_time", pre=True, always=True) + def _check_rotate_time(cls, val): + if val and isinstance(val, str): + val = datetime.time.fromisoformat(val) + return val + + +class AutoLoadPM(ExtraBaseModel): + enabled: bool = Field(default=True) + only_base: bool = Field(default=False) + ignore_modules: List[str] = Field(default=[]) + + +class InterceptPM(ExtraBaseModel): + auto_load: AutoLoadPM = Field(default_factory=AutoLoadPM) + include_modules: List[str] = Field(default=[]) + mute_modules: List[str] = Field(default=[]) + + +class ExtraPM(ExtraBaseModel): + pass + + +class LoggerConfigPM(ExtraBaseModel): + app_name: constr(strip_whitespace=True) = Field( + default_factory=get_app_name, + min_length=1, + max_length=127, + ) + level: LogLevelEnum = Field(default=LogLevelEnum.INFO) + use_backtrace: bool = Field(default=True) + use_diagnose: bool = Field(default=False) + stream: StreamPM = Field(default_factory=StreamPM) + file: FilePM = Field(default_factory=FilePM) + intercept: InterceptPM = Field(default_factory=InterceptPM) + extra: ExtraPM = Field(default_factory=ExtraPM) diff --git a/examples/advanced/app.py b/examples/advanced/main.py similarity index 100% rename from examples/advanced/app.py rename to examples/advanced/main.py diff --git a/examples/advanced/requirements.txt b/examples/advanced/requirements.txt index 34a1bf0..7a46392 100644 --- a/examples/advanced/requirements.txt +++ b/examples/advanced/requirements.txt @@ -1,5 +1,3 @@ -exceptiongroup>=1.1.3,<2.0.0 -packaging>=23.1 -certifi>=2023.7.22 -fastapi[all]>=0.101.1,<1.0.0 +fastapi>=0.99.1,<1.0.0 +uvicorn[standard]>=0.23.0,<1.0.0 gunicorn>=21.2.0,<22.0.0 diff --git a/requirements.txt b/requirements.txt index 0d2a4cf..ea1ff8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ PyYAML>=6.0.1,<7.0 -pydantic>=2.1.1,<3.0.0 +pydantic>=1.10.0,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0 loguru>=0.7.2,<1.0.0 diff --git a/setup.py b/setup.py index f15685f..03f00af 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ python_requires=">=3.8", install_requires=[ "PyYAML>=6.0,<7.0", - "pydantic>=2.1.1,<3.0.0", + "pydantic>=1.10.0,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "loguru>=0.7.2,<1.0.0", ], classifiers=[