diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..52d433b --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,84 @@ +name: Check + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +env: + PACKAGE_DIR: powerbi_ext + TESTS_DIR: tests + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + run: | + pipx install poetry + poetry --version + + - name: Install dependencies + run: | + poetry install + + - name: Test with pytest + run: poetry run pytest --cov=$PACKAGE_DIR $TESTS_DIR + + lint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + run: | + pipx install poetry + poetry --version + + - name: Install dependencies + run: | + poetry install + + - name: Lint with pylint + run: poetry run pylint --jobs 0 $PACKAGE_DIR --fail-under 9 + + format: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install black + run: pip install black + + - name: Check format with black + run: black --check $PACKAGE_DIR diff --git a/poetry.lock b/poetry.lock index fe6f93a..c61606b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,19 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "astroid" +version = "3.0.0" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "astroid-3.0.0-py3-none-any.whl", hash = "sha256:f2510e7fdcd6cfda4ec50014726d4857abf79acfc010084ce8c26091913f1b25"}, + {file = "astroid-3.0.0.tar.gz", hash = "sha256:1defdbca052635dd29657ea674edfc45e4b5be9cd53630c5b084fcfed94344a8"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + [[package]] name = "asttokens" version = "2.4.0" @@ -287,6 +301,73 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.3.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" version = "41.0.4" @@ -348,6 +429,34 @@ asttokens = ">=2.0.0,<3.0.0" executing = ">=1.1.1" pygments = ">=2.15.0" +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "exceptiongroup" +version = "1.1.3" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "executing" version = "2.0.0" @@ -389,6 +498,17 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isort" version = "5.12.0" @@ -489,6 +609,17 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + [[package]] name = "pathspec" version = "0.11.2" @@ -515,6 +646,21 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "portalocker" version = "2.8.2" @@ -653,6 +799,75 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pylint" +version = "3.0.1" +description = "python code static checker" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pylint-3.0.1-py3-none-any.whl", hash = "sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea"}, + {file = "pylint-3.0.1.tar.gz", hash = "sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40"}, +] + +[package.dependencies] +astroid = ">=3.0.0,<=3.1.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, +] +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "7.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "pywin32" version = "306" @@ -785,6 +1000,17 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tomlkit" +version = "0.12.1" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, + {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, +] + [[package]] name = "typer" version = "0.6.1" @@ -836,4 +1062,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "bc2ea5409378b620210011d5aae2a929e8054b452b7d2d4e89dec7609357dff4" +content-hash = "8cb1f3433ce48b700e630301b8c1f75010750be746c19eee6374dd40194868b4" diff --git a/powerbi_ext/auth.py b/powerbi_ext/auth.py index 5abd7a2..22d714c 100644 --- a/powerbi_ext/auth.py +++ b/powerbi_ext/auth.py @@ -1,15 +1,18 @@ +"""PowerBI authentication module.""" import os +import typing as t from azure.identity import ClientSecretCredential SCOPE = "https://analysis.windows.net/powerbi/api/.default" -def get_token( - tenant_id: str = None, - client_id: str = None, - client_secret: str = None, +def get_credential( + tenant_id: t.Optional[str] = None, + client_id: t.Optional[str] = None, + client_secret: t.Optional[str] = None, ): + """Get Azure ClientSecretCredential using Meltano env variables""" if not tenant_id: tenant_id = os.environ["POWERBI_EXT_TENANT_ID"] if not client_id: @@ -23,4 +26,21 @@ def get_token( client_secret=client_secret, ) - return credential.get_token(SCOPE).token + return credential + + +def get_token( + tenant_id: t.Optional[str] = None, + client_id: t.Optional[str] = None, + client_secret: t.Optional[str] = None, +): + """Get Azure token""" + credential = get_credential( + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + ) + + access_token = credential.get_token(SCOPE) + token = access_token.token + return token diff --git a/powerbi_ext/extension.py b/powerbi_ext/extension.py index c84d1bb..abb1200 100644 --- a/powerbi_ext/extension.py +++ b/powerbi_ext/extension.py @@ -17,10 +17,11 @@ class PowerBIExtension(ExtensionBase): """Extension implementing the ExtensionBase interface.""" - def __init__(self) -> None: + def __init__(self, token: t.Optional[str] = None) -> None: """Initialize the extension.""" self.log = structlog.get_logger(name=self.__class__.__name__) - token = get_token() + if not token: + token = get_token() self.log.info("Bearer token accessed.") self.headers = {"Authorization": f"Bearer {token}"} @@ -57,9 +58,9 @@ def refresh( res = requests.post(url, json=body, headers=self.headers, timeout=TIMEOUT) self.log.info(res.status_code) if res.status_code != 200: - print(res.reason, res.headers) - else: - return res.headers["RequestId"] + self.log.error(res.reason, res.headers) + raise requests.RequestException(res.status_code, res.reason, res.headers) + return res.headers["RequestId"] def describe(self) -> models.Describe: """Describe the extension. diff --git a/powerbi_ext/main.py b/powerbi_ext/main.py index f3e2323..157dedc 100644 --- a/powerbi_ext/main.py +++ b/powerbi_ext/main.py @@ -1,7 +1,5 @@ """PowerBI cli entrypoint.""" -import sys - import structlog import typer from meltano.edk.extension import DescribeFormat @@ -13,8 +11,6 @@ log = structlog.get_logger(APP_NAME) -ext = PowerBIExtension() - app = typer.Typer( name=APP_NAME, pretty_exceptions_enable=False, @@ -28,13 +24,8 @@ def describe( ) ) -> None: """Describe the available commands of this extension.""" - try: - typer.echo(ext.describe_formatted(output_format)) - except Exception: - log.exception( - "describe failed with uncaught exception, please report to maintainer" - ) - sys.exit(1) + ext = PowerBIExtension() + typer.echo(ext.describe_formatted(output_format)) @app.command() @@ -43,14 +34,12 @@ def refresh( None, "-w", "--workspace", - envvar="POWERBI_EXT_WORKSPACE_ID", - show_envvar=True, help="Workspace ID. If not provided, will look for Dataset in 'My Workspace'", ), dataset_id: str = typer.Argument(..., help="Dataset ID"), ) -> None: """Refresh the given dataset in the given workspace""" - + ext = PowerBIExtension() typer.echo(ext.refresh(workspace_id, dataset_id)) diff --git a/pyproject.toml b/pyproject.toml index 6d52afd..26477f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,13 @@ typer = "^0.6.1" azure-identity = "^1.14.0" requests = "^2.31.0" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] black = "^22.3.0" isort = "^5.10.1" flake8 = "^3.9.0" +pytest = "^7.4.2" +pytest-cov = "^4.1.0" +pylint = "^3.0.1" [build-system] requires = ["poetry-core>=1.0.8"] diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..faabfb6 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,79 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +from azure.identity import ClientSecretCredential + +from powerbi_ext.auth import SCOPE, get_credential, get_token + +TOKEN = "token" +TENANT_ID, CLIENT_ID, CLIENT_SECRET = "tenant_id", "client_id", "client_secret" + + +def test_get_credential_with_args(): + with patch.object(ClientSecretCredential, "__new__") as mock_ClientSecretCredential: + get_credential( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + ) + + mock_ClientSecretCredential.assert_called_once_with( + ClientSecretCredential, + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + ) + + +def test_get_credential_without_args_missing_envvar_tenant_id(): + if os.getenv("POWERBI_EXT_TENANT_ID"): + del os.environ["POWERBI_EXT_TENANT_ID"] + os.environ["POWERBI_EXT_CLIENT_ID"] = CLIENT_ID + os.environ["POWERBI_EXT_CLIENT_SECRET"] = CLIENT_SECRET + + with pytest.raises(KeyError, match="POWERBI_EXT_TENANT_ID"): + get_credential() + + +def test_get_credential_without_args_missing_envvar_client_id(): + os.environ["POWERBI_EXT_TENANT_ID"] = TENANT_ID + if os.getenv("POWERBI_EXT_CLIENT_ID"): + del os.environ["POWERBI_EXT_CLIENT_ID"] + os.environ["POWERBI_EXT_CLIENT_SECRET"] = CLIENT_SECRET + + with pytest.raises(KeyError, match="POWERBI_EXT_CLIENT_ID"): + get_credential() + + +def test_get_credential_without_args_missing_envvar_client_secret(): + os.environ["POWERBI_EXT_TENANT_ID"] = TENANT_ID + os.environ["POWERBI_EXT_CLIENT_ID"] = CLIENT_ID + if os.getenv("POWERBI_EXT_CLIENT_SECRET"): + del os.environ["POWERBI_EXT_CLIENT_SECRET"] + + with pytest.raises(KeyError, match="POWERBI_EXT_CLIENT_SECRET"): + get_credential() + + +def test_get_token(): + mock_access_token = MagicMock(token=TOKEN) + mock_get_token = MagicMock(return_value=mock_access_token) + mock_credential = MagicMock(get_token=mock_get_token) + with patch( + "powerbi_ext.auth.get_credential", return_value=mock_credential + ) as mock_get_credential: + result = get_token( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + ) + + mock_get_credential.assert_called_once_with( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + ) + mock_get_token.assert_called_once_with(SCOPE) + + assert result == TOKEN diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..08494b7 --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,75 @@ +from unittest.mock import MagicMock, patch + +import pytest +from meltano.edk.models import Describe, ExtensionCommand +from requests import RequestException + +from powerbi_ext.extension import BASE_URL, TIMEOUT, PowerBIExtension + +TOKEN = "token" +WORKSPACE_ID = "workspace_id" +DATASET_ID = "dataset_id" + + +@patch("powerbi_ext.extension.get_token", return_value=TOKEN) +def test_init_not_token(mock_get_token: MagicMock): + ext = PowerBIExtension() + mock_get_token.assert_called_once() + assert ext.log + assert ext.headers == {"Authorization": f"Bearer {TOKEN}"} + + +class TestExtension: + ext = PowerBIExtension(token=TOKEN) + + def test_invoke(self): + with pytest.raises(NotImplementedError): + self.ext.invoke() + + @patch("requests.post") + def test_refresh_ok(self, mock_post: MagicMock): + mock_res = MagicMock(status_code=200, headers={"RequestId": "RequestId"}) + url = BASE_URL + f"/groups/{WORKSPACE_ID}/datasets/{DATASET_ID}" + "/refreshes" + body = { + "notifyOption": "MailOnCompletion", + } + mock_post.return_value = mock_res + res = self.ext.refresh(workspace_id=WORKSPACE_ID, dataset_id=DATASET_ID) + mock_post.assert_called_once_with( + url, json=body, headers=self.ext.headers, timeout=TIMEOUT + ) + assert res == "RequestId" + + @patch("requests.post") + def test_refresh_not_ok(self, mock_post: MagicMock): + mock_res = MagicMock(status_code=202) + url = BASE_URL + f"/groups/{WORKSPACE_ID}/datasets/{DATASET_ID}" + "/refreshes" + body = { + "notifyOption": "MailOnCompletion", + } + mock_post.return_value = mock_res + with pytest.raises(RequestException): + self.ext.refresh(workspace_id=WORKSPACE_ID, dataset_id=DATASET_ID) + + mock_post.assert_called_once_with( + url, json=body, headers=self.ext.headers, timeout=TIMEOUT + ) + + @patch.object(ExtensionCommand, "__new__") + @patch.object(Describe, "__new__") + def test_describe( + self, + mock_describe_class: MagicMock, + mock_command_class: MagicMock, + ): + name, description = "powerbi_extension", "extension commands" + mock_command = MagicMock(name=name, description=description) + mock_command_class.return_value = mock_command + + self.ext.describe() + + mock_command_class.assert_called_once_with( + ExtensionCommand, name=name, description=description + ) + + mock_describe_class.assert_called_once_with(Describe, commands=[mock_command]) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..7b9c7e1 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,57 @@ +from unittest.mock import MagicMock, patch + +from typer.testing import CliRunner + +from powerbi_ext.main import PowerBIExtension, app + +runner = CliRunner() + +WORKSPACE_ID = "workspace_id" +DATASET_ID = "dataset_id" + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "describe" in result.stdout + assert "refresh" in result.stdout + + +@patch.object(PowerBIExtension, "__new__") +def test_refresh_workspace_dataset_ok(mock_ext_class: MagicMock): + mock_ext_refresh = MagicMock() + mock_ext = MagicMock(refresh=mock_ext_refresh) + mock_ext_class.return_value = mock_ext + + result = runner.invoke(app, ["refresh", "-w", WORKSPACE_ID, DATASET_ID]) + + mock_ext_class.assert_called_once() + mock_ext_refresh.assert_called_once_with(WORKSPACE_ID, DATASET_ID) + assert result.exit_code == 0 + + +@patch.object(PowerBIExtension, "__new__") +def test_refresh_dataset_ok(mock_ext_class: MagicMock): + mock_ext_refresh = MagicMock() + mock_ext = MagicMock(refresh=mock_ext_refresh) + mock_ext_class.return_value = mock_ext + + result = runner.invoke(app, ["refresh", DATASET_ID]) + + mock_ext_class.assert_called_once() + mock_ext_refresh.assert_called_once_with(None, DATASET_ID) + assert result.exit_code == 0 + + +@patch.object(PowerBIExtension, "__new__") +def test_refresh_error(mock_ext: MagicMock): + result = runner.invoke(app, ["refresh"]) + mock_ext.assert_not_called() + assert result.exit_code == 2 + + +@patch.object(PowerBIExtension, "__new__") +def test_describe(mock_ext: MagicMock): + result = runner.invoke(app, ["describe"]) + assert result.exit_code == 0 + mock_ext.assert_called_once()