From a03bf2973ab2c172b0dd994eac33e9db56990a5f Mon Sep 17 00:00:00 2001 From: Anca Lita <27920906+ancalita@users.noreply.github.com> Date: Mon, 7 Nov 2022 16:02:08 +0000 Subject: [PATCH] Implement improvement for customizing logging config (#877) * implement changes * pin sanic-testing to fix failing integration tests, add docstring * re-add mypy ignore * add testing * fix default logging not logging to file * add changelog --- changelog/877.improvement.md | 1 + data/test_invalid_yaml.yml | 2 + .../test_invalid_format_value_in_config.yml | 20 ++ .../test_invalid_handler_key_in_config.yml | 21 ++ ...test_invalid_value_for_level_in_config.yml | 20 ++ ...st_missing_required_key_invalid_config.yml | 20 ++ .../test_non_existent_handler_id.yml | 20 ++ .../test_valid_logging_config.yml | 19 ++ poetry.lock | 225 +++++++++--------- pyproject.toml | 3 +- rasa_sdk/__main__.py | 5 +- rasa_sdk/constants.py | 5 + rasa_sdk/exceptions.py | 60 +++++ rasa_sdk/utils.py | 152 +++++++++++- tests/test_utils.py | 216 ++++++++++++++++- 15 files changed, 655 insertions(+), 134 deletions(-) create mode 100644 changelog/877.improvement.md create mode 100644 data/test_invalid_yaml.yml create mode 100644 data/test_logging_config_files/test_invalid_format_value_in_config.yml create mode 100644 data/test_logging_config_files/test_invalid_handler_key_in_config.yml create mode 100644 data/test_logging_config_files/test_invalid_value_for_level_in_config.yml create mode 100644 data/test_logging_config_files/test_missing_required_key_invalid_config.yml create mode 100644 data/test_logging_config_files/test_non_existent_handler_id.yml create mode 100644 data/test_logging_config_files/test_valid_logging_config.yml create mode 100644 rasa_sdk/exceptions.py diff --git a/changelog/877.improvement.md b/changelog/877.improvement.md new file mode 100644 index 000000000..ce19b00ff --- /dev/null +++ b/changelog/877.improvement.md @@ -0,0 +1 @@ +Added CLI option `--logging-config-file` to enable configuration of custom logs formatting. diff --git a/data/test_invalid_yaml.yml b/data/test_invalid_yaml.yml new file mode 100644 index 000000000..c8fe42e20 --- /dev/null +++ b/data/test_invalid_yaml.yml @@ -0,0 +1,2 @@ +user: user + password: pass diff --git a/data/test_logging_config_files/test_invalid_format_value_in_config.yml b/data/test_logging_config_files/test_invalid_format_value_in_config.yml new file mode 100644 index 000000000..f4a3ba56f --- /dev/null +++ b/data/test_logging_config_files/test_invalid_format_value_in_config.yml @@ -0,0 +1,20 @@ +version: 1 +disable_existing_loggers: false +formatters: + normalFormatter: + # invalid value + format: None +handlers: + test_handler: + level: INFO + formatter: normalFormatter + class: logging.FileHandler + filename: "logging_test.log" +loggers: + root: + handlers: [test_handler] + level: INFO + rasa: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/data/test_logging_config_files/test_invalid_handler_key_in_config.yml b/data/test_logging_config_files/test_invalid_handler_key_in_config.yml new file mode 100644 index 000000000..e73c17efb --- /dev/null +++ b/data/test_logging_config_files/test_invalid_handler_key_in_config.yml @@ -0,0 +1,21 @@ +version: 1 +disable_existing_loggers: false +formatters: + normalFormatter: + format: "{\"time\": \"%(asctime)s\", \"name\": \"[%(name)s]\", \"levelname\": \"%(levelname)s\", \"message\": \"%(message)s\"}" +handlers: + test_handler: + level: INFO + formatter: normalFormatter + class: logging.FileHandler + filename: "logging_test.log" + # invalid unknown key + extra_key: "hello world" +loggers: + root: + handlers: [test_handler] + level: INFO + rasa: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/data/test_logging_config_files/test_invalid_value_for_level_in_config.yml b/data/test_logging_config_files/test_invalid_value_for_level_in_config.yml new file mode 100644 index 000000000..59e263c8f --- /dev/null +++ b/data/test_logging_config_files/test_invalid_value_for_level_in_config.yml @@ -0,0 +1,20 @@ +version: 1 +disable_existing_loggers: false +formatters: + normalFormatter: + format: "{\"time\": \"%(asctime)s\", \"name\": \"[%(name)s]\", \"levelname\": \"%(levelname)s\", \"message\": \"%(message)s\"}" +handlers: + test_handler: + # invalid value for level + level: HIGH + formatter: normalFormatter + class: logging.FileHandler + filename: "logging_test.log" +loggers: + root: + handlers: [test_handler] + level: INFO + rasa: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/data/test_logging_config_files/test_missing_required_key_invalid_config.yml b/data/test_logging_config_files/test_missing_required_key_invalid_config.yml new file mode 100644 index 000000000..5b197ad0a --- /dev/null +++ b/data/test_logging_config_files/test_missing_required_key_invalid_config.yml @@ -0,0 +1,20 @@ +# missing mandatory key +# version: 1 +disable_existing_loggers: false +formatters: + normalFormatter: + format: "{\"time\": \"%(asctime)s\", \"name\": \"[%(name)s]\", \"levelname\": \"%(levelname)s\", \"message\": \"%(message)s\"}" +handlers: + test_handler: + level: INFO + formatter: normalFormatter + class: logging.FileHandler + filename: "logging_test.log" +loggers: + root: + handlers: [test_handler] + level: INFO + rasa: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/data/test_logging_config_files/test_non_existent_handler_id.yml b/data/test_logging_config_files/test_non_existent_handler_id.yml new file mode 100644 index 000000000..19f617ede --- /dev/null +++ b/data/test_logging_config_files/test_non_existent_handler_id.yml @@ -0,0 +1,20 @@ +version: 1 +disable_existing_loggers: false +formatters: + normalFormatter: + format: "{\"time\": \"%(asctime)s\", \"name\": \"[%(name)s]\", \"levelname\": \"%(levelname)s\", \"message\": \"%(message)s\"}" +handlers: + test_handler: + level: INFO + formatter: normalFormatter + class: logging.FileHandler + filename: "logging_test.log" +loggers: + root: + # a non-existent handler id + handlers: [some_handler] + level: INFO + rasa: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/data/test_logging_config_files/test_valid_logging_config.yml b/data/test_logging_config_files/test_valid_logging_config.yml new file mode 100644 index 000000000..d06826be5 --- /dev/null +++ b/data/test_logging_config_files/test_valid_logging_config.yml @@ -0,0 +1,19 @@ +version: 1 +disable_existing_loggers: false +formatters: + customFormatter: + format: "{\"time\": \"%(asctime)s\", \"name\": \"[%(name)s]\", \"levelname\": \"%(levelname)s\", \"message\": \"%(message)s\"}" +handlers: + test_handler: + level: INFO + formatter: customFormatter + class: logging.FileHandler + filename: "logging_test.log" +loggers: + root: + handlers: [test_handler] + level: INFO + rasa_sdk: + handlers: [test_handler] + level: INFO + propagate: 0 diff --git a/poetry.lock b/poetry.lock index 4ecc84dda..c34af16d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,7 +8,7 @@ python-versions = ">=3.7,<4.0" [[package]] name = "anyio" -version = "3.6.1" +version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "dev" optional = false @@ -22,7 +22,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +trio = ["trio (>=0.16,<0.22)"] [[package]] name = "attrs" @@ -63,7 +63,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.9.14" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -105,11 +105,11 @@ click = "*" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coloredlogs" @@ -127,7 +127,7 @@ cron = ["capturer (>=2.4)"] [[package]] name = "coverage" -version = "6.4.4" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -163,6 +163,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "exceptiongroup" +version = "1.0.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "flake8" version = "5.0.4" @@ -284,13 +295,14 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [[package]] name = "incremental" -version = "21.3.0" -description = "A small library that versions your Python projects." +version = "22.10.0" +description = "\"A small library that versions your Python projects.\"" category = "dev" optional = false python-versions = "*" [package.extras] +mypy = ["click (>=6.0)", "twisted (>=16.4.0)", "mypy (==0.812)"] scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] [[package]] @@ -354,9 +366,9 @@ typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=3.10" [package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] +python2 = ["typed-ast (>=1.4.0,<2)"] +dmypy = ["psutil (>=4.0)"] [[package]] name = "mypy-extensions" @@ -420,8 +432,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "prompt-toolkit" @@ -434,14 +446,6 @@ python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pycodestyle" version = "2.9.1" @@ -501,7 +505,7 @@ python-versions = "*" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -510,12 +514,12 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -581,6 +585,29 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "ruamel.yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "sanic" version = "21.12.2" @@ -707,7 +734,7 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -757,7 +784,7 @@ python-versions = "*" [[package]] name = "websockets" -version = "10.3" +version = "10.4" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" category = "main" optional = false @@ -765,20 +792,20 @@ python-versions = ">=3.7" [[package]] name = "zipp" -version = "3.8.1" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "98ca2cd60f1846d2500af47cb559fabae99e78e5ae4b101235d8f7e0a3f0a05a" +content-hash = "a160895e83900ea1fd90937809df7a57e528e9b3fc1b79391fc41aaf0dd690bd" [metadata.files] aiofiles = [] @@ -791,26 +818,36 @@ click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -click-default-group = [] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +click-default-group = [ + {file = "click-default-group-1.2.2.tar.gz", hash = "sha256:d9560e8e8dfa44b3562fbc9425042a0fd6d21956fcc2db0077f63f34253ab904"}, ] +colorama = [] coloredlogs = [] coverage = [] -coveralls = [] -docopt = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +coveralls = [ + {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, + {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, ] +docopt = [] +exceptiongroup = [] flake8 = [] -flake8-docstrings = [] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] -httpcore = [] +httpcore = [ + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, +] httptools = [] -httpx = [] +httpx = [ + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, +] humanfriendly = [] idna = [] importlib-metadata = [ @@ -826,48 +863,7 @@ jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] +markupsafe = [] mccabe = [] multidict = [] mypy = [] @@ -875,53 +871,47 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, -] +packaging = [] pathspec = [] pep440-version-utils = [] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"}, - {file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] +platformdirs = [] +pluggy = [] +prompt-toolkit = [] pycodestyle = [] -pydocstyle = [] -pyflakes = [] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pyreadline = [ - {file = "pyreadline-2.1.win-amd64.exe", hash = "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"}, - {file = "pyreadline-2.1.win32.exe", hash = "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e"}, - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] +pyflakes = [] +pyparsing = [] +pyreadline = [] pyreadline3 = [] pytest = [] pytest-cov = [] questionary = [] requests = [] -rfc3986 = [] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +"ruamel.yaml" = [] +"ruamel.yaml.clib" = [] sanic = [] sanic-cors = [] sanic-routing = [] -sanic-testing = [] -semantic-version = [] +sanic-testing = [ + {file = "sanic-testing-22.6.0.tar.gz", hash = "sha256:8f006d2332106539cd6f3da8a5c0d1f31472261f3293e43e2c9bbad605e72c5b"}, + {file = "sanic_testing-22.6.0-py3-none-any.whl", hash = "sha256:d84303e83066de7f18e8c3a0cd04512ba1517dbc31123f14e8aec318b22c008c"}, +] +semantic-version = [ + {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, + {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, +] sniffio = [] -snowballstemmer = [] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -961,9 +951,6 @@ typing-extensions = [] ujson = [] urllib3 = [] uvloop = [] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] +wcwidth = [] websockets = [] zipp = [] diff --git a/pyproject.toml b/pyproject.toml index d4aadab68..31a8c1033 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ Sanic-Cors = "^2.0.0" # interface for testing CLI interactions. this change breaks our unit # tests in rasa-oss. prompt-toolkit = "^3.0,<3.0.29" +"ruamel.yaml" = ">=0.16.5,<0.18.0" [tool.poetry.dev-dependencies] pytest-cov = "^3.0.0" @@ -86,4 +87,4 @@ toml = "^0.10.0" pep440-version-utils = "^0.3.0" semantic_version = "^2.8.5" mypy = "^0.971" -sanic-testing = "^22.3.0" +sanic-testing = "^22.3.0, <22.9.0" diff --git a/rasa_sdk/__main__.py b/rasa_sdk/__main__.py index ce132a90a..f4520dc40 100644 --- a/rasa_sdk/__main__.py +++ b/rasa_sdk/__main__.py @@ -11,7 +11,10 @@ def main_from_args(args): utils.configure_colored_logging(args.loglevel) utils.configure_file_logging( - logging.getLogger(APPLICATION_ROOT_LOGGER_NAME), args.log_file, args.loglevel + logging.getLogger(APPLICATION_ROOT_LOGGER_NAME), + args.log_file, + args.loglevel, + args.logging_config_file, ) utils.update_sanic_log_level() diff --git a/rasa_sdk/constants.py b/rasa_sdk/constants.py index 71f7b71e3..73400cc1e 100644 --- a/rasa_sdk/constants.py +++ b/rasa_sdk/constants.py @@ -4,3 +4,8 @@ DEFAULT_LOG_LEVEL_LIBRARIES = "ERROR" ENV_LOG_LEVEL_LIBRARIES = "LOG_LEVEL_LIBRARIES" APPLICATION_ROOT_LOGGER_NAME = "rasa_sdk" +DEFAULT_ENCODING = "utf-8" +YAML_VERSION = (1, 2) +PYTHON_LOGGING_SCHEMA_DOCS = ( + "https://docs.python.org/3/library/logging.config.html#dictionary-schema-details" +) diff --git a/rasa_sdk/exceptions.py b/rasa_sdk/exceptions.py new file mode 100644 index 000000000..c6a9393bc --- /dev/null +++ b/rasa_sdk/exceptions.py @@ -0,0 +1,60 @@ +from pathlib import Path +from typing import Optional, Text, Union + +from ruamel.yaml.error import ( + MarkedYAMLError, + MarkedYAMLWarning, + MarkedYAMLFutureWarning, +) + + +class FileNotFoundException(FileNotFoundError): + """Raised when a file, expected to exist, doesn't exist.""" + + +class FileIOException(Exception): + """Raised if there is an error while doing file IO.""" + + +class YamlSyntaxException(Exception): + """Raised when a YAML file can not be parsed properly due to a syntax error.""" + + def __init__( + self, + filename: Optional[Union[Text, Path]] = None, + underlying_yaml_exception: Optional[Exception] = None, + ) -> None: + """Represents the exception constructor.""" + self.filename = filename + + self.underlying_yaml_exception = underlying_yaml_exception + + def __str__(self) -> Text: + if self.filename: + exception_text = f"Failed to read '{self.filename}'." + else: + exception_text = "Failed to read YAML." + + if self.underlying_yaml_exception: + if isinstance( + self.underlying_yaml_exception, + (MarkedYAMLError, MarkedYAMLWarning, MarkedYAMLFutureWarning), + ): + self.underlying_yaml_exception.note = None + if isinstance( + self.underlying_yaml_exception, + (MarkedYAMLWarning, MarkedYAMLFutureWarning), + ): + self.underlying_yaml_exception.warn = None + exception_text += f" {self.underlying_yaml_exception}" + + if self.filename: + exception_text = exception_text.replace( + 'in ""', f'in "{self.filename}"' + ) + + exception_text += ( + "\n\nYou can use https://yamlchecker.com/ to validate the " + "YAML syntax of your file." + ) + return exception_text diff --git a/rasa_sdk/utils.py b/rasa_sdk/utils.py index c51fe6af4..7f8815573 100644 --- a/rasa_sdk/utils.py +++ b/rasa_sdk/utils.py @@ -1,17 +1,30 @@ import asyncio import inspect import logging +import logging.config import warnings import os +from pathlib import Path +from ruamel import yaml as yaml +from ruamel.yaml import YAMLError +from ruamel.yaml.constructor import DuplicateKeyError -from typing import AbstractSet, Any, List, Text, Optional, Coroutine, Union +from typing import AbstractSet, Any, Dict, List, Text, Optional, Coroutine, Union import rasa_sdk from rasa_sdk.constants import ( + DEFAULT_ENCODING, DEFAULT_SANIC_WORKERS, ENV_SANIC_WORKERS, DEFAULT_LOG_LEVEL_LIBRARIES, ENV_LOG_LEVEL_LIBRARIES, + PYTHON_LOGGING_SCHEMA_DOCS, + YAML_VERSION, +) +from rasa_sdk.exceptions import ( + FileIOException, + FileNotFoundException, + YamlSyntaxException, ) logger = logging.getLogger(__name__) @@ -77,6 +90,13 @@ def add_logging_file_arguments(parser): default=None, help="Store logs in specified file.", ) + parser.add_argument( + "--logging-config_file", + type=str, + default=None, + help="If set, the name of the logging configuration file will be set " + "to the given name.", + ) def configure_colored_logging(loglevel): @@ -96,31 +116,76 @@ def configure_colored_logging(loglevel): ) -def configure_file_logging( - logger_obj: logging.Logger, log_file: Optional[Text], loglevel: int +def configure_logging_from_input_file(logging_config_file: Union[Path, Text]) -> None: + """Parses YAML file content to configure logging. + + Args: + logging_config_file: YAML file containing logging configuration to handle + custom formatting + """ + logging_config_dict = read_yaml_file(logging_config_file) + + try: + logging.config.dictConfig(logging_config_dict) + except (ValueError, TypeError, AttributeError, ImportError) as e: + logging.debug( + f"The logging config file {logging_config_file} could not " + f"be applied because it failed validation against " + f"the built-in Python logging schema. " + f"More info at {PYTHON_LOGGING_SCHEMA_DOCS}.", + exc_info=e, + ) + + +def set_default_logging( + logger_obj: logging.Logger, output_log_file: Optional[Text], loglevel: int ) -> None: - """Configure logging to a file. + """Configure default logging to a file. :param logger_obj: Logger object to configure. - :param log_file: Path of log file to write to. - :param loglevel: Log Level - :return: + :param output_log_file: Path of log file to write to. + :param loglevel: Log Level. + :return: None. """ - if not log_file: + if not output_log_file: return if not loglevel: loglevel = logging.INFO + logger_obj.setLevel(loglevel) + formatter = logging.Formatter( "%(asctime)s [%(levelname)-5.5s] %(name)s - %(message)s" ) - file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler = logging.FileHandler(output_log_file, encoding=DEFAULT_ENCODING) file_handler.setLevel(loglevel) file_handler.setFormatter(formatter) logger_obj.addHandler(file_handler) +def configure_file_logging( + logger_obj: logging.Logger, + output_log_file: Optional[Text], + loglevel: int, + logging_config_file: Optional[Text], +) -> None: + """Configure logging configuration. + + :param logger_obj: Logger object to configure. + :param output_log_file: Path of log file to write to. + :param loglevel: Log Level. + :param logging_config_file: YAML file containing logging configuration to handle + custom formatting + :return: None. + """ + if logging_config_file is not None: + configure_logging_from_input_file(logging_config_file) + return + + set_default_logging(logger_obj, output_log_file, loglevel) + + def arguments_of(func) -> AbstractSet[Text]: """Return the parameters of the function `func` as a list of their names.""" return inspect.signature(func).parameters.keys() @@ -225,8 +290,73 @@ def update_sanic_log_level() -> None: async def call_potential_coroutine( coroutine_or_return_value: Union[Any, Coroutine] ) -> Any: - """Await if its a coroutine.""" + """Await if it's a coroutine.""" if asyncio.iscoroutine(coroutine_or_return_value): - return await coroutine_or_return_value # type: ignore [misc] # https://github.com/python/mypy/issues/7587 + return await coroutine_or_return_value # type: ignore[misc] # https://github.com/python/mypy/issues/7587 return coroutine_or_return_value + + +def read_file(filename: Union[Text, Path], encoding: Text = DEFAULT_ENCODING) -> Any: + """Read text from a file.""" + try: + with open(filename, encoding=encoding) as f: + return f.read() + except FileNotFoundError: + raise FileNotFoundException( + f"Failed to read file, " f"'{os.path.abspath(filename)}' does not exist." + ) + except UnicodeDecodeError: + raise FileIOException( + f"Failed to read file '{os.path.abspath(filename)}', " + f"could not read the file using {encoding} to decode " + f"it. Please make sure the file is stored with this " + f"encoding." + ) + + +def read_yaml(content: Text, reader_type: Text = "safe") -> Any: + """Parses yaml from a text. + + Args: + content: A text containing yaml content. + reader_type: Reader type to use. By default, "safe" will be used. + + Raises: + ruamel.yaml.parser.ParserError: If there was an error when parsing the YAML. + """ + if _is_ascii(content): + # Required to make sure emojis are correctly parsed + content = ( + content.encode("utf-8") + .decode("raw_unicode_escape") + .encode("utf-16", "surrogatepass") + .decode("utf-16") + ) + + yaml_parser = yaml.YAML(typ=reader_type) + yaml_parser.version = YAML_VERSION # type: ignore[assignment] + yaml_parser.preserve_quotes = True # type: ignore[assignment] + + return yaml_parser.load(content) or {} + + +def _is_ascii(text: Text) -> bool: + return all(ord(character) < 128 for character in text) + + +def read_yaml_file(filename: Union[Text, Path]) -> Dict[Text, Any]: + """Parses a yaml file. + + Raises an exception if the content of the file can not be parsed as YAML. + + Args: + filename: The path to the file which should be read. + + Returns: + Parsed content of the file. + """ + try: + return read_yaml(read_file(filename, DEFAULT_ENCODING)) + except (YAMLError, DuplicateKeyError) as e: + raise YamlSyntaxException(filename, e) diff --git a/tests/test_utils.py b/tests/test_utils.py index cae59113b..ec32cd343 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,41 @@ +import json +import logging import os -from typing import Callable, Any +import sys +from pathlib import Path +from typing import Any, Text import pytest +from pytest import LogCaptureFixture +from ruamel.yaml import YAMLError import rasa_sdk.utils +from rasa_sdk.exceptions import ( + FileIOException, + FileNotFoundException, + YamlSyntaxException, +) from rasa_sdk.utils import number_of_sanic_workers -from rasa_sdk.constants import DEFAULT_SANIC_WORKERS, ENV_SANIC_WORKERS +from rasa_sdk.constants import ( + APPLICATION_ROOT_LOGGER_NAME, + DEFAULT_SANIC_WORKERS, + ENV_SANIC_WORKERS, +) + + +@pytest.fixture(autouse=True) +def reset_logging() -> None: + manager = logging.root.manager + manager.disabled = logging.NOTSET + for logger in manager.loggerDict.values(): + if isinstance(logger, logging.Logger): + logger.setLevel(logging.NOTSET) + logger.propagate = True + logger.disabled = False + logger.filters.clear() + handlers = logger.handlers.copy() + for handler in handlers: + logger.removeHandler(handler) def test_default_number_of_sanic_workers(): @@ -45,3 +75,185 @@ def my_function(): actual = await rasa_sdk.utils.call_potential_coroutine(my_function()) assert actual == expected + + +def test_read_file_with_not_existing_path(): + with pytest.raises(FileNotFoundException): + rasa_sdk.utils.read_file("some path") + + +def test_read_yaml_string(): + config = """ + user: user + password: pass + """ + content = rasa_sdk.utils.read_yaml(config) + assert content["user"] == "user" and content["password"] == "pass" + + +def test_emojis_in_yaml(): + test_data = """ + data: + - one πŸ˜πŸ’― πŸ‘©πŸΏβ€πŸ’»πŸ‘¨πŸΏβ€πŸ’» + - two Β£ (?u)\\b\\w+\\b f\u00fcr + """ + content = rasa_sdk.utils.read_yaml(test_data) + + assert content["data"][0] == "one πŸ˜πŸ’― πŸ‘©πŸΏβ€πŸ’»πŸ‘¨πŸΏβ€πŸ’»" + assert content["data"][1] == "two Β£ (?u)\\b\\w+\\b fΓΌr" + + +def test_read_file_with_wrong_encoding(tmp_path: Path): + file = tmp_path / "myfile.txt" + file.write_text("Γ€", encoding="latin-1") + with pytest.raises(FileIOException): + rasa_sdk.utils.read_file(file) + + +def test_read_yaml_raises_yaml_error(): + config = """ + user: user + password: pass + """ + with pytest.raises(YAMLError): + rasa_sdk.utils.read_yaml(config) + + +def test_read_valid_yaml_file(): + root_dir = Path(__file__).resolve().parents[1] + file = root_dir / "data/test_logging_config_files/test_valid_logging_config.yml" + content = rasa_sdk.utils.read_yaml_file(file) + + assert content["version"] == 1 + assert content["handlers"]["test_handler"]["formatter"] == "customFormatter" + assert content["loggers"]["rasa_sdk"]["handlers"][0] == "test_handler" + + +def test_read_invalid_yaml_file_raises(): + root_dir = Path(__file__).resolve().parents[1] + file = root_dir / "data/test_invalid_yaml.yml" + with pytest.raises(YamlSyntaxException): + rasa_sdk.utils.read_yaml_file(file) + + +def test_valid_logging_configuration() -> None: + root_dir = Path(__file__).resolve().parents[1] + logging_config_file = ( + root_dir / "data/test_logging_config_files/test_valid_logging_config.yml" + ) + rasa_sdk.utils.configure_logging_from_input_file( + logging_config_file=logging_config_file + ) + rasa_sdk_logger = logging.getLogger("rasa_sdk") + + handlers = rasa_sdk_logger.handlers + assert len(handlers) == 1 + assert isinstance(handlers[0], logging.FileHandler) + assert "test_handler" == rasa_sdk_logger.handlers[0].name + + logging_message = "This is a test info log." + rasa_sdk_logger.info(logging_message) + + handler_filename = handlers[0].baseFilename + assert Path(handler_filename).exists() + + with open(handler_filename, "r") as logs: + data = logs.readlines() + logs_dict = json.loads(data[-1]) + assert logs_dict.get("message") == logging_message + + for key in ["time", "name", "levelname"]: + assert key in logs_dict.keys() + + +@pytest.mark.parametrize( + "logging_config_file", + [ + "data/test_logging_config_files/test_missing_required_key_invalid_config.yml", + "data/test_logging_config_files/test_invalid_value_for_level_in_config.yml", + "data/test_logging_config_files/test_invalid_handler_key_in_config.yml", + ], +) +def test_cli_invalid_logging_configuration( + logging_config_file: Text, caplog: LogCaptureFixture +) -> None: + root_dir = Path(__file__).resolve().parents[1] + file = root_dir / logging_config_file + with caplog.at_level(logging.DEBUG): + rasa_sdk.utils.configure_logging_from_input_file(logging_config_file=file) + + assert ( + f"The logging config file {file} could not be applied " + f"because it failed validation against the built-in Python " + f"logging schema." in caplog.text + ) + + +@pytest.mark.skipif( + sys.version_info.minor == 7, reason="no error is raised with python 3.7" +) +def test_cli_invalid_format_value_in_config(caplog: LogCaptureFixture) -> None: + root_dir = Path(__file__).resolve().parents[1] + logging_config_file = ( + root_dir + / "data/test_logging_config_files/test_invalid_format_value_in_config.yml" + ) + + with caplog.at_level(logging.DEBUG): + rasa_sdk.utils.configure_logging_from_input_file( + logging_config_file=logging_config_file + ) + + assert ( + f"The logging config file {logging_config_file} could not be applied " + f"because it failed validation against the built-in Python " + f"logging schema." in caplog.text + ) + + +@pytest.mark.skipif( + sys.version_info.minor == 9, reason="no error is raised with python 3.9" +) +def test_cli_non_existent_handler_id_in_config(caplog: LogCaptureFixture) -> None: + root_dir = Path(__file__).resolve().parents[1] + logging_config_file = ( + root_dir / "data/test_logging_config_files/test_non_existent_handler_id.yml" + ) + + with caplog.at_level(logging.DEBUG): + rasa_sdk.utils.configure_logging_from_input_file( + logging_config_file=logging_config_file + ) + + assert ( + f"The logging config file {logging_config_file} could not be applied " + f"because it failed validation against the built-in Python " + f"logging schema." in caplog.text + ) + + +def test_configure_default_logging(): + output_file = "test_default_logging.log" + rasa_sdk.utils.configure_file_logging( + logging.getLogger(APPLICATION_ROOT_LOGGER_NAME), + output_file, + logging.INFO, + None, + ) + rasa_sdk_logger = logging.getLogger("rasa_sdk") + + handlers = rasa_sdk_logger.handlers + assert len(handlers) == 1 + handler = handlers[0] + assert isinstance(handler, logging.FileHandler) + + logging_message = "Testing info log." + rasa_sdk_logger.info(logging_message) + + handler_filename = handler.baseFilename + assert Path(handler_filename).exists() + assert Path(handler_filename).name == output_file + + with open(handler_filename, "r") as logs: + data = logs.readlines() + assert "[INFO ] rasa_sdk - Testing info log." in data[-1]