diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..acb3e93 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,8 @@ +# Logging and output + +We use the logging facility for both debugging and output to the user. If you +want to output a normal message (replacement of `print()`) use `lg.warn()`. The +name might feel counter-intuitive, but I don't want to hack the logging-system +and add new levels. `INFO` is basically reseved for successful commands. +Unsuccessful commands are logged on `ERROR`. You can also hide unsuccessful command +using `hide_error=True` in `run_command`. diff --git a/Makefile b/Makefile index 3c1f245..69438b9 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ wait-for-ready: up ## wait for web-server to be ready for testing .PHONY: poetry-install poetry-install: wait-for-ready ## install dev environment - @docker compose exec testing poetry install + @docker compose exec testing poetry install --without lsp .PHONY: mypy mypy: poetry-install @@ -48,24 +48,17 @@ mypy: poetry-install pytest: poetry-install ## run pytest @docker compose exec testing poetry run pytest -vv --cov -.PHONY: check-isort -check-isort: poetry-install ## check isort - @docker compose exec testing poetry run isort --check pyaptly +.PHONY: format +format: poetry-install ## format code with ruff + @docker compose exec testing poetry run ruff format pyaptly -.PHONY: check-black -check-black: poetry-install ## check black - @docker compose exec testing poetry run black --check pyaptly - -.PHONY: check-black -format-black: poetry-install ## format code with black - @docker compose exec testing poetry run black pyaptly - -.PHONY: flake8 -flake8: poetry-install ## run flake8 - @docker compose exec testing poetry run flake8 pyaptly +.PHONY: fix +fix: poetry-install ## fix code with ruff + @docker compose exec testing poetry run ruff check --fix pyaptly .PHONY: lint-code -lint-code: check-isort check-black flake8 ## check all linters +lint-code: ## check all linters + @docker compose exec testing poetry run ruff check pyaptly .PHONY: test test: pytest mypy lint-code ## run all testing @@ -82,9 +75,9 @@ entr-pytest: poetry-install ## run pytest with entr entr-mypy: poetry-install ## run pytest with entr @docker compose exec testing bash -c "find -name '*.py' | SHELL=bash poetry run entr bash -c 'make local-mypy; echo ---'" -.PHONY: entr-flake8 -entr-flake8: poetry-install ## run flake8 with entr - @docker compose exec testing bash -c "find -name '*.py' | SHELL=bash poetry run entr bash -c 'flake8 pyaptly; echo ---'" +.PHONY: entr-lint +entr-lint: poetry-install ## run ruff with entr + @docker compose exec testing bash -c "find -name '*.py' | SHELL=bash poetry run entr bash -c 'ruff check pyaptly; echo ---'" .PHONY: local-mypy local-mypy: ## Run mypy as daemon locally (requires local-dev) diff --git a/README.md b/README.md index 6b66302..dfc26cc 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,75 @@ # Pyaptly -Automates the creation and managment of aptly mirrors and snapshots based on yml +Automates the creation and managment of aptly mirrors and snapshots based on toml input files. **Important**: Corrently under heavy development: - For for the old version [switch to the master branch](https://github.com/adfinis/pyaptly/tree/master) - Main branch builds contain [alpha packages](https://github.com/adfinis/pyaptly/actions/runs/8147002919), see Artifacts + +## Example commands + +Initialize a new aptly server. + +```shell +pyaptly -c mirrors.toml mirror create +pyaptly -c mirrors.toml mirror update +pyaptly -c mirrors.toml snapshot create +pyaptly -c mirrors.toml publish create +``` + +Update mirrors and snapshots and switch publish endpoints with +```automatic-update: true``` to the new snapshots. + +```shell +pyaptly -c mirrors.toml mirror update +pyaptly -c mirrors.toml snapshot create +pyaptly -c mirrors.toml publish create +pyaptly -c mirrors.toml publish update +``` + +Manually trigger a switch to the new snapshots for the publish endpoint +ubuntu/stable. + +```shell +pyaptly -c mirrors.toml publish update ubuntu/stable +``` + +## Debugging + +The most interesting mode for users is not `--debug` but `--info` which shows +all commands executed. + +```bash +> pyaptly legacy -- --info --config pyaptly/tests/repo.toml repo create +Command call + cmd: gpg --no-default-keyring --keyring trustedkeys.gpg --list-keys --with-colons -> 0 + stdout: 'tru::1:1709575833:0:3:1:5 + pub:-:255:22:2841988729C7F3FF:1701882080:::-:::scESC:::::ed25519:::0: + fpr:::::::::6380C07FF6496016E01CF4522841988729C7F3FF: + uid:-::::1701882080::5BBE9C7E7AA5EEE3538F66274125D69FA727FD1E::Pyaptly Test 01 ::::::::::0: + sub:-:255:18:0A1CBEF26FE4F36E:1701882080::::::e:::::cv25519:: + fpr:::::::::9EE64E40A5E3530D3E18A97C0A1CBEF26FE4F36E: + pub:-:255:22:EC54D33E5B5EBE98:1701882297:::-:::scESC:::::ed25519:::0: + fpr:::::::::660D45228AB6B59CCE48AFB3EC54D33E5B5EBE98: + uid:-::::1701882297::F3EF71B78669C0FC259A4078151BDC5815A6015D::Pyaptly Test 02 ::::::::::0: + sub:-:255:18:042FE0F5BB743B60:1701882297::::::e:::::cv25519:: + fpr:::::::::AE58B62134E02AF8E5D55FF4042FE0F5BB743B60:' +Command call + cmd: aptly repo list -raw -> 0 + stderr: 'Config file not found, creating default config at /root/.aptly.conf' +Command call + cmd: aptly mirror list -raw -> 0 +Command call + cmd: aptly snapshot list -raw -> 0 +Command call + cmd: aptly publish list -raw -> 0 +Command call + cmd: aptly repo -architectures=amd64,i386 -distribution=stable -component=main create centrify -> 0 + stdout: 'Local repo [centrify] successfully added. + You can run 'aptly repo add centrify ...' to add packages to repository.' +``` + +Commands that fail are always displayed in red on a tty, but that actually only +happens if something is broken. diff --git a/README.rst b/README.rst deleted file mode 100644 index 55a58d2..0000000 --- a/README.rst +++ /dev/null @@ -1,112 +0,0 @@ -======= -Pyaptly -======= - -Automates the creation and managment of aptly mirrors and snapshots based on toml -input files. - -|pypi| |travis| |coverage| [1]_ - -.. |pypi| image:: https://badge.fury.io/py/Pyaptly.svg - :target: https://badge.fury.io/py/Pyaptly -.. |travis| image:: https://travis-ci.org/adfinis-sygroup/pyaptly.png?branch=master - :target: https://travis-ci.org/adfinis-sygroup/pyaptly -.. |coverage| image:: https://img.shields.io/badge/coverage-100%25-brightgreen.svg - -`Read the Docs`_ - -.. _`Read the Docs`: https://docs.adfinis-sygroup.ch/public/pyaptly/ - -.. [1] Coverage enforced by tests (on travis) - -Example commands ----------------- - -Initialize a new aptly server. - -.. code:: shell - - pyaptly -c mirrors.yml mirror create - pyaptly -c mirrors.yml mirror update - pyaptly -c mirrors.yml snapshot create - pyaptly -c mirrors.yml publish create - -Update mirrors and snapshots and switch publish endpoints with -``automatic-update: true`` to the new snapshots. - -.. code:: shell - - pyaptly -c mirrors.yml mirror update - pyaptly -c mirrors.yml snapshot create - pyaptly -c mirrors.yml publish create - pyaptly -c mirrors.yml publish update - -Manually trigger a switch to the new snapshots for the publish endpoint -ubuntu/stable. - -.. code:: - - pyaptly -c mirrors.yml publish update ubuntu/stable - -Install Debian/Ubuntu -===================== - -Sources: - -.. code-block:: text - - deb http://aptly.adfinis-sygroup.ch/adsy-public/debian wheezy main - - deb http://aptly.adfinis-sygroup.ch/adsy-public/debian jessie main - - deb http://aptly.adfinis-sygroup.ch/adsy-public/ubuntu trusty main - - deb http://aptly.adfinis-sygroup.ch/adsy-public/ubuntu vivid main - - deb http://aptly.adfinis-sygroup.ch/adsy-public/ubuntu xenial main - -Install: - -.. code-block:: bash - - wget -O - http://aptly.adfinis-sygroup.ch/aptly.asc | apt-key add - - apt-get update - apt-get install python-pyaptly - -Testing -------- - -Automatic - -.. code:: shell - - git submodule update --init --recursive - make test-local - -Manual. There is a safety check in tests. They won't work if you don't set -$HOME. - -.. code:: shell - - git submodule update --init --recursive - source testenv - py.test -x - -or - -.. code:: shell - - git submodule update --init --recursive - export HOME="$(pwd)" - export PATH="$HOME/aptly_0.9.6_linux_amd64/:$PATH" - py.test -x - -Vagrant Box ------------ - -The box provisions aptly, nginx and two repos which can be used for tests: - -.. code:: - - aptly mirror create mirro-fakerepo01 http://localhost/fakerepo01 main - aptly mirror create mirro-fakerepo02 http://localhost/fakerepo02 main diff --git a/poetry.lock b/poetry.lock index c39967d..aceac93 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "attrs" @@ -20,48 +20,27 @@ tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] -name = "black" -version = "23.12.1" -description = "The uncompromising code formatter." +name = "cattrs" +version = "23.2.3" +description = "Composable complex class support for attrs and dataclasses." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, ] [package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" +attrs = ">=23.1.0" [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] [[package]] name = "click" @@ -178,133 +157,6 @@ files = [ pyreadline = {version = "*", markers = "platform_system == \"Windows\""} pyrepl = ">=0.8.2" -[[package]] -name = "flake8" -version = "6.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, - {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.1.0,<3.2.0" - -[[package]] -name = "flake8-bugbear" -version = "23.12.2" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-bugbear-23.12.2.tar.gz", hash = "sha256:32b2903e22331ae04885dae25756a32a8c666c85142e933f43512a70f342052a"}, - {file = "flake8_bugbear-23.12.2-py3-none-any.whl", hash = "sha256:83324bad4d90fee4bf64dd69c61aff94debf8073fbd807c8b6a36eec7a2f0719"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=6.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] - -[[package]] -name = "flake8-debugger" -version = "4.1.2" -description = "ipdb/pdb statement checker plugin for flake8" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, - {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, -] - -[package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" - -[[package]] -name = "flake8-docstrings" -version = "1.7.0" -description = "Extension for flake8 which uses pydocstyle to check docstrings" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, - {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, -] - -[package.dependencies] -flake8 = ">=3" -pydocstyle = ">=2.1" - -[[package]] -name = "flake8-isort" -version = "6.1.1" -description = "flake8 plugin that integrates isort" -optional = false -python-versions = ">=3.8" -files = [ - {file = "flake8_isort-6.1.1-py3-none-any.whl", hash = "sha256:0fec4dc3a15aefbdbe4012e51d5531a2eb5fa8b981cdfbc882296a59b54ede12"}, - {file = "flake8_isort-6.1.1.tar.gz", hash = "sha256:c1f82f3cf06a80c13e1d09bfae460e9666255d5c780b859f19f8318d420370b3"}, -] - -[package.dependencies] -flake8 = "*" -isort = ">=5.0.0,<6" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "flake8-pyproject" -version = "1.2.3" -description = "Flake8 plug-in loading the configuration from pyproject.toml" -optional = false -python-versions = ">= 3.6" -files = [ - {file = "flake8_pyproject-1.2.3-py3-none-any.whl", hash = "sha256:6249fe53545205af5e76837644dc80b4c10037e73a0e5db87ff562d75fb5bd4a"}, -] - -[package.dependencies] -Flake8 = ">=5" - -[package.extras] -dev = ["pyTest", "pyTest-cov"] - -[[package]] -name = "flake8-string-format" -version = "0.3.0" -description = "string format checker, plugin for flake8" -optional = false -python-versions = "*" -files = [ - {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, - {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, -] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "flake8-tuple" -version = "0.4.1" -description = "Check code for 1 element tuple." -optional = false -python-versions = "*" -files = [ - {file = "flake8_tuple-0.4.1-py2.py3-none-any.whl", hash = "sha256:d828cc8e461c50cacca116e9abb0c9e3be565e8451d3f5c00578c63670aae680"}, - {file = "flake8_tuple-0.4.1.tar.gz", hash = "sha256:8a1b42aab134ef4c3fef13c6a8f383363f158b19fbc165bd91aed9c51851a61d"}, -] - -[package.dependencies] -flake8 = "*" -six = "*" - [[package]] name = "freezegun" version = "1.4.0" @@ -366,13 +218,13 @@ files = [ [[package]] name = "hypothesis" -version = "6.98.15" +version = "6.98.17" description = "A library for property-based testing" optional = false python-versions = ">=3.8" files = [ - {file = "hypothesis-6.98.15-py3-none-any.whl", hash = "sha256:5b40fd81fce9e0b35f0a47e10eb41f375a6b9e8551d0e1084c83b8b0d0d1bb6b"}, - {file = "hypothesis-6.98.15.tar.gz", hash = "sha256:1e31210951511b24ce8b3b6e04d791c466385a30ac3af571bf2223954b025d77"}, + {file = "hypothesis-6.98.17-py3-none-any.whl", hash = "sha256:313f64b9f9f95e12c8b5342466bef7f352d2608afeeb434817c039602b45f0c4"}, + {file = "hypothesis-6.98.17.tar.gz", hash = "sha256:bbd227000cc21a9686a00867f031479c3812d8ab076e4af1c813f6b3a50c98f5"}, ] [package.dependencies] @@ -406,20 +258,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jedi" version = "0.19.1" @@ -456,6 +294,21 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "lsprotocol" +version = "2023.0.1" +description = "Python implementation of the Language Server Protocol." +optional = false +python-versions = ">=3.7" +files = [ + {file = "lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2"}, + {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"}, +] + +[package.dependencies] +attrs = ">=21.3.0" +cattrs = "!=23.2.1" + [[package]] name = "markupsafe" version = "2.1.5" @@ -525,17 +378,6 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mock" version = "5.1.0" @@ -635,17 +477,6 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pdbpp" version = "0.10.3" @@ -666,21 +497,6 @@ wmctrl = "*" funcsigs = ["funcsigs"] testing = ["funcsigs", "pytest"] -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - [[package]] name = "pluggy" version = "1.4.0" @@ -696,45 +512,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - -[[package]] -name = "pyflakes" -version = "3.1.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, - {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, -] - [[package]] name = "pygments" version = "2.17.2" @@ -871,56 +648,41 @@ files = [ six = ">=1.5" [[package]] -name = "python-lsp-black" -version = "1.3.0" -description = "Black plugin for the Python LSP Server" -optional = false -python-versions = ">=3.7" -files = [ - {file = "python-lsp-black-1.3.0.tar.gz", hash = "sha256:5aa257e9e7b7e5a2316ef2a9fbcd242e82e0f695bf1622e31c0bf5cd69e6113f"}, - {file = "python_lsp_black-1.3.0-py3-none-any.whl", hash = "sha256:5f583b4395d8d048885974095088ab81e36e501de369cc49a621a82473bb9070"}, -] - -[package.dependencies] -black = ">=22.3.0" -python-lsp-server = ">=1.4.0" - -[package.extras] -dev = ["flake8", "isort (>=5.0)", "mypy", "pre-commit", "pytest", "types-pkg-resources", "types-setuptools"] - -[[package]] -name = "python-lsp-isort" -version = "0.1" -description = "isort plugin for the Python LSP Server" +name = "python-lsp-jsonrpc" +version = "1.1.2" +description = "JSON RPC 2.0 server library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "python-lsp-isort-0.1.tar.gz", hash = "sha256:f02948bc8e7549905032100e772f03464f7548afa96f07d744ff1f93cc58339a"}, + {file = "python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912"}, + {file = "python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c"}, ] [package.dependencies] -isort = ">=5.0" -python-lsp-server = "*" +ujson = ">=3.0.0" [package.extras] -dev = ["pytest"] +test = ["coverage", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-cov"] [[package]] -name = "python-lsp-jsonrpc" -version = "1.1.2" -description = "JSON RPC 2.0 server library" +name = "python-lsp-ruff" +version = "2.2.0" +description = "Ruff linting plugin for pylsp" optional = false python-versions = ">=3.8" files = [ - {file = "python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912"}, - {file = "python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c"}, + {file = "python-lsp-ruff-2.2.0.tar.gz", hash = "sha256:67c14067f76bc3d16bd5473a574e0d7b3bd422d723b62d2b2a83356e8af051db"}, + {file = "python_lsp_ruff-2.2.0-py3-none-any.whl", hash = "sha256:fe0487c7997b38a59862c44e19cb815e24bb1b2f7cef4f1f32c61cf623a5ce94"}, ] [package.dependencies] -ujson = ">=3.0.0" +cattrs = "!=23.2.1" +lsprotocol = ">=2023.0.1" +python-lsp-server = "*" +ruff = ">=0.2.0" [package.extras] -test = ["coverage", "pycodestyle", "pyflakes", "pylint", "pytest", "pytest-cov"] +dev = ["pre-commit", "pytest"] [[package]] name = "python-lsp-server" @@ -1014,6 +776,32 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "ruff" +version = "0.3.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, + {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, + {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, + {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, + {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, +] + [[package]] name = "setuptools" version = "69.1.1" @@ -1041,17 +829,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1101,6 +878,17 @@ files = [ {file = "tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9"}, ] +[[package]] +name = "types-colorama" +version = "0.4.15.20240311" +description = "Typing stubs for colorama" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a"}, + {file = "types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e"}, +] + [[package]] name = "types-pyyaml" version = "6.0.12.12" @@ -1228,4 +1016,4 @@ test = ["pytest"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "4d57210cb742dc4897ab952e868dd82e79ade4ddff1bb13a9dbe50fef19c0989" +content-hash = "417dd251bc86a11a5f6aab54b8a1d7334edafa59886120efc23a601eeadb1955" diff --git a/pyaptly/cli.py b/pyaptly/cli.py index 4e3068e..2877677 100644 --- a/pyaptly/cli.py +++ b/pyaptly/cli.py @@ -1,4 +1,5 @@ """python-click based command line interface for pyaptly.""" + import sys from pathlib import Path diff --git a/pyaptly/command.py b/pyaptly/command.py index 12bbfc2..d187ff3 100644 --- a/pyaptly/command.py +++ b/pyaptly/command.py @@ -1,4 +1,5 @@ """Commands with dependencies.""" + import collections import logging @@ -291,7 +292,7 @@ def order_commands(commands, has_dependency_cb=lambda x: False): incoming_set = set([cmd for cmd in commands]) assert incoming_set == scheduled_set - lg.info("Reordered commands: %s", [str(cmd) for cmd in scheduled]) + lg.debug("Reordered commands: %s", [str(cmd) for cmd in scheduled]) return scheduled diff --git a/pyaptly/config_file.py b/pyaptly/config_file.py index 0104164..aeef53d 100644 --- a/pyaptly/config_file.py +++ b/pyaptly/config_file.py @@ -1,4 +1,5 @@ """Handling pyaptly config-files.""" + from pathlib import Path import tomli_w diff --git a/pyaptly/custom_logger.py b/pyaptly/custom_logger.py new file mode 100644 index 0000000..de635b7 --- /dev/null +++ b/pyaptly/custom_logger.py @@ -0,0 +1,37 @@ +import logging + +from colorama import Fore, Style + +from . import util + + +class CustomFormatter(logging.Formatter): + debug = "%(levelname)s - %(filename)s:%(lineno)d: %(message)s" + info_warn = "%(message)s" + error_plus = "%(levelname)s: %(message)s" + + FORMATS_COLOR = { + logging.DEBUG: Style.DIM + debug + Style.RESET_ALL, + logging.INFO: Fore.YELLOW + info_warn + Style.RESET_ALL, + logging.WARNING: info_warn, + logging.ERROR: Fore.RED + error_plus + Style.RESET_ALL, + logging.CRITICAL: Fore.MAGENTA + error_plus + Style.RESET_ALL, + } + + FORMATS = { + logging.DEBUG: debug, + logging.INFO: info_warn, + logging.WARNING: info_warn, + logging.ERROR: error_plus, + logging.CRITICAL: error_plus, + } + + def format(self, record): + if util.isatty(): + formats = self.FORMATS_COLOR # pragma: no cover + else: + formats = self.FORMATS + + log_fmt = formats.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) diff --git a/pyaptly/date_tools.py b/pyaptly/date_tools.py index ba1845a..c7374de 100644 --- a/pyaptly/date_tools.py +++ b/pyaptly/date_tools.py @@ -1,4 +1,5 @@ """Tools to convert and round dates.""" + import datetime diff --git a/pyaptly/main.py b/pyaptly/main.py index f1319cf..853a3c7 100755 --- a/pyaptly/main.py +++ b/pyaptly/main.py @@ -1,11 +1,20 @@ """Aptly mirror/snapshot managment automation.""" + import argparse import logging import sys import tomli -from . import command, mirror, publish, repo, snapshot, state_reader +from . import ( + command, + custom_logger, + mirror, + publish, + repo, + snapshot, + state_reader, +) _logging_setup = False @@ -36,6 +45,12 @@ def main(argv=None): help="Enable debug output", action="store_true", ) + parser.add_argument( + "--info", + "-i", + help="Enable info output (show executed commands)", + action="store_true", + ) parser.add_argument( "--pretend", "-p", @@ -64,14 +79,16 @@ def main(argv=None): args = parser.parse_args(argv) root = logging.getLogger() - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) + formatter = custom_logger.CustomFormatter() if not _logging_setup: # noqa handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) root.addHandler(handler) - handler.setLevel(logging.CRITICAL) + root.setLevel(logging.WARNING) + handler.setLevel(logging.WARNING) + if args.info: + root.setLevel(logging.INFO) + handler.setLevel(logging.INFO) if args.debug: root.setLevel(logging.DEBUG) handler.setLevel(logging.DEBUG) diff --git a/pyaptly/mirror.py b/pyaptly/mirror.py index 5bdb49b..3cfc99e 100644 --- a/pyaptly/mirror.py +++ b/pyaptly/mirror.py @@ -1,4 +1,5 @@ """Create and update mirrors in aptly.""" + import logging from . import state_reader, util diff --git a/pyaptly/publish.py b/pyaptly/publish.py index 25c0acc..ba2a3d5 100644 --- a/pyaptly/publish.py +++ b/pyaptly/publish.py @@ -1,4 +1,5 @@ """Publish snapshots in aptly.""" + import datetime import logging import re diff --git a/pyaptly/repo.py b/pyaptly/repo.py index b04f430..91fd851 100644 --- a/pyaptly/repo.py +++ b/pyaptly/repo.py @@ -1,4 +1,5 @@ """Create repos in aptly.""" + import logging from . import command, state_reader, util diff --git a/pyaptly/snapshot.py b/pyaptly/snapshot.py index 099b217..e2259ef 100644 --- a/pyaptly/snapshot.py +++ b/pyaptly/snapshot.py @@ -1,4 +1,5 @@ """Create and update snapshots in aptly.""" + import datetime import logging from typing import Optional diff --git a/pyaptly/state_reader.py b/pyaptly/state_reader.py index 6d55690..779ae0f 100644 --- a/pyaptly/state_reader.py +++ b/pyaptly/state_reader.py @@ -1,4 +1,5 @@ """The state reader helps to find the delta between current and target state.""" + import logging import re diff --git a/pyaptly/tests/test_graph.py b/pyaptly/tests/test_graph.py index 6d50cad..8fb4d7e 100644 --- a/pyaptly/tests/test_graph.py +++ b/pyaptly/tests/test_graph.py @@ -1,4 +1,5 @@ """Testing dependency graphs.""" + import random from functools import partial from typing import Union diff --git a/pyaptly/tests/test_helpers.py b/pyaptly/tests/test_helpers.py index 4ae0e0f..f62ce62 100644 --- a/pyaptly/tests/test_helpers.py +++ b/pyaptly/tests/test_helpers.py @@ -1,4 +1,5 @@ """Testing testing helper functions.""" + from .. import command, state_reader diff --git a/pyaptly/tests/test_mirror.py b/pyaptly/tests/test_mirror.py index b0bf5f6..22efed4 100644 --- a/pyaptly/tests/test_mirror.py +++ b/pyaptly/tests/test_mirror.py @@ -1,4 +1,5 @@ """Test mirror functionality.""" + import logging import pytest @@ -7,17 +8,25 @@ @pytest.mark.parametrize("config", ["debug.toml"], indirect=True) -def test_debug(environment, config): +@pytest.mark.parametrize("kind", ["debug", "info"]) +def test_debug(environment, config, kind): """Test if debug is enabled with -d.""" + if kind == "debug": + arg = "-d" + expect = logging.DEBUG + else: + arg = "-i" + expect = logging.INFO + main._logging_setup = False # revert logging setup by environment fixture args = [ - "-d", + arg, "-c", config, "mirror", "create", ] main.main(args) - assert logging.getLogger().level == logging.DEBUG + assert logging.getLogger().level == expect @pytest.mark.parametrize("config", ["mirror-extra.toml"], indirect=True) diff --git a/pyaptly/tests/test_publish.py b/pyaptly/tests/test_publish.py index af15561..06bd410 100644 --- a/pyaptly/tests/test_publish.py +++ b/pyaptly/tests/test_publish.py @@ -1,4 +1,5 @@ """Test publish functionality.""" + import pytest from .. import command, main, state_reader diff --git a/pyaptly/tests/test_snapshot.py b/pyaptly/tests/test_snapshot.py index 62f5bf5..a97fbb5 100644 --- a/pyaptly/tests/test_snapshot.py +++ b/pyaptly/tests/test_snapshot.py @@ -1,4 +1,5 @@ """Test snapshot functionality.""" + import pytest from .. import main, state_reader, util diff --git a/pyaptly/tests/test_util.py b/pyaptly/tests/test_util.py index f6f8089..4bf1c5f 100644 --- a/pyaptly/tests/test_util.py +++ b/pyaptly/tests/test_util.py @@ -1,14 +1,13 @@ """Test the util.py module.""" + from datetime import datetime import pytest from .. import snapshot, util -EXPECT = """ -stdout: 'first - second' -""".strip() +EXPECT = """stdout: 'first + second'""" @pytest.mark.parametrize("decode", [True, False]) @@ -29,7 +28,7 @@ def test_run(test_path, debug_mode, caplog, decode, unicode_error): caplog.clear() util.run_command(["sh", "-c", "printf error 1>&2; false"], decode=decode) assert "stderr: 'error'" in caplog.messages[0] - assert "returncode: 1" in caplog.messages[0] + assert "-> 1" in caplog.messages[0] caplog.clear() util.run_command(["sh", "-c", "printf 'first\nsecond'"], decode=decode) assert EXPECT in caplog.messages[0] diff --git a/pyaptly/util.py b/pyaptly/util.py index 64a041a..696bd7c 100644 --- a/pyaptly/util.py +++ b/pyaptly/util.py @@ -1,9 +1,12 @@ """Basic function like running processes and logging.""" import logging +import os import subprocess from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError # noqa: F401 + +from colorama import Fore, init +from subprocess import PIPE, CalledProcessError # noqa: F401 from typing import Optional, Sequence _DEFAULT_KEYSERVER: str = "hkps://keys.openpgp.org" @@ -14,20 +17,30 @@ RESULT_LOG = """ Command call -args: {args} -returncode: {returncode} -stdout: '{stdout}' -stderr: '{stderr}' + cmd: {cmd} {color_begin}-> {returncode}{color_end} """.strip() -_indent = " " * 13 +OUTPUT_LOG = " {out_type}: '{output}'" +_indent = " " * 15 + +_isatty_cache: bool | None = None + + +lg = logging.getLogger(__name__) -logger = logging.getLogger(__name__) + +def isatty(): + global _isatty_cache + if _isatty_cache is None: + _isatty_cache = os.isatty(1) + if _isatty_cache: + init() # pragma: no cover + return _isatty_cache def unit_or_list_to_list(thingy): - """Ensure that a yml entry is always a list. + """Ensure that a toml entry is always a list. - Used to allow lists and single units in the yml file. + Used to allow lists and single units in the toml file. :param thingy: The data to ensure it is a list :type thingy: list, tuple or other @@ -46,35 +59,42 @@ def get_default_keyserver(): return _DEFAULT_KEYSERVER -def is_debug_mode(): - """Check if we are in debug mode.""" - return _DEBUG or _PYTEST_DEBUG - - -def run_command(cmd_args: Sequence[str | Path], *, decode: bool = True, **kwargs): +def run_command( + cmd_args: Sequence[str | Path], + *, + decode: bool = True, + hide_error: bool = False, + **kwargs, +): """Instrumented subprocess.run for easier debugging. - By default this run command will add `encoding="UTF-8"` to kwargs. Disable - with `decode=False`. + - By default this run command will add `encoding="UTF-8"` to kwargs. Disable + with `decode=False`. + - Command that often or normally fail can also set `hide_error=True` to only + show them in if the loglevel is `INFO` (Logging and output in DEVELOPMENT.md) """ - debug = is_debug_mode() added_stdout = False added_stderr = False - if debug: - if "stdout" not in kwargs: - kwargs["stdout"] = PIPE - added_stdout = True - if "stderr" not in kwargs: - kwargs["stderr"] = PIPE - added_stderr = True + # TODO assert PIPE or None + if "stdout" not in kwargs: + kwargs["stdout"] = PIPE + added_stdout = True + if "stderr" not in kwargs: + kwargs["stderr"] = PIPE + added_stderr = True result = None if decode and "encoding" not in kwargs: kwargs["encoding"] = "UTF-8" try: result = subprocess.run(cmd_args, **kwargs) finally: - if debug and result: - log_run_result(result) + if result: + log_msg = format_run_result(result, result.returncode) + if result.returncode == 0: + lg.info(log_msg) + else: + if not hide_error or lg.root.level <= 20: + lg.error(log_msg) # Do not change returned result by debug mode if added_stdout: delattr(result, "stdout") @@ -112,15 +132,34 @@ def indent_out(output: bytes | str) -> str: return "\n".join(result) -def log_run_result(result: subprocess.CompletedProcess): - """Log a CompletedProcess result log debug.""" - msg = RESULT_LOG.format( - args=result.args, - returncode=result.returncode, - stdout=indent_out(result.stdout), - stderr=indent_out(result.stderr), - ) - logger.debug(msg) +def format_run_result(result: subprocess.CompletedProcess, returncode: int): + """Format a CompletedProcess result log.""" + color_begin = "" + color_end = "" + if isatty(): # pragma: no cover + if returncode == 0: + color_begin = Fore.RED + color_end = Fore.YELLOW + else: + color_begin = Fore.YELLOW + color_end = Fore.RED + msg = [ + RESULT_LOG.format( + cmd=" ".join([str(x) for x in result.args]), + returncode=result.returncode, + color_begin=color_begin, + color_end=color_end, + stdout=indent_out(result.stdout), + stderr=indent_out(result.stderr), + ) + ] + for out_type, output in [("stdout", result.stdout), ("stderr", result.stderr)]: + output = output.strip() + if output: + output = indent_out(output) + msg.append(OUTPUT_LOG.format(out_type=out_type, output=output)) + pass + return "\n".join(msg) def parse_aptly_show_command(show: str) -> dict[str, str]: diff --git a/pyproject.toml b/pyproject.toml index 1bbf30c..f9835a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ pyyaml = "^6.0.1" click = "^8.0.0" tomli = "^2.0.1" tomli-w = "^1.0.0" +colorama = "^0.4.6" frozendict = "^2.2.0" [tool.poetry.group.dev.dependencies] @@ -61,26 +62,19 @@ mock = "^5.1.0" pytest = "^7.4.3" mypy = "^1.7.1" pdbpp = "^0.10.3" -black = "^23.11.0" -isort = "^5.12.0" -python-lsp-server = "^1.9.0" -python-lsp-black = "^1.3.0" -python-lsp-isort = "^0.1" - -flake8 = "^6.1.0" -flake8-bugbear = "^23.12.2" -flake8-debugger = "^4.1.2" -flake8-isort = "^6.1.1" -flake8-docstrings = "^1.7.0" -flake8-string-format = "^0.3.0" -flake8-tuple = "^0.4.1" -flake8-pyproject = "^1.2.3" types-toml = "^0.10.8.7" types-pyyaml = "^6.0.12.12" pytest-coverage = "^0.0" pyp2rpm = "3.3.10" +types-colorama = "^0.4.15.20240311" +ruff = "^0.3.2" + +[tool.poetry.group.lsp.dependencies] +python-lsp-server = "^1.9.0" +python-lsp-ruff = "^2.2.0" + [build-system] requires = ["poetry-core"]