diff --git a/.env.example b/.env.example index 1c8d370..1940db6 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,4 @@ ENV=development DEBUG=true BEANS_LOGGING_DISABLE_DEFAULT=false +BEANS_LOGGING_CONFIG_PATH="./configs/logging.yml" diff --git a/.github/workflows/2.build-publish.yml b/.github/workflows/2.build-publish.yml index 7bd6844..b7a1c19 100644 --- a/.github/workflows/2.build-publish.yml +++ b/.github/workflows/2.build-publish.yml @@ -28,10 +28,9 @@ jobs: - name: Install dependencies run: | python -m pip install -U pip - python -m pip install -r ./requirements.txt - python -m pip install pytest==7.3.1 pytest-cov==4.1.0 + python -m pip install -r ./requirements.test.txt - name: Test with pytest - run: python -m pytest -sv + run: ./scripts/test.sh build_publish: needs: test @@ -49,16 +48,16 @@ jobs: - name: Install dependencies run: | python -m pip install -U pip - python -m pip install build==0.10.0 twine==4.0.2 + python -m pip install -r ./requirements.build.txt - name: Build and publish package + # run: | + # echo -e "[testpypi]\nusername = __token__\npassword = ${{ secrets.TEST_PYPI_API_TOKEN }}" > ~/.pypirc + # ./scripts/build.sh -c -t -u + # rm -rfv ~/.pypirc run: | echo -e "[pypi]\nusername = __token__\npassword = ${{ secrets.PYPI_API_TOKEN }}" > ~/.pypirc ./scripts/build.sh -c -t -u -p rm -rfv ~/.pypirc - # run: | - # echo -e "[testpypi]\nusername = __token__\npassword = ${{ secrets.TEST_PYPI_API_TOKEN }}" > ~/.pypirc - # ./scripts/build.sh -c -t -u - # rm -rfv ~/.pypirc - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 9eb2dca..5e8a41b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # beans_logging +[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bybatkhuu/mod.python-logging/2.build-publish.yml?logo=GitHub)](https://github.com/bybatkhuu/mod.python-logging/actions/workflows/2.build-publish.yml) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/bybatkhuu/mod.python-logging?logo=GitHub)](https://github.com/bybatkhuu/mod.python-logging/releases) [![PyPI](https://img.shields.io/pypi/v/beans-logging?logo=PyPi)](https://pypi.org/project/beans-logging) @@ -173,6 +174,7 @@ Run the [**`examples/simple`**](https://github.com/bybatkhuu/mod.python-logging/ ```sh cd ./examples/simple + python ./main.py ``` @@ -342,8 +344,8 @@ Output: To run tests, run the following command: ```sh -# Install python development dependencies: -pip install -r ./requirements.dev.txt +# Install python test dependencies: +pip install -r ./requirements.test.txt # Run tests: python -m pytest -sv @@ -400,6 +402,11 @@ logger: extra: ``` +## Documentation + +- [docs](https://github.com/bybatkhuu/mod.python-logging/blob/main/docs/README.md) +- [scripts](https://github.com/bybatkhuu/mod.python-logging/blob/main/docs/scripts/README.md) + --- ## References diff --git a/beans_logging/_base.py b/beans_logging/_base.py index ad6b559..9f50601 100644 --- a/beans_logging/_base.py +++ b/beans_logging/_base.py @@ -2,6 +2,7 @@ ## Standard libraries import os +import copy import json import logging from typing import Union @@ -13,6 +14,7 @@ from loguru import logger ## Internal modules +from ._utils import create_dir, deep_merge from ._handler import InterceptHandler from .rotation import RotationChecker from .schema import LoggerConfigPM @@ -26,25 +28,23 @@ use_file_json_filter, use_file_json_err_filter, ) -from ._utils import create_dir class LoggerLoader: """LoggerLoader class for setting up loguru logger. Attributes: - _CONFIGS_DIR (str ): Default configs directory. Defaults to '${PWD}/configs'. - _CONFIG_FILENAME (str ): Default logger config filename. Defaults to 'logger.yml'. + _CONFIG_FILE_PATH (str ): Default logger config file path. Defaults to '${PWD}/configs/logger.yml'. - handlers_map (dict ): Registered logger handlers map as dictionary. - configs_dir (str ): Logger configs directory. - config_filename (str ): Logger config filename. - config (LoggerConfigPM): Logger config as . + handlers_map (dict ): Registered logger handlers map as dictionary. Defaults to None. + config (LoggerConfigPM): Logger config as . Defaults to None. + config_file_path (str ): Logger config file path. Defaults to `LoggerLoader._CONFIG_FILE_PATH`. Methods: load() : Load logger handlers based on logger config. remove_handler() : Remove all handlers or specific handler by name or id from logger. update_config() : Update logger config with new config. + _load_env_vars() : Load 'BEANS_LOGGING_CONFIG_PATH' environment variable for logger config file path. _load_config_file() : Load logger config from file. _check_env() : Check environment variables for logger config. _add_stream_std_handler() : Add std stream handler to logger. @@ -56,31 +56,32 @@ class LoggerLoader: _load_intercept_handlers() : Load intercept handlers to catch third-pary modules log or mute them. """ - _CONFIGS_DIR = os.path.join(os.getcwd(), "configs") - _CONFIG_FILENAME = "logger.yml" + _CONFIG_FILE_PATH = os.path.join(os.getcwd(), "configs", "logger.yml") + @validate_call def __init__( self, - configs_dir: str = _CONFIGS_DIR, - config_filename: str = _CONFIG_FILENAME, + config: Union[LoggerConfigPM, dict, None] = None, + config_file_path: str = _CONFIG_FILE_PATH, load_config_file: bool = True, - config: Union[dict, LoggerConfigPM, None] = None, auto_load: bool = False, ): """LoggerLoader constructor method. Args: - configs_dir (str, optional): Logger configs directory. Defaults to LoggerLoader._CONFIGS_DIR. - config_filename (str, optional): Logger config filename. Defaults to LoggerLoader._CONFIG_FILENAME. - load_config_file (bool, optional): Indicates whether to load logger config file or not. Defaults to True. - config (Union[dict, LoggerConfigPM, None], optional): New logger config to update loaded config. Defaults to None. - auto_load (bool, optional): Indicates whether to load logger handlers or not. Defaults to False. + config (Union[LoggerConfigPM, + dict, + None ], optional): New logger config to update loaded config. Defaults to None. + config_file_path (str , optional): Logger config file path. Defaults to `LoggerLoader._CONFIG_FILE_PATH`. + load_config_file (bool , optional): Indicates whether to load logger config file or not. Defaults to True. + auto_load (bool , optional): Indicates whether to load logger handlers or not. Defaults to False. """ - self.handlers_map: dict = {"default": 0} - self.configs_dir = configs_dir - self.config_filename = config_filename + self.handlers_map = {"default": 0} self.config = LoggerConfigPM() + self.config_file_path = config_file_path + + self._load_env_vars() if load_config_file: self._load_config_file() @@ -155,11 +156,11 @@ def remove_handler( self.handlers_map.clear() @validate_call - def update_config(self, config: Union[dict, LoggerConfigPM]): + def update_config(self, config: Union[LoggerConfigPM, dict]): """Update logger config with new config. Args: - config (Union[dict, LoggerConfigPM], required): New logger config to update loaded config. + config (Union[LoggerConfigPM, dict], required): New logger config to update loaded config. Raises: Exception: Failed to load `config` argument into . @@ -168,8 +169,8 @@ def update_config(self, config: Union[dict, LoggerConfigPM]): try: if isinstance(config, dict): _config_dict = self.config.model_dump() - _config_dict.update(config) - self.config = LoggerConfigPM(**_config_dict) + _merged_dict = deep_merge(_config_dict, config) + self.config = LoggerConfigPM(**_merged_dict) elif isinstance(config, LoggerConfigPM): self.config = config except Exception: @@ -178,59 +179,92 @@ def update_config(self, config: Union[dict, LoggerConfigPM]): ) raise + def _load_env_vars(self): + """Load 'BEANS_LOGGING_CONFIG_PATH' environment variable for logger config file path.""" + + _env_config_file_path = os.getenv("BEANS_LOGGING_CONFIG_PATH") + if _env_config_file_path: + try: + self.config_file_path = _env_config_file_path + except Exception: + logger.warning( + "Failed to load 'BEANS_LOGGING_CONFIG_PATH' environment variable!" + ) + def _load_config_file(self): """Load logger config from file.""" - _config_file_format = "YAML" - if self.config_filename.lower().endswith((".yml", ".yaml")): - _config_file_format = "YAML" - if self.config_filename.lower().endswith(".json"): - _config_file_format = "JSON" - # elif self.config_filename.lower().endswith(".toml"): - # _config_file_format = "TOML" - - _config_path = os.path.abspath( - os.path.join(self.configs_dir, self.config_filename) - ) + _file_format = "" + if self.config_file_path.lower().endswith((".yml", ".yaml")): + _file_format = "YAML" + if self.config_file_path.lower().endswith(".json"): + _file_format = "JSON" + # elif self.config_file_path.lower().endswith(".toml"): + # _file_format = "TOML" ## Loading config from file, if it's exits: - if os.path.isfile(_config_path): - if _config_file_format == "YAML": + if os.path.isfile(self.config_file_path): + if _file_format == "YAML": try: - with open(_config_path, "r", encoding="utf-8") as _config_file: - _new_config_dict = yaml.safe_load(_config_file)["logger"] + with open( + self.config_file_path, "r", encoding="utf-8" + ) as _config_file: + _new_config_dict = yaml.safe_load(_config_file) or {} + if "logger" not in _new_config_dict: + logger.warning( + f"'{self.config_file_path}' YAML config file doesn't have 'logger' section!" + ) + return + + _new_config_dict = _new_config_dict["logger"] _config_dict = self.config.model_dump() - _config_dict.update(_new_config_dict) - self.config = LoggerConfigPM(**_config_dict) + _merged_dict = deep_merge(_config_dict, _new_config_dict) + self.config = LoggerConfigPM(**_merged_dict) except Exception: logger.critical( - f"Failed to load '{_config_path}' yaml config file." + f"Failed to load '{self.config_file_path}' yaml config file." ) raise - elif _config_file_format == "JSON": + elif _file_format == "JSON": try: - with open(_config_path, "r", encoding="utf-8") as _config_file: - _new_config_dict = json.load(_config_file)["logger"] + with open( + self.config_file_path, "r", encoding="utf-8" + ) as _config_file: + _new_config_dict = json.load(_config_file) or {} + if "logger" not in _new_config_dict: + logger.warning( + f"'{self.config_file_path}' JSON config file doesn't have 'logger' section!" + ) + return + + _new_config_dict = _new_config_dict["logger"] _config_dict = self.config.model_dump() - _config_dict.update(_new_config_dict) - self.config = LoggerConfigPM(**_config_dict) + _merged_dict = deep_merge(_config_dict, _new_config_dict) + self.config = LoggerConfigPM(**_merged_dict) except Exception: logger.critical( - f"Failed to load '{_config_path}' json config file." + f"Failed to load '{self.config_file_path}' json config file." ) raise - # elif _config_file_format == "TOML": + # elif _file_format == "TOML": # try: # import toml - # with open(_config_path, "r", encoding="utf-8") as _config_file: - # _new_config_dict = toml.load(_config_file)["logger"] + # 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( + # f"'{self.config_file_path}' TOML config file doesn't have 'logger' section!" + # ) + # return + + # _new_config_dict = _new_config_dict["logger"] # _config_dict = self.config.model_dump() - # _config_dict.update(_new_config_dict) - # self.config = LoggerConfigPM(**_config_dict) + # _merged_dict = deep_merge(_config_dict, _new_config_dict) + # self.config = LoggerConfigPM(**_merged_dict) # except Exception: # logger.critical( - # f"Failed to load '{_config_path}' toml config file." + # f"Failed to load '{self.config_file_path}' toml config file." # ) # raise @@ -422,7 +456,7 @@ def add_custom_handler(self, handler_name: str, **kwargs) -> int: if "sink" not in kwargs: raise ValueError( - f"The `sink` argument is required for custom handler '{handler_name}'!" + f"`sink` argument is required for custom handler '{handler_name}'!" ) _handler_id = logger.add(**kwargs) @@ -486,93 +520,79 @@ def handlers_map(self) -> Union[dict, None]: try: return self.__handlers_map except AttributeError: - return None + self.__handlers_map = {"default": 0} + + return self.__handlers_map @handlers_map.setter def handlers_map(self, handlers_map: dict): if not isinstance(handlers_map, dict): raise TypeError( - f"The `handlers_map` attribute type {type(handlers_map)} is invalid, must be !." + f"`handlers_map` attribute type {type(handlers_map)} is invalid, must be !." ) - self.__handlers_map = handlers_map + self.__handlers_map = copy.deepcopy(handlers_map) ## handlers_map ## - ## configs_dir ## + ## config ## @property - def configs_dir(self) -> str: + def config(self) -> LoggerConfigPM: try: - return self.__configs_dir + return self.__config except AttributeError: - return LoggerLoader._CONFIGS_DIR + self.__config = LoggerConfigPM() + + return self.__config - @configs_dir.setter - def configs_dir(self, configs_dir: str): - if not isinstance(configs_dir, str): + @config.setter + def config(self, config: LoggerConfigPM): + if not isinstance(config, LoggerConfigPM): raise TypeError( - f"The `configs_dir` attribute type {type(configs_dir)} is invalid, must be a !" + f"`config` attribute type {type(config)} is invalid, must be a !" ) - configs_dir = configs_dir.strip() - if configs_dir == "": - raise ValueError("The `configs_dir` attribute value is empty!") - - self.__configs_dir = configs_dir + self.__config = copy.deepcopy(config) - ## configs_dir ## + ## config ## - ## config_filename ## + ## config_file_path ## @property - def config_filename(self) -> str: + def config_file_path(self) -> str: try: - return self.__config_filename + return self.__config_file_path except AttributeError: - return LoggerLoader._CONFIG_FILENAME + self.__config_file_path = LoggerLoader._CONFIG_FILE_PATH + + return self.__config_file_path - @config_filename.setter - def config_filename(self, config_filename: str): - if not isinstance(config_filename, str): + @config_file_path.setter + def config_file_path(self, config_file_path: str): + if not isinstance(config_file_path, str): raise TypeError( - f"The `config_filename` attribute type {type(config_filename)} is invalid, must be a !" + f"`config_file_path` attribute type {type(config_file_path)} is invalid, must be a !" ) - config_filename = config_filename.strip() - if config_filename == "": - raise ValueError("The `config_filename` attribute value is empty!") + config_file_path = config_file_path.strip() + if config_file_path == "": + raise ValueError("`config_file_path` attribute value is empty!") - if (not config_filename.lower().endswith((".yml", ".yaml"))) and ( - not config_filename.lower().endswith(".json") + if (not config_file_path.lower().endswith((".yml", ".yaml"))) and ( + not config_file_path.lower().endswith(".json") ): - if not config_filename.lower().endswith(".toml"): + if not config_file_path.lower().endswith(".toml"): raise NotImplementedError( - f"The `config_filename` attribute value '{config_filename}' is invalid, TOML file format is not supported yet!" + f"`config_file_path` attribute value '{config_file_path}' is invalid, TOML file format is not supported yet!" ) raise ValueError( - f"The `config_filename` attribute value '{config_filename}' is invalid, must be a file with '.yml', '.yaml' or '.json' extension!" + f"`config_file_path` attribute value '{config_file_path}' is invalid, file must be '.yml', '.yaml' or '.json' format!" ) - self.__config_filename = config_filename - - ## config_filename ## - - ## config ## - @property - def config(self) -> Union[LoggerConfigPM, None]: - try: - return self.__config - except AttributeError: - return None - - @config.setter - def config(self, config: LoggerConfigPM): - if not isinstance(config, LoggerConfigPM): - raise TypeError( - f"The `config` attribute type {type(config)} is invalid, must be a !" - ) + if not os.path.isabs(config_file_path): + config_file_path = os.path.join(os.getcwd(), config_file_path) - self.__config = config + self.__config_file_path = config_file_path - ## config ## + ## config_file_path ## ### ATTRIBUTES ### diff --git a/beans_logging/_utils.py b/beans_logging/_utils.py index 07a5db9..75c1cee 100644 --- a/beans_logging/_utils.py +++ b/beans_logging/_utils.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import copy import errno # import logging @@ -39,3 +40,30 @@ def create_dir(create_dir: str, quiet: bool = True): logger.debug(f"Successfully created '{create_dir}' directory.") else: logger.success(f"Successfully created '{create_dir}' directory.") + + +@validate_call +def deep_merge(dict1: dict, dict2: dict) -> dict: + """Return a new dictionary that's the result of a deep merge of two dictionaries. + If there are conflicts, values from `dict2` will overwrite those in `dict1`. + + Args: + dict1 (dict, required): The base dictionary that will be merged. + dict2 (dict, required): The dictionary to merge into `dict1`. + + Returns: + dict: The merged dictionary. + """ + + _merged = copy.deepcopy(dict1) + for _key, _val in dict2.items(): + if ( + _key in _merged + and isinstance(_merged[_key], dict) + and isinstance(_val, dict) + ): + _merged[_key] = deep_merge(_merged[_key], _val) + else: + _merged[_key] = copy.deepcopy(_val) + + return _merged diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fb6c523 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +# Documentation + +## Pages + +- [README](../README.md) +- [scripts](./scripts/README.md) diff --git a/docs/scripts/1.base.md b/docs/scripts/1.base.md new file mode 100644 index 0000000..8612031 --- /dev/null +++ b/docs/scripts/1.base.md @@ -0,0 +1,12 @@ +# base.sh + +This is a base script used to define common utility functions for other scripts in the project. + +It contains the following key features: + +- **Date format definition**: Defines a date format that is used throughout the scripts. +- **Colour codes**: Sets up color codes for console logging if the terminal is recognized as an xterm. +- **Logging functions**: Includes `echoError`, `echoWarn`, `echoInfo`, and `echoOk` which log messages to the console with a timestamp, color-coded based on their severity (error, warning, information, success respectively). +- **Existence checks**: Implements the `exitIfNotExists` and `exitIfNoGit` functions which check for the existence of a file and the Git software, respectively. They log an error and terminate the script with an appropriate exit code if the check fails. + +**Source code**: [**`base.sh`**](../../scripts/base.sh) diff --git a/docs/scripts/2.clean.md b/docs/scripts/2.clean.md new file mode 100644 index 0000000..dca388b --- /dev/null +++ b/docs/scripts/2.clean.md @@ -0,0 +1,23 @@ +# clean.sh + +This script is designed to clean up the build environment by removing artifacts and other temporary or unwanted files and directories. + +The script performs the following operations: + +- **Loading base script**: Includes the `base.sh` script to gain access to its utility functions and environment variables. +- **Delete system files**: Finds and deletes all `.DS_Store` and `.Thumbs.db` files in the project directory and its subdirectories. +- **Delete cache directories**: Finds and deletes all `__pycache__` directories in the project directory and its subdirectories. +- **Delete project-related directories**: Removes directories created during the test and build process or by tools used in the project, such as `.benchmarks`, `.pytest_cache`, `build`, and `dist` directories. +- **Delete `.coverage` file**: Removes the `.coverage` file that's created when coverage information is collected for the project. + +**Usage**: + +To execute the clean script, simply run the following command in the terminal: + +```sh +./clean.sh +``` + +This will clean up the project directory, removing any unnecessary files and directories and ensuring a clean environment for a fresh build. + +**Source code**: [**`clean.sh`**](../../scripts/clean.sh) diff --git a/docs/scripts/3.get-version.md b/docs/scripts/3.get-version.md new file mode 100644 index 0000000..cd16c5b --- /dev/null +++ b/docs/scripts/3.get-version.md @@ -0,0 +1,22 @@ +# get-version.sh + +This script is used to retrieve the current version of the application from a specified version file. + +The script performs the following operations: + +- `VERSION_FILE` is either loaded from the environment or, if it's not present in the environment, it defaults to `beans_logging/__version__.py`. +- It first checks if the `VERSION_FILE` variable is not empty and if the file exists. If these conditions are met, it retrieves the value of `__version__` from the file by using `grep`, `awk`, and `tr` commands. The `grep` command filters the line containing `__version__ =` , the `awk` command splits the line into two parts at `=`, and the `tr` command removes the quotes around the version. If these operations fail, it exits the script with status code `2`. +- If the `VERSION_FILE` variable is empty or the file does not exist, it sets the current version to `0.0.0`. +- Finally, it echoes the current version to the console. + +**Usage**: + +To execute the get version script, simply run the following command in the terminal: + +```sh +./get-version.sh +``` + +This script can be used to conveniently fetch the version. It is used by the `bump-version.sh` script to retrieve the current version before incrementing it. + +**Source code**: [**`get-version.sh`**](../../scripts/get-version.sh) diff --git a/docs/scripts/4.test.md b/docs/scripts/4.test.md new file mode 100644 index 0000000..7e88deb --- /dev/null +++ b/docs/scripts/4.test.md @@ -0,0 +1,24 @@ +# test.sh + +This script is used to run the pytest tests for the project. + +The script performs the following operations: + +- **Loading base script**: Includes the `base.sh` script to gain access to its utility functions and environment variables. +- **Running pytest**: Runs the pytest tests for the project. + +**Usage**: + +To execute the test script, simply run the following command in the terminal: + +```sh +./test.sh +``` + +**Source code**: [**`test.sh`**](../../scripts/test.sh) + +--- + +## References + +- diff --git a/docs/scripts/5.bump-version.md b/docs/scripts/5.bump-version.md new file mode 100644 index 0000000..ec3e101 --- /dev/null +++ b/docs/scripts/5.bump-version.md @@ -0,0 +1,40 @@ +# bump-version.sh + +This script is used to manage the versioning of the project. It allows you to increment the major, minor, or patch part of the version, as per [Semantic Versioning](https://semver.org) rules. + +The script carries out the following operations: + +- **Loading base script**: It sources the `base.sh` script, leveraging the utility functions defined there. +- **Loading environment variables**: If a `.env` file is present in the root directory, the script loads the environment variables from this file. +- **Sets variables**: Sets the `VERSION_FILE` and other variables. The `VERSION_FILE` variable is either loaded from the environment or defaults to `beans_logging/__version__.py`. +- **Parses input arguments**: It parses the `-b` or `--bump-type` argument for the type of version bump (**`major`**, **`minor`**, or **`patch`**) and `-p` or `--push-tag` to decide whether to push the tag to the Git repository or not. +- **Checks and increments the version**: It uses `get-version.sh` to extract the current version from the file specified by `VERSION_FILE`. Based on the bump type, it increments the appropriate part of the version and writes the new version back to the file. +- **Commits and tags**: If the `-p` or `--push-tag` flag was provided, it adds and commits the changes, pushes the changes, creates a new tag with the new version, and pushes the tag to the Git repository. It will prevent the operation if the tag already exists. + +**Usage**: + +To execute the bump version script, run the following command in the terminal: + +```sh +./bump-version.sh -b= -p +``` + +Replace **``** with either **`major`**, **`minor`**, or **`patch`** to indicate which part of the version to increment. The `-p` or `--push-tag` flag tells the script to commit the changes and push the tag to the Git repository. + +**Examples**: + +To bump the **`minor`** version and push the **new tag**, run: + +```sh +./bump-version.sh -b=minor -p +``` + +This script streamlines the versioning process, reducing the chances of errors and ensuring consistency in versioning. + +**Source code**: [**`bump-version.sh`**](../../scripts/bump-version.sh) + +--- + +## References + +- diff --git a/docs/scripts/6.build.md b/docs/scripts/6.build.md new file mode 100644 index 0000000..bc4a871 --- /dev/null +++ b/docs/scripts/6.build.md @@ -0,0 +1,32 @@ +# build.sh + +This script is used to build a Python project and optionally run tests and publish the package. It also includes a cleaning operation to clear the build directories. + +This script has the following key features: + +- **Loading base script**: Includes the `base.sh` script to gain access to its utility functions. +- **Checking for required tools**: It verifies if Python and the build package are installed on the system. If tests are not disabled, it also checks if pytest is installed. If uploading is enabled, it checks for the presence of Twine. +- **Command-line argument parsing**: It parses `-c` or `--disable-clean` to disable cleaning the build directories, `-t` or `--disable-test` to disable running tests, `-u` or `--upload` to enable publishing the package, and `-p` or `--production` to switch the package repository from staging (default) to production. +- **Clean operation**: Cleans the build directories before and after building (if enabled). If `-c` or `--disable-clean` is passed, the script will not clean the build directories. +- **Testing operation**: Runs pytest tests if not disabled by `-t` or `--disable-test` flag. +- **Build operation**: Builds a Python package using the Python build package. +- **Publishing operation**: Publishes the built package to a PyPi repository using Twine if the `-u` or `--upload` flag is passed. Defaults to the TestPyPi (staging) repository, but can be switched to the production (PyPi) repository using the `-p` or `--production` flag. + +**Usage**: + +To execute the build script with the different flags, use the following commands: + +```sh +./build.sh [-c|--disable-clean] [-t|--disable-test] [-u|--upload] [-p|--production] +``` + +**Examples**: + +- To build without cleaning: `./build.sh -c` +- To build without running tests: `./build.sh -t` +- To build and publish to the staging repository: `./build.sh -u` +- To build and publish to the production repository: `./build.sh -u -p` + +This script is particularly beneficial for developers, streamlining the build, testing, and publishing process. It provides a one-stop solution for all the build needs of a Python project, reducing chances of errors and ensuring consistency. + +**Source code**: [**`build.sh`**](../../scripts/build.sh) diff --git a/docs/scripts/README.md b/docs/scripts/README.md new file mode 100644 index 0000000..234eba4 --- /dev/null +++ b/docs/scripts/README.md @@ -0,0 +1,24 @@ +# Scripts + +This document provides an overview and usage instructions for the following scripts in this project: + +- [**`base.sh`**](./1.base.md) +- [**`clean.sh`**](./2.clean.md) +- [**`get-version.sh`**](./3.get-version.md) +- [**`test.sh`**](./4.test.md) +- [**`bump-version.sh`**](./5.bump-version.md) +- [**`build.sh`**](./6.build.md) + +All the scripts are located in the [**`scripts`**](../../scripts) directory: + +```txt +scripts/ +├── base.sh +├── build.sh +├── bump-version.sh +├── clean.sh +├── get-version.sh +└── test.sh +``` + +These scripts are designed to be used in a Linux or macOS environment. They may work in a Windows environment with the appropriate tools installed, but this is not guaranteed. diff --git a/examples/advanced/app.py b/examples/advanced/app.py index 164dea2..3e65811 100755 --- a/examples/advanced/app.py +++ b/examples/advanced/app.py @@ -31,7 +31,6 @@ async def lifespan(app: FastAPI): app.add_middleware( HttpAccessLogMiddleware, has_proxy_headers=True, - has_cf_headers=True, msg_format=logger_loader.config.extra.http_std_msg_format, debug_format=logger_loader.config.extra.http_std_debug_format, ) diff --git a/examples/advanced/logger.py b/examples/advanced/logger.py index 76439f2..a12f75c 100644 --- a/examples/advanced/logger.py +++ b/examples/advanced/logger.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from beans_logging import LoggerLoader, Logger +from beans_logging import Logger, LoggerLoader from beans_logging.fastapi import add_file_http_handler, add_file_json_http_handler diff --git a/requirements.build.txt b/requirements.build.txt new file mode 100644 index 0000000..07addaa --- /dev/null +++ b/requirements.build.txt @@ -0,0 +1,4 @@ +setuptools>=67.8.0,<70.0.0 +wheel>=0.40.0,<1.0.0 +build>=0.10.0,<1.0.0 +twine>=4.0.2,<5.0.0 diff --git a/requirements.dev.txt b/requirements.dev.txt index d6986e8..a73a243 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,8 +1,2 @@ -pytest>=7.3.1,<8.0.0 -pytest-cov>=4.1.0,<5.0.0 -pytest-xdist>=3.3.1,<4.0.0 -pytest-benchmark>=4.0.0,<5.0.0 -setuptools>=67.8.0,<70.0.0 -wheel>=0.40.0,<1.0.0 -build>=0.10.0,<1.0.0 -twine>=4.0.2,<5.0.0 +-r requirements.test.txt +-r requirements.build.txt diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..e1646f8 --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,5 @@ +-r ./requirements.txt +pytest>=7.3.1,<8.0.0 +pytest-cov>=4.1.0,<5.0.0 +pytest-xdist>=3.3.1,<4.0.0 +pytest-benchmark>=4.0.0,<5.0.0 diff --git a/scripts/build.sh b/scripts/build.sh index 610e0c3..6bf7094 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -81,10 +81,7 @@ main() fi if [ "${_IS_TEST}" == true ]; then - echoInfo "Running test..." - # python -m pytest -sv -o log_cli=true || exit 2 - python -m pytest -sv || exit 2 - echoOk "Done." + ./scripts/test.sh || exit 2 fi echoInfo "Building package..." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..b379dd3 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +## --- Base --- ## +# Getting path of this script file: +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +_PROJECT_DIR="$(cd "${_SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd)" +cd "${_PROJECT_DIR}" || exit 2 + +# Loading base script: +# shellcheck disable=SC1091 +source ./scripts/base.sh + + +if [ -z "$(which python)" ]; then + echoError "Python not found or not installed." + exit 1 +fi + +if [ -z "$(which pytest)" ]; then + echoError "Pytest not found or not installed." + exit 1 +fi +## --- Base --- ## + + +echoInfo "Running test..." +python -m pytest -sv || exit 2 +# python -m pytest -sv -o log_cli=true || exit 2 +# python -m pytest -sv --cov -o log_cli=true || exit 2 +echoOk "Done." diff --git a/setup.py b/setup.py index b559789..674387c 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from setuptools import setup +from setuptools import setup, find_packages from distutils.util import convert_path @@ -14,7 +14,7 @@ setup( name=_package_name, - packages=[_package_name], + packages=find_packages(), version=f"{_package_version}", license="MIT", description="beans_logging is a python package for simple logger and easily managing logging modules. It is a Loguru based custom logging package for python projects.", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..48f7fe7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +import pytest + +from beans_logging import logger + + +@pytest.fixture(scope="session", autouse=True) +def setup_and_teardown(): + # Equivalent of setUp + logger.info("Setting up...") + + yield # This is where the testing happens! + + # Equivalent of tearDown + logger.info("Tearing down!") diff --git a/tests/test_beans_logging.py b/tests/test_beans_logging.py index 99ebb77..3b94faa 100644 --- a/tests/test_beans_logging.py +++ b/tests/test_beans_logging.py @@ -2,7 +2,7 @@ import pytest -from beans_logging import LoggerConfigPM, Logger, logger, LoggerLoader +from beans_logging import Logger, LoggerConfigPM, LoggerLoader @pytest.fixture @@ -14,19 +14,27 @@ def logger_loader(): del _logger_loader -def test_init(logger_loader): +@pytest.fixture +def logger(): + from beans_logging import logger + + yield logger + + del logger + + +def test_init(logger, logger_loader): logger.info("Testing initialization of 'LoggerLoader'...") assert isinstance(logger_loader, LoggerLoader) assert logger_loader.handlers_map == {"default": 0} - assert logger_loader.configs_dir == LoggerLoader._CONFIGS_DIR - assert logger_loader.config_filename == LoggerLoader._CONFIG_FILENAME + assert logger_loader.config_file_path == LoggerLoader._CONFIG_FILE_PATH assert isinstance(logger_loader.config, LoggerConfigPM) - logger.success("Done: Initialization of 'LoggerLoader'.") + logger.success("Done: Initialization of 'LoggerLoader'.\n") -def test_load(logger_loader): +def test_load(logger, logger_loader): logger.info("Testing 'load' method of 'LoggerLoader'...") logger_loader.update_config(config={"level": "TRACE"}) @@ -42,10 +50,10 @@ def test_load(logger_loader): _logger.error("Error occured.") _logger.critical("CRITICAL ERROR.") - logger.success("Done: 'load' method.") + logger.success("Done: 'load' method.\n") -def test_methods(): +def test_methods(logger): logger.info("Testing 'logger' methods...") logger.trace("Tracing...") @@ -56,4 +64,4 @@ def test_methods(): logger.error("Error occured.") logger.critical("CRITICAL ERROR.") - logger.success("Done: 'logger' methods.") + logger.success("Done: 'logger' methods.\n")