diff --git a/.gitignore b/.gitignore index 3ad3f14..8c19112 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ venv.bak/ .vscode/ /log/ tmp/ +libs/skyline-apiserver/log/ # MAC OS .DS_Store diff --git a/libs/skyline-apiserver/Makefile b/libs/skyline-apiserver/Makefile index 936a1a9..1301b59 100644 --- a/libs/skyline-apiserver/Makefile +++ b/libs/skyline-apiserver/Makefile @@ -45,17 +45,17 @@ lint: # poetry run mypy --config-file=../../mypy.ini $(PY_FILES) poetry run isort --check-only --diff $(PY_FILES) poetry run black --check --diff --color --config ../../pyproject.toml $(PY_FILES) - poetry run flake8 $(PY_FILES) + poetry run flake8 --config ../../.flake8 $(PY_FILES) .PHONY: test test: - echo TODO + poetry run pytest .PHONY: clean clean: - rm -rf .venv dist + rm -rf .venv dist htmlcov .coverage log .PHONY: db_revision @@ -74,4 +74,4 @@ db_sync: # Find python files without "type annotations" future_check: - @find src ! -size 0 -type f -name *.py -exec grep -L 'from __future__ import annotations' {} \; + @find skyline_apiserver ! -size 0 -type f -name *.py -exec grep -L 'from __future__ import annotations' {} \; diff --git a/libs/skyline-apiserver/poetry.lock b/libs/skyline-apiserver/poetry.lock index 0ba348c..331e964 100644 --- a/libs/skyline-apiserver/poetry.lock +++ b/libs/skyline-apiserver/poetry.lock @@ -255,6 +255,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.3.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" version = "37.0.2" @@ -737,6 +748,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mimesis" +version = "4.1.3" +description = "Mimesis: fake data generator." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "msgpack" version = "1.0.3" @@ -1159,7 +1178,7 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] [[package]] name = "pyparsing" -version = "3.0.8" +version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false @@ -1227,6 +1246,22 @@ pytest = ">=5.4.0" [package.extras] testing = ["coverage", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "pytest-forked" version = "1.4.0" @@ -1239,6 +1274,29 @@ python-versions = ">=3.6" py = "*" pytest = ">=3.10" +[[package]] +name = "pytest-html" +version = "3.1.1" +description = "pytest plugin for generating HTML reports" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0,<6.0.0 || >6.0.0" +pytest-metadata = "*" + +[[package]] +name = "pytest-metadata" +version = "1.11.0" +description = "pytest plugin for test session metadata" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +pytest = ">=2.9.0" + [[package]] name = "pytest-xdist" version = "2.4.0" @@ -1614,15 +1672,10 @@ version = "0.1.0" description = "" category = "main" optional = false -python-versions = "^3.8" -develop = true +python-versions = ">=3.8,<4.0" [package.dependencies] -loguru = "0.5.3" - -[package.source] -type = "directory" -url = "../skyline-log" +loguru = "*" [[package]] name = "skyline-policy-manager" @@ -1872,7 +1925,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "6fe9ff8d4b419b55eeb52e5c28abbc395362082f9baf915802c0afedfe6806dd" +content-hash = "ded4edfc5f1c33f84ed7fb28b5e2adadc53e1e694c8d78010156ff1b9a60f971" [metadata.files] add-trailing-comma = [ @@ -2011,6 +2064,49 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, +] cryptography = [ {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, @@ -2255,6 +2351,9 @@ mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +mimesis = [ + {file = "mimesis-4.1.3.tar.gz", hash = "sha256:90f36c21c1bb9944afc17178eb5868b0c85aa1fe49eb04bcbdafafd1ad4ca2ba"}, +] msgpack = [ {file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96acc674bb9c9be63fa8b6dabc3248fdc575c4adc005c440ad02f87ca7edd079"}, {file = "msgpack-1.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c3ca57c96c8e69c1a0d2926a6acf2d9a522b41dc4253a8945c4c6cd4981a4e3"}, @@ -2534,8 +2633,8 @@ pyopenssl = [ {file = "pyOpenSSL-22.0.0.tar.gz", hash = "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf"}, ] pyparsing = [ - {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, - {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pyperclip = [ {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, @@ -2575,10 +2674,22 @@ pytest-asyncio = [ {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, ] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] pytest-forked = [ {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, ] +pytest-html = [ + {file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"}, + {file = "pytest_html-3.1.1-py3-none-any.whl", hash = "sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"}, +] +pytest-metadata = [ + {file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"}, + {file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"}, +] pytest-xdist = [ {file = "pytest-xdist-2.4.0.tar.gz", hash = "sha256:89b330316f7fc475f999c81b577c2b926c9569f3d397ae432c0c2e2496d61ff9"}, {file = "pytest_xdist-2.4.0-py3-none-any.whl", hash = "sha256:7b61ebb46997a0820a263553179d6d1e25a8c50d8a8620cd1aa1e20e3be99168"}, @@ -2829,7 +2940,9 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] skyline-config = [] -skyline-log = [] +skyline-log = [ + {file = "skyline_log-0.1.0-py3-none-any.whl", hash = "sha256:60e61784ce43061c62ea424d271fd6ad0c04ba2a9e2df5d1e1f490a9cceb8d3b"}, +] skyline-policy-manager = [] sniffio = [ {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, diff --git a/libs/skyline-apiserver/pyproject.toml b/libs/skyline-apiserver/pyproject.toml index 294fc8f..db10ebb 100644 --- a/libs/skyline-apiserver/pyproject.toml +++ b/libs/skyline-apiserver/pyproject.toml @@ -23,6 +23,7 @@ aiomysql = "0.0.21" pymysql = "0.9.3" aiosqlite = "0.17.0" dnspython = "2.1.0" +loguru = "0.5.3" python-keystoneclient = "3.21.0" python-cinderclient = "5.0.2" python-glanceclient = "2.17.1" @@ -33,7 +34,6 @@ python-octaviaclient = "1.10.1" osc-placement = "1.7.0" keystoneauth1 = "3.17.4" skyline-policy-manager = "*" -skyline-log = "*" skyline-config = "*" [tool.poetry.dev-dependencies] @@ -45,16 +45,29 @@ mypy = "0.910" pytest = "6.2.5" pytest-xdist = "2.4.0" pytest-asyncio = "0.15.1" +pytest-cov = "2.12.1" +pytest-html = "3.1.1" +mimesis = "4.1.3" +click = "7.1.2" asgi-lifespan = "1.0.1" types-PyYAML = "5.4.10" skyline-policy-manager = {path = "../skyline-policy-manager", develop = true} -skyline-log = {path = "../skyline-log", develop = true} skyline-config = {path = "../skyline-config", develop = true} [tool.poetry.scripts] swagger-generator = 'skyline_apiserver.cmd.generate_swagger:main' config-sample-generator = 'skyline_apiserver.cmd.generate_sample_config:main' +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-v -s -p no:cacheprovider -n auto --cov=skyline_apiserver --cov-append --cov-report=term-missing --cov-report=html" +testpaths = [ + "skyline_apiserver/tests", +] +markers = [ + "ddt(*args: TestData): Mark the test as a data-driven test." +] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/libs/skyline-apiserver/skyline_apiserver/__main__.py b/libs/skyline-apiserver/skyline_apiserver/__main__.py index a7f161b..bc6b4c6 100644 --- a/libs/skyline-apiserver/skyline_apiserver/__main__.py +++ b/libs/skyline-apiserver/skyline_apiserver/__main__.py @@ -20,7 +20,7 @@ from pprint import pprint import uvloop from skyline_apiserver.config import configure -from skyline_log import setup +from skyline_apiserver.log import setup async def main() -> None: diff --git a/libs/skyline-apiserver/skyline_apiserver/api/v1/contrib.py b/libs/skyline-apiserver/skyline_apiserver/api/v1/contrib.py index 8f073cb..de3aae1 100644 --- a/libs/skyline-apiserver/skyline_apiserver/api/v1/contrib.py +++ b/libs/skyline-apiserver/skyline_apiserver/api/v1/contrib.py @@ -22,9 +22,9 @@ from skyline_apiserver import schemas from skyline_apiserver.client.openstack import system from skyline_apiserver.client.openstack.system import get_endpoints from skyline_apiserver.config import CONF +from skyline_apiserver.log import LOG from skyline_apiserver.schemas import common from skyline_apiserver.types import constants -from skyline_log import LOG router = APIRouter() diff --git a/libs/skyline-apiserver/skyline_apiserver/api/v1/login.py b/libs/skyline-apiserver/skyline_apiserver/api/v1/login.py index 81f0e84..c8e6ad4 100644 --- a/libs/skyline-apiserver/skyline_apiserver/api/v1/login.py +++ b/libs/skyline-apiserver/skyline_apiserver/api/v1/login.py @@ -35,8 +35,8 @@ from skyline_apiserver.core.security import ( parse_access_token, ) from skyline_apiserver.db import api as db_api +from skyline_apiserver.log import LOG from skyline_apiserver.types import constants -from skyline_log import LOG router = APIRouter() diff --git a/libs/skyline-apiserver/skyline_apiserver/client/openstack/system.py b/libs/skyline-apiserver/skyline_apiserver/client/openstack/system.py index 7b0f6ae..e1562ba 100644 --- a/libs/skyline-apiserver/skyline_apiserver/client/openstack/system.py +++ b/libs/skyline-apiserver/skyline_apiserver/client/openstack/system.py @@ -22,8 +22,8 @@ from keystoneauth1.session import Session from skyline_apiserver.client import utils from skyline_apiserver.client.utils import get_system_session from skyline_apiserver.config import CONF +from skyline_apiserver.log import LOG from skyline_apiserver.types import constants -from skyline_log import LOG from starlette.concurrency import run_in_threadpool diff --git a/libs/skyline-apiserver/skyline_apiserver/db/alembic/env.py b/libs/skyline-apiserver/skyline_apiserver/db/alembic/env.py index fd12450..dcb4ada 100644 --- a/libs/skyline-apiserver/skyline_apiserver/db/alembic/env.py +++ b/libs/skyline-apiserver/skyline_apiserver/db/alembic/env.py @@ -18,7 +18,7 @@ from alembic import context from databases import DatabaseURL from skyline_apiserver.config import CONF, configure from skyline_apiserver.db.models import METADATA -from skyline_log import setup as log_setup +from skyline_apiserver.log import setup as log_setup from sqlalchemy import create_engine, pool configure("skyline") diff --git a/libs/skyline-apiserver/skyline_apiserver/log/__init__.py b/libs/skyline-apiserver/skyline_apiserver/log/__init__.py new file mode 100644 index 0000000..35346e2 --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/log/__init__.py @@ -0,0 +1,83 @@ +# Copyright 2021 99cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect +import logging +from logging import Handler, LogRecord +from pathlib import PurePath +from typing import Optional, Union + +import loguru +from loguru import logger + +LOG = loguru.logger + + +class InterceptHandler(logging.Handler): + def emit(self, record: LogRecord) -> None: + # Get corresponding Loguru level if it exists + level = getattr(logger.level(record.levelname), "name", record.levelno) + + # Find caller from where originated the logged message + frame, depth = getattr(inspect.currentframe(), "f_back", None), 1 + while frame and frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log( + level, + record.getMessage(), + ) + + +def setup( + sink: Union[PurePath, str, Handler], + debug: bool = False, + colorize: bool = False, + level: Optional[str] = None, +) -> None: + if debug: + default_level = "DEBUG" + backtrace = True + diagnose = True + else: + default_level = "WARNING" + backtrace = False + diagnose = True + if level is None: + level = default_level + + LOG.remove() + LOG.add( + sink, + level=level, + format=( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} |" + " {name}:{function}:{line} -" + " {message}" + ), + filter=None, + colorize=colorize, + backtrace=backtrace, + diagnose=diagnose, + serialize=False, + enqueue=False, + catch=True, + ) + logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) + + +__all__ = ("LOG", "setup") diff --git a/libs/skyline-apiserver/skyline_apiserver/main.py b/libs/skyline-apiserver/skyline_apiserver/main.py index bbc4449..d395ed0 100644 --- a/libs/skyline-apiserver/skyline_apiserver/main.py +++ b/libs/skyline-apiserver/skyline_apiserver/main.py @@ -20,8 +20,8 @@ from fastapi import FastAPI from skyline_apiserver.api.v1 import api_router from skyline_apiserver.config import CONF, configure from skyline_apiserver.db import setup as db_setup +from skyline_apiserver.log import LOG, setup as log_setup from skyline_apiserver.policies import setup as policies_setup -from skyline_log import LOG, setup as log_setup from starlette.middleware.cors import CORSMiddleware PROJECT_NAME = "Skyline API" diff --git a/libs/skyline-apiserver/tests/api/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/__init__.py similarity index 100% rename from libs/skyline-apiserver/tests/api/__init__.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/__init__.py diff --git a/libs/skyline-apiserver/tests/api/v1/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/__init__.py similarity index 100% rename from libs/skyline-apiserver/tests/api/v1/__init__.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/v1/__init__.py diff --git a/libs/skyline-apiserver/tests/api/v1/test_contrib.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_contrib.py similarity index 100% rename from libs/skyline-apiserver/tests/api/v1/test_contrib.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_contrib.py diff --git a/libs/skyline-apiserver/tests/api/v1/test_extension.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_extension.py similarity index 100% rename from libs/skyline-apiserver/tests/api/v1/test_extension.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_extension.py diff --git a/libs/skyline-apiserver/tests/api/v1/test_login.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_login.py similarity index 91% rename from libs/skyline-apiserver/tests/api/v1/test_login.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_login.py index 42f892b..2a059f1 100644 --- a/libs/skyline-apiserver/tests/api/v1/test_login.py +++ b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_login.py @@ -84,6 +84,7 @@ async def test_switch_project(client: AsyncClient, login_jwt: str) -> None: await utils._logout(client) +@pytest.mark.skipif(os.getenv("TEST_API") != "true", reason="No backend OpenStack for api-test.") @pytest.mark.asyncio async def test_get_profile_no_auth(client: AsyncClient) -> None: r = await client.get(f"{main.API_PREFIX}/profile") @@ -92,6 +93,7 @@ async def test_get_profile_no_auth(client: AsyncClient) -> None: assert result["detail"] == constants.ERR_MSG_TOKEN_NOTFOUND +@pytest.mark.skipif(os.getenv("TEST_API") != "true", reason="No backend OpenStack for api-test.") @pytest.mark.asyncio async def test_get_profile_token_expire(client: AsyncClient) -> None: profile = utils.get_session_profile() @@ -107,6 +109,7 @@ async def test_get_profile_token_expire(client: AsyncClient) -> None: assert result["detail"].startswith(constants.ERR_MSG_TOKEN_EXPIRED) +@pytest.mark.skipif(os.getenv("TEST_API") != "true", reason="No backend OpenStack for api-test.") @pytest.mark.asyncio async def test_get_profile_token_revoke(client: AsyncClient) -> None: profile = utils.get_session_profile() @@ -123,6 +126,7 @@ async def test_get_profile_token_revoke(client: AsyncClient) -> None: assert result["detail"] == constants.ERR_MSG_TOKEN_REVOKED +@pytest.mark.skipif(os.getenv("TEST_API") != "true", reason="No backend OpenStack for api-test.") @pytest.mark.asyncio async def test_logout(client: AsyncClient, session_token: str) -> None: r = await client.post( diff --git a/libs/skyline-apiserver/tests/api/v1/test_setting.py b/libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_setting.py similarity index 100% rename from libs/skyline-apiserver/tests/api/v1/test_setting.py rename to libs/skyline-apiserver/skyline_apiserver/tests/api/v1/test_setting.py diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/conftest.py b/libs/skyline-apiserver/skyline_apiserver/tests/conftest.py new file mode 100644 index 0000000..fb89810 --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/tests/conftest.py @@ -0,0 +1,70 @@ +# Copyright 2021 99cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, Iterator, Optional + +import pytest +from _pytest.mark import ParameterSet +from asgi_lifespan import LifespanManager +from httpx import AsyncClient +from skyline_apiserver.config import CONF +from skyline_apiserver.main import app +from skyline_apiserver.tests.models import TestData +from skyline_apiserver.tests.utils import utils + +if TYPE_CHECKING: + from _pytest.python import Metafunc + + +@pytest.fixture(scope="function") +async def client() -> Iterator[AsyncClient]: + async with LifespanManager(app): + async with AsyncClient(app=app, base_url="http://test") as ac: + yield ac + + CONF.cleanup() + + +@pytest.fixture(scope="function") +async def session_token(client: AsyncClient) -> str: + return utils.get_session_token() + + +@pytest.fixture(scope="function") +async def login_jwt(client: AsyncClient) -> str: + return await utils.get_jwt_from_cookie(client) + + +def pytest_generate_tests(metafunc: Optional["Metafunc"]) -> None: + for marker in metafunc.definition.iter_markers(name="ddt"): + test_data: TestData + for test_data in marker.args: + argument_length = len(test_data.arguments) + argvalues = [] + for argument_data in test_data.argument_data_set: + if len(argument_data.values) != argument_length: + raise ValueError( + f'Argument data "{argument_data.id}" of method ' + f'"{metafunc.function.__name__}" doesn\'t match ' + "number of arguments.", + ) + argvalues.append( + ParameterSet( + id=argument_data.id, + marks=argument_data.marks, + values=argument_data.values, + ), + ) + + metafunc.parametrize(test_data.arguments, argvalues, indirect=test_data.indirect) diff --git a/libs/skyline-apiserver/tests/core/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/core/__init__.py similarity index 100% rename from libs/skyline-apiserver/tests/core/__init__.py rename to libs/skyline-apiserver/skyline_apiserver/tests/core/__init__.py diff --git a/libs/skyline-apiserver/tests/core/config/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/core/config/__init__.py similarity index 100% rename from libs/skyline-apiserver/tests/core/config/__init__.py rename to libs/skyline-apiserver/skyline_apiserver/tests/core/config/__init__.py diff --git a/libs/skyline-apiserver/tests/core/config/backup_test_base.py b/libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_base.py similarity index 100% rename from libs/skyline-apiserver/tests/core/config/backup_test_base.py rename to libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_base.py diff --git a/libs/skyline-apiserver/tests/core/config/backup_test_default.py b/libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_default.py similarity index 100% rename from libs/skyline-apiserver/tests/core/config/backup_test_default.py rename to libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_default.py diff --git a/libs/skyline-apiserver/tests/core/config/backup_test_openstack.py b/libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_openstack.py similarity index 100% rename from libs/skyline-apiserver/tests/core/config/backup_test_openstack.py rename to libs/skyline-apiserver/skyline_apiserver/tests/core/config/backup_test_openstack.py diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/fake.py b/libs/skyline-apiserver/skyline_apiserver/tests/fake.py new file mode 100644 index 0000000..e9a03fe --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/tests/fake.py @@ -0,0 +1,19 @@ +# Copyright 2021 99cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from mimesis import Generic + +FAKER = Generic() diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/models.py b/libs/skyline-apiserver/skyline_apiserver/tests/models.py new file mode 100644 index 0000000..c7cb00a --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/tests/models.py @@ -0,0 +1,36 @@ +# Copyright 2021 99cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Collection, Sequence, Tuple, Union + + +@dataclass +class ArgumentData: + id: str + values: Sequence[object] + # TODO: Fix type annotation of `marks` after the pytest > 7.0.0 + # marks: Collection[Union[pytest.MarkDecorator, pytest.Mark]] + marks: Collection[Any] = () + + +@dataclass +class TestData: + arguments: Tuple[str, ...] + argument_data_set: Sequence[ArgumentData] + indirect: Union[bool, Tuple[str]] = False + + __test__ = False diff --git a/libs/skyline-apiserver/tests/utils/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/unit/__init__.py similarity index 100% rename from libs/skyline-apiserver/tests/utils/__init__.py rename to libs/skyline-apiserver/skyline_apiserver/tests/unit/__init__.py diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/unit/log/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/unit/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/unit/log/test_log.py b/libs/skyline-apiserver/skyline_apiserver/tests/unit/log/test_log.py new file mode 100644 index 0000000..7436cb2 --- /dev/null +++ b/libs/skyline-apiserver/skyline_apiserver/tests/unit/log/test_log.py @@ -0,0 +1,226 @@ +# Copyright 2021 99cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging +from logging import StreamHandler +from pathlib import Path + +import pytest +from _pytest.capture import CaptureFixture +from _pytest.fixtures import SubRequest +from skyline_apiserver.log import LOG, setup as log_setup +from skyline_apiserver.tests.fake import FAKER +from skyline_apiserver.tests.models import ArgumentData, TestData + + +class TestLog: + @pytest.fixture + def file_sink_captor(self, request: SubRequest, tmp_path: Path) -> Path: + file_name: str = request.param + file = tmp_path.joinpath(file_name) + file.touch() + return file + + @pytest.mark.ddt( + TestData( + arguments=("file_sink_captor",), + indirect=("file_sink_captor",), + argument_data_set=[ + ArgumentData( + id="str_file_path", + values=(FAKER.text.word(),), + ), + ], + ), + TestData( + arguments=("debug",), + argument_data_set=[ + ArgumentData( + id="enable_debug", + values=(True,), + ), + ArgumentData( + id="disable_debug", + values=(False,), + ), + ], + ), + TestData( + arguments=("level",), + argument_data_set=[ + ArgumentData( + id="debug_level", + values=("debug",), + ), + ArgumentData( + id="info_level", + values=("info",), + ), + ArgumentData( + id="warning_level", + values=("warning",), + ), + ArgumentData( + id="error_level", + values=("error",), + ), + ], + ), + ) + def test_file_sink_setup(self, file_sink_captor: Path, debug: bool, level: str) -> None: + log_setup(file_sink_captor.as_posix(), debug) + content = FAKER.text.text() + log = getattr(LOG, level) + log(content) + file_content = file_sink_captor.read_text() + if debug is False and level in ["debug", "info"]: + assert f"| {level.upper():<8} |" not in file_content + assert content not in file_content + else: + assert f"| {level.upper():<8} |" in file_content + assert content in file_content + + @pytest.fixture + def stream_sink_captor( + self, + request: SubRequest, + capsys: CaptureFixture[str], + ) -> CaptureFixture[str]: + return capsys + + @pytest.mark.ddt( + TestData( + arguments=("stream_sink_captor",), + indirect=("stream_sink_captor",), + argument_data_set=[ + ArgumentData( + id="std_output", + values=(StreamHandler,), + ), + ], + ), + TestData( + arguments=("debug",), + argument_data_set=[ + ArgumentData( + id="enable_debug", + values=(True,), + ), + ArgumentData( + id="disable_debug", + values=(False,), + ), + ], + ), + TestData( + arguments=("level",), + argument_data_set=[ + ArgumentData( + id="debug_level", + values=("debug",), + ), + ArgumentData( + id="info_level", + values=("info",), + ), + ArgumentData( + id="warning_level", + values=("warning",), + ), + ArgumentData( + id="error_level", + values=("error",), + ), + ], + ), + ) + def test_stream_sink_setup( + self, + stream_sink_captor: CaptureFixture[str], + debug: bool, + level: str, + ) -> None: + log_setup(StreamHandler(), debug) + content = FAKER.text.text() + log = getattr(LOG, level) + log(content) + std_out, std_err = stream_sink_captor.readouterr() + if debug is False and level in ["debug", "info"]: + assert f"| {level.upper():<8} |" not in std_err + assert content not in std_err + else: + assert f"| {level.upper():<8} |" in std_err + assert content in std_err + + @pytest.mark.ddt( + TestData( + arguments=("file_sink_captor",), + indirect=("file_sink_captor",), + argument_data_set=[ + ArgumentData( + id="str_file_path", + values=(FAKER.text.word(),), + ), + ], + ), + TestData( + arguments=("debug",), + argument_data_set=[ + ArgumentData( + id="enable_debug", + values=(True,), + ), + ArgumentData( + id="disable_debug", + values=(False,), + ), + ], + ), + TestData( + arguments=("level",), + argument_data_set=[ + ArgumentData( + id="debug_level", + values=("debug",), + ), + ArgumentData( + id="info_level", + values=("info",), + ), + ArgumentData( + id="warning_level", + values=("warning",), + ), + ArgumentData( + id="error_level", + values=("error",), + ), + ], + ), + ) + def test_standard_logging(self, file_sink_captor: Path, debug: bool, level: str) -> None: + log_setup(file_sink_captor.as_posix(), debug) + content = FAKER.text.text() + std_logger = logging.getLogger() + log = getattr(std_logger, level) + log(content) + file_content = file_sink_captor.read_text() + if debug is False and level in ["debug", "info"]: + assert f"| {level.upper():<8} |" not in file_content + assert content not in file_content + else: + assert f"| {level.upper():<8} |" in file_content + assert content in file_content diff --git a/libs/skyline-apiserver/skyline_apiserver/tests/utils/__init__.py b/libs/skyline-apiserver/skyline_apiserver/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/skyline-apiserver/tests/utils/utils.py b/libs/skyline-apiserver/skyline_apiserver/tests/utils/utils.py similarity index 100% rename from libs/skyline-apiserver/tests/utils/utils.py rename to libs/skyline-apiserver/skyline_apiserver/tests/utils/utils.py diff --git a/libs/skyline-apiserver/tests/conftest.py b/libs/skyline-apiserver/tests/conftest.py deleted file mode 100644 index 65485d1..0000000 --- a/libs/skyline-apiserver/tests/conftest.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2021 99cloud -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Iterator - -import pytest -from asgi_lifespan import LifespanManager -from httpx import AsyncClient -from skyline_apiserver.config import CONF -from skyline_apiserver.main import app -from utils import utils - - -@pytest.fixture(scope="function") -async def client() -> Iterator[AsyncClient]: - async with LifespanManager(app): - async with AsyncClient(app=app, base_url="http://test") as ac: - yield ac - - CONF.cleanup() - - -@pytest.fixture(scope="function") -async def session_token(client: AsyncClient) -> str: - return utils.get_session_token() - - -@pytest.fixture(scope="function") -async def login_jwt(client: AsyncClient) -> str: - return await utils.get_jwt_from_cookie(client)