diff --git a/.config/requirements-test.txt b/.config/requirements-test.txt new file mode 100644 index 0000000..eb4a25d --- /dev/null +++ b/.config/requirements-test.txt @@ -0,0 +1,4 @@ +coverage +pytest +pytest-plus +pytest-xdist diff --git a/.config/requirements.in b/.config/requirements.in new file mode 100644 index 0000000..0f128a2 --- /dev/null +++ b/.config/requirements.in @@ -0,0 +1 @@ +ansible-core>=2.12.0 diff --git a/.config/requirements.txt b/.config/requirements.txt new file mode 100644 index 0000000..278b52b --- /dev/null +++ b/.config/requirements.txt @@ -0,0 +1,27 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --extra=test --no-annotate --output-file=.config/requirements.txt --resolver=backtracking --strip-extras --unsafe-package=ansible-core pyproject.toml +# +attrs==22.2.0 +cffi==1.15.1 +coverage==7.0.5 +cryptography==39.0.0 +exceptiongroup==1.1.0 +execnet==1.9.0 +iniconfig==2.0.0 +jinja2==3.1.2 +markupsafe==2.1.1 +packaging==23.0 +pluggy==1.0.0 +pycparser==2.21 +pytest==7.2.1 +pytest-plus==0.4.0 +pytest-xdist==3.1.0 +pyyaml==6.0 +resolvelib==0.8.1 +tomli==2.0.1 + +# The following packages are considered to be unsafe in a requirements file: +# ansible-core diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..9257145 --- /dev/null +++ b/.flake8 @@ -0,0 +1,83 @@ +# spell-checker:ignore testmon tmontmp +[flake8] + +# Don't even try to analyze these: +extend-exclude = + # No need to traverse egg info dir + *.egg-info, + # GitHub configs + .github, + # Cache files of MyPy + .mypy_cache, + # Cache files of pytest + .pytest_cache, + # Temp dir of pytest-testmon + .tmontmp, + # Occasional virtualenv dir + .venv + # VS Code + .vscode, + # Temporary build dir + build, + # This contains sdists and wheels of ansible-lint that we don't want to check + dist, + # Occasional virtualenv dir + env, + # Metadata of `pip wheel` cmd is autogenerated + pip-wheel-metadata, + +# Let's not over-complicate the code: +max-complexity = 10 + +# Accessibility/large fonts and PEP8 friendly: +#max-line-length = 79 +# Accessibility/large fonts and PEP8 unfriendly: +max-line-length = 100 + +# The only allowed ignores are related to black and isort +# https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length +# "H" are generated by hacking plugin, which is not black compatible +extend-ignore = + E203, + E501, + # complexity is also measured by pylint: too-many-branches + C901, + # We use type annotations instead + DAR101, + DAR104, + # https://github.com/terrencepreilly/darglint/issues/165 + DAR301, + # duplicate of pylint W0611 (unused-import) + F401, + # duplicate of pylint E0602 (undefined-variable) + F821, + # duplicate of pylint W0612 (unused-variable) + F841, + H, + +# Allow certain violations in certain files: +per-file-ignores = + # FIXME: D102 Missing docstring in public method + src/ansiblelint/cli.py: D102 + src/ansiblelint/formatters/__init__.py: D102 + src/ansiblelint/rules/*.py: D102 + src/ansiblelint/rules/__init__.py: D102 + + # FIXME: C901 Function is too complex + # FIXME: refactor _defaults_from_yamllint_config using match case + # once python 3.10 is mandatory + # Ref: https://github.com/ansible/ansible-lint/pull/2077 + src/ansiblelint/yaml_utils.py: C901 + + # FIXME: drop these once they're fixed + # Ref: https://github.com/ansible/ansible-lint/issues/725 + test/*: D102 + +# flake8-pytest-style +# PT001: +pytest-fixture-no-parentheses = true +# PT006: +pytest-parametrize-names-type = tuple +# PT007: +pytest-parametrize-values-type = tuple +pytest-parametrize-values-row-type = tuple diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6a4dae2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +--- +version: 2 +updates: + - package-ecosystem: pip + directory: /.config/ + schedule: + day: sunday + interval: weekly + labels: + - dependabot-deps-updates + - skip-changelog + versioning-strategy: lockfile-only + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + labels: + - "dependencies" + - "skip-changelog" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..11fa614 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,3 @@ +--- +# see https://github.com/ansible/devtools +_extends: ansible/devtools diff --git a/.github/workflows/ack.yml b/.github/workflows/ack.yml new file mode 100644 index 0000000..5e7b9f5 --- /dev/null +++ b/.github/workflows/ack.yml @@ -0,0 +1,10 @@ +--- +# See https://github.com/ansible/devtools/blob/main/.github/workflows/ack.yml +name: ack +on: + pull_request_target: + types: [opened, labeled, unlabeled, synchronize] + +jobs: + ack: + uses: ansible/devtools/.github/workflows/ack.yml@main diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..1debf04 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,13 @@ +--- +# See https://github.com/ansible/devtools/blob/main/.github/workflows/push.yml +name: push +"on": + push: + branches: + - main + - "releases/**" + - "stable/**" + +jobs: + ack: + uses: ansible/devtools/.github/workflows/push.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a94d8ec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +--- +name: release + +"on": + release: + types: [published] + +jobs: + pypi: + name: Publish to PyPI registry + environment: release + runs-on: ubuntu-22.04 + + env: + FORCE_COLOR: 1 + PY_COLORS: 1 + TOXENV: pkg + + steps: + - name: Switch to using Python 3.9 by default + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install tox + run: python3 -m pip install --user "tox>=4.0.0" + - name: Check out src from Git + uses: actions/checkout@v3 + with: + fetch-depth: 0 # needed by setuptools-scm + - name: Build dists + run: python -m tox + - name: Publish to pypi.org + if: >- # "create" workflows run separately from "push" & "pull_request" + github.event_name == 'release' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml new file mode 100644 index 0000000..069cedc --- /dev/null +++ b/.github/workflows/tox.yml @@ -0,0 +1,219 @@ +--- +name: tox + +on: + create: # is used for publishing to PyPI and TestPyPI + tags: # any tag regardless of its name, no branches + - "**" + push: # only publishes pushes to the main branch to TestPyPI + branches: # any integration branch but not tag + - "main" + pull_request: + branches: + - "main" + release: + types: + - published # It seems that you can publish directly without creating + schedule: + - cron: 1 0 * * * # Run daily at 0:01 UTC + # Run every Friday at 18:02 UTC + # https://crontab.guru/#2_18_*_*_5 + # - cron: 2 18 * * 5 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + FORCE_COLOR: 1 + PY_COLORS: 1 + +jobs: + pre: + name: pre + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.generate_matrix.outputs.matrix }} + steps: + - name: Determine matrix + id: generate_matrix + uses: coactions/dynamic-matrix@v1 + with: + min_python: "3.9" + max_python: "3.11" + other_names: | + lint + pkg + platforms: linux,macos + + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + needs: pre + defaults: + run: + shell: ${{ matrix.shell || 'bash'}} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.pre.outputs.matrix) }} + # max-parallel: 5 + # The matrix testing goal is to cover the *most likely* environments + # which are expected to be used by users in production. Avoid adding a + # combination unless there are good reasons to test it, like having + # proof that we failed to catch a bug by not running it. Using + # distribution should be preferred instead of custom builds. + env: + # vars safe to be passed to wsl: + WSLENV: FORCE_COLOR:PYTEST_REQPASS:TOXENV:GITHUB_STEP_SUMMARY + # Number of expected test passes, safety measure for accidental skip of + # tests. Update value if you add/remove tests. + PYTEST_REQPASS: 1 + + steps: + - name: Activate WSL1 + if: "contains(matrix.shell, 'wsl')" + uses: Vampire/setup-wsl@v1 + + - name: MacOS workaround for https://github.com/actions/virtual-environments/issues/1187 + if: ${{ matrix.os == 'macOS-latest' }} + run: | + sudo sysctl -w net.link.generic.system.hwcksum_tx=0 + sudo sysctl -w net.link.generic.system.hwcksum_rx=0 + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # needed by setuptools-scm + + - name: Set pre-commit cache + uses: actions/cache@v3 + if: ${{ matrix.passed_name == 'lint' }} + with: + path: | + ~/.cache/pre-commit + key: pre-commit-${{ matrix.name || matrix.passed_name }}-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Set galaxy cache + uses: actions/cache@v3 + if: ${{ startsWith(matrix.passed_name, 'py') }} + with: + path: | + examples/playbooks/collections/*.tar.gz + examples/playbooks/collections/ansible_collections + key: galaxy-${{ hashFiles('examples/playbooks/collections/requirements.yml') }} + + - name: Set up Python ${{ matrix.python_version || '3.9' }} + if: "!contains(matrix.shell, 'wsl')" + uses: actions/setup-python@v4 + with: + cache: pip + python-version: ${{ matrix.python_version || '3.9' }} + + - name: Install tox + run: | + python3 -m pip install --upgrade pip + python3 -m pip install --upgrade "tox>=4.0.0" + + - name: Log installed dists + run: python3 -m pip freeze --all + + - name: Initialize tox envs ${{ matrix.passed_name }} + run: python3 -m tox --notest --skip-missing-interpreters false -vv -e ${{ matrix.passed_name }} + timeout-minutes: 5 # average is under 1, but macos can be over 3 + + # sequential run improves browsing experience (almost no speed impact) + - name: tox -e ${{ matrix.passed_name }} + run: python3 -m tox -e ${{ matrix.passed_name }} + + - name: Combine coverage data + if: ${{ startsWith(matrix.passed_name, 'py') }} + # produce a single .coverage file at repo root + run: tox -e coverage + + - name: Upload coverage data + if: ${{ startsWith(matrix.passed_name, 'py') }} + uses: codecov/codecov-action@v3 + with: + name: ${{ matrix.passed_name }} + fail_ci_if_error: false # see https://github.com/codecov/codecov-action/issues/598 + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true # optional (default = false) + + - name: Archive logs + uses: actions/upload-artifact@v3 + with: + name: logs.zip + path: .tox/**/log/ + # https://github.com/actions/upload-artifact/issues/123 + continue-on-error: true + + - name: Report failure if git reports dirty status + run: | + if [[ -n $(git status -s) ]]; then + # shellcheck disable=SC2016 + echo -n '::error file=git-status::' + printf '### Failed as git reported modified and/or untracked files\n```\n%s\n```\n' "$(git status -s)" | tee -a "$GITHUB_STEP_SUMMARY" + exit 99 + fi + # https://github.com/actions/toolkit/issues/193 + codeql: + name: codeql + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["python"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" + + check: # This job does nothing and is only used for the branch protection + if: always() + permissions: + pull-requests: write # allow codenotify to comment on pull-request + + needs: + - build + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + - name: Check out src from Git + uses: actions/checkout@v3 + + - name: Notify repository owners about lint change affecting them + uses: sourcegraph/codenotify@v0.6.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # https://github.com/sourcegraph/codenotify/issues/19 + continue-on-error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0f2002 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +src/pia/_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0804af4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,160 @@ +--- +ci: + # format compatible with commitlint + autoupdate_commit_msg: "chore: pre-commit autoupdate" + autoupdate_schedule: monthly + autofix_commit_msg: | + chore: auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + skip: + # https://github.com/pre-commit-ci/issues/issues/55 + - pip-compile +exclude: > + (?x)^( + .config/requirements.*| + .vscode/extensions.json| + .vscode/settings.json| + src/.*/_version.py + )$ +repos: + - repo: meta + hooks: + - id: check-useless-excludes + - repo: https://github.com/pre-commit/mirrors-prettier + # keep it before yamllint + rev: v3.0.0-alpha.4 + hooks: + - id: prettier + always_run: true + additional_dependencies: + - prettier + - prettier-plugin-toml + - prettier-plugin-sort-json + # - repo: https://github.com/streetsidesoftware/cspell-cli + # rev: v6.17.1 + # hooks: + # - id: cspell + # # entry: codespell --relative + # args: [--relative, --no-progress, --no-summary] + # name: Spell check with cspell + - repo: https://github.com/sirosen/check-jsonschema + rev: 0.19.2 + hooks: + - id: check-github-workflows + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - id: fix-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: debug-statements + language_version: python3 + # - repo: https://github.com/codespell-project/codespell + # rev: v2.2.2 + # hooks: + # - id: codespell + # exclude: > + # (?x)^( + # .config/dictionary.txt| + # examples/broken/encoding.j2| + # test/schemas/negative_test/.*| + # test/schemas/test/.*| + # src/ansiblelint/schemas/.*\.json + # )$ + # additional_dependencies: + # - tomli + - repo: https://github.com/PyCQA/doc8 + rev: v1.1.1 + hooks: + - id: doc8 + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.28.0 + hooks: + - id: yamllint + files: \.(yaml|yml)$ + types: [file, yaml] + entry: yamllint --strict + - repo: https://github.com/PyCQA/isort + rev: 5.11.4 + hooks: + - id: isort + args: + # https://github.com/pre-commit/mirrors-isort/issues/9#issuecomment-624404082 + - --filter-files + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + language_version: python3 + - repo: https://github.com/pycqa/flake8.git + rev: 6.0.0 + hooks: + - id: flake8 + language_version: python3 + additional_dependencies: + - flake8-2020>=1.6.0 + # - flake8-black>=0.1.1 + - flake8-docstrings>=1.5.0 + - flake8-pytest-style>=1.2.2 + - flake8-future-annotations>=0.0.3 + - repo: https://github.com/asottile/pyupgrade + # keep it after flake8 + rev: v3.3.1 + hooks: + - id: pyupgrade + args: ["--py38-plus"] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.991 + hooks: + - id: mypy + # empty args needed in order to match mypy cli behavior + args: [--strict] + additional_dependencies: + - pytest + - repo: https://github.com/pycqa/pylint + rev: v2.15.9 + hooks: + - id: pylint + args: + - --output-format=colorized + additional_dependencies: + - ansible-core>=2.14.0 + - pytest + - repo: https://github.com/jazzband/pip-tools + rev: 6.12.1 + hooks: + - id: pip-compile + name: lock + always_run: true + entry: pip-compile --resolver=backtracking -q --no-annotate --output-file=.config/requirements-lock.txt pyproject.toml --strip-extras --unsafe-package ruamel-yaml-clib + language: python + files: ^.config\/requirements.*$ + alias: lock + stages: [manual] + language_version: "3.9" # minimal we support officially + additional_dependencies: + - pip>=22.3.1 + - id: pip-compile + name: deps + entry: pip-compile --resolver=backtracking -q --no-annotate --output-file=.config/requirements.txt pyproject.toml --extra test --strip-extras --unsafe-package ansible-core + language: python + files: ^.config\/requirements.*$ + alias: deps + language_version: "3.9" # minimal we support officially + always_run: true + additional_dependencies: + - pip>=22.3.1 + - id: pip-compile + entry: pip-compile --resolver=backtracking -q --no-annotate --output-file=.config/requirements.txt pyproject.toml --extra test --strip-extras --unsafe-package ansible-core --upgrade + language: python + always_run: true + files: ^.config\/requirements.*$ + alias: up + stages: [manual] + language_version: "3.9" # minimal we support officially + additional_dependencies: + - pip>=22.3.1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7bda9a2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.formatting.provider": "black", + "editor.formatOnSave": true +} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..c43d877 --- /dev/null +++ b/.yamllint @@ -0,0 +1,9 @@ +--- +rules: + document-start: + present: true + indentation: + level: error + indent-sequences: consistent +ignore: | + .tox diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5e1834 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 PyContribs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..469890f --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# PIA - Package Installer for Ansible + +> Like pip but for Ansible content, resilient content management that works +> fast, offline, with caching, compatible with multiple versions of ansible +> and a very friendly user interface. + +This project aims to become an alternative to the current `ansible-galaxy` +command line tool, one that would address some shortcomings such as: + +- Ability to run offline, especially when checking if current dependencies + are up to date. +- Use caching similar to how pip does, so network access would be needed only + if current data is not available locally. +- Be idempotent. + +## Installation + +``` +pip3 install pia +``` + +## Usage + +Please note that not all features are implemented yet. + +```bash +# Install a collection +pia install namespace.collection_name +# should also accept aliases like `pip i ...` + +# Install a collection archive from disk +pia install ./path/to/collection.tar.gz + +# Uninstall a collection +pia uninstall namespace.collection_name + +# List installed collections +pia list +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cdcd47c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,173 @@ +[build-system] +requires = [ + "setuptools >= 63.0.0", # required by pyproject+setuptools_scm integration + "setuptools_scm[toml] >= 7.0.5", # required for "no-local-version" scheme + +] +build-backend = "setuptools.build_meta" + +[project] +# https://peps.python.org/pep-0621/#readme +requires-python = ">=3.8" +dynamic = ["version", "dependencies", "optional-dependencies"] +name = "pia" +description = "(Alternative) Package Installer for Ansible" +readme = "README.md" +authors = [{ "name" = "Sorin Sbarnea", "email" = "sorin.sbarnea@gmail.com" }] +maintainers = [ + { "name" = "Sorin Sbarnea", "email" = "sorin.sbarnea@gmail.com" } +] +license = { text = "MIT" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: MacOS", + "Operating System :: POSIX", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python", + "Topic :: System :: Systems Administration", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Topic :: Utilities", +] +keywords = ["ansible"] + +[project.urls] +homepage = "https://github.com/pycontribs/pia" +repository = "https://github.com/pycontribs/pia" +changelog = "https://github.com/pycontribs/pia/releases" + +[project.scripts] +pia = "pia.__main__:main" + +[tool.black] +target-version = ["py39"] + +[tool.codespell] +skip = ".tox,.mypy_cache,build,.git,.eggs,pip-wheel-metadata" + +[tool.coverage.run] +source = ["src"] +branch = true +parallel = true +concurrency = ["multiprocessing", "thread"] +data_file = ".tox/.coverage" + +[tool.coverage.report] +exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"] +fail_under = 35 +skip_covered = true + +[tool.isort] +profile = "black" +known_third_party = "ansible,pytest,setuptools,yaml" +# https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 + +[tool.mypy] +python_version = 3.9 +color_output = true +error_summary = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_any_generics = true +# disallow_any_unimported = True +# warn_redundant_casts = True +# warn_return_any = True +# warn_unused_configs = True +# site-packages is here to help vscode mypy integration getting confused +exclude = "(build|dist|test/local-content|site-packages|~/.pyenv)" + +[[tool.mypy.overrides]] +module = ["ansible.*", "yamllint.*", "pia._version"] +ignore_missing_imports = true +ignore_errors = true + +[tool.pylint.MAIN] +extension-pkg-allow-list = ["black.parsing"] + +[tool.pylint.IMPORTS] +preferred-modules = ["py:pathlib", "unittest:pytest"] + +[tool.pylint.MASTER] +# Ignore as being generated: +ignore-paths = "^src/pia/_version.*$" + +[tool.pylint."MESSAGES CONTROL"] +disable = [ + # On purpose disabled as we rely on black + "line-too-long", +] + +[tool.pylint.TYPECHECK] +# pylint is unable to detect Namespace attributes and will throw a E1101 +generated-members = "options.*" + +[tool.pylint.SUMMARY] +# We don't need the score spamming console, as we either pass or fail +score = "n" + +# spell-checker:ignore filterwarnings norecursedirs optionflags +[tool.pytest.ini_options] +# do not add options here as this will likely break either console runs or IDE +# integration like vscode or pycharm +addopts = "-p no:pytest_cov" +# https://code.visualstudio.com/docs/python/testing +# coverage is re-enabled in `tox.ini`. That approach is safer than +# `--no-cov` which prevents activation from tox.ini and which also fails +# when plugin is effectively missing. +doctest_optionflags = ["ALLOW_UNICODE", "ELLIPSIS"] +filterwarnings = [ + "error", + # Ansible originated + "ignore:The _yaml extension module is now located at yaml._yaml and its location is subject to change:DeprecationWarning:", + # Ansible insides on py310: + "ignore:_SixMetaPathImporter:ImportWarning", + "ignore:_AnsibleCollectionFinder:ImportWarning", + "ignore:_AnsibleCollectionRootPkgLoader:ImportWarning", + "ignore:_AnsibleCollectionNSPkgLoader.exec_module:ImportWarning", + "ignore:_AnsibleCollectionPkgLoader.exec_module:ImportWarning", + "ignore:_AnsiblePathHookFinder.find_spec:ImportWarning", + "ignore:The distutils package is deprecated and slated for removal:DeprecationWarning", +] +minversion = "4.6.6" +norecursedirs = [ + "build", + "collections", + "dist", + "docs", + ".cache", + ".eggs", + ".git", + ".github", + ".tox", + "*.egg", +] +# Using --pyargs instead of testpath as we embed some tests +# See: https://github.com/pytest-dev/pytest/issues/6451#issuecomment-687043537 +# testpaths = +xfail_strict = true + +[tool.setuptools.dynamic] +optional-dependencies.test = { file = [".config/requirements-test.txt"] } +optional-dependencies.lock = { file = [".config/requirements-lock.txt"] } +dependencies = { file = [".config/requirements.in"] } + +[tool.setuptools_scm] +local_scheme = "no-local-version" +write_to = "src/pia/_version.py" diff --git a/src/pia/__init__.py b/src/pia/__init__.py new file mode 100644 index 0000000..df1f4e4 --- /dev/null +++ b/src/pia/__init__.py @@ -0,0 +1,7 @@ +"""PIA - Package Installer for Ansible.""" +try: + from pia._version import version as __version__ +except ImportError: # pragma: no branch + __version__ = "0.1.dev1" + +__all__ = ("__version__",) diff --git a/src/pia/__main__.py b/src/pia/__main__.py new file mode 100644 index 0000000..42a2b7d --- /dev/null +++ b/src/pia/__main__.py @@ -0,0 +1,85 @@ +"""PIA command line.""" +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys + +from pia import __version__ +from pia.proc import get_local_collections + + +def main(args: list[str] | None = None) -> None: + """Execute entrypoint for the pia package.""" + parser = argparse.ArgumentParser( + prog="pia", + description="Pia helps you list, install and uninstall Ansible content.", + epilog="pia is not ready for consumption yet!", + ) + parser.add_argument( + "-V", "--version", action="version", version=f"%(prog)s {__version__}" + ) + parser.add_argument( + "-f", "--force", action="store_true", default=False, help="Force command." + ) + # parser.add_argument( + # "command", + # help="The command to run: install, uninstall or list.", + # ) + subparsers = parser.add_subparsers(dest="command", help="sub-command help") + parser_install = subparsers.add_parser("install", help="Installs content") + parser_install.add_argument( + "collections", nargs="+", type=str, help="collection name such acme.goodies" + ) + + parser_uninstall = subparsers.add_parser("uninstall", help="Uninstalls content") + parser_uninstall.add_argument( + "collections", nargs="+", type=str, help="collection name such acme.goodies" + ) + + parser_list = subparsers.add_parser("list", help="Lists content already installed") + parser_list.add_argument( + "format", + choices=["plain", "json"], + default="plain", + const="plain", + nargs="?", + help="Output format", + ) + + options = parser.parse_args(args) + if options.command == "list": + + colmap = get_local_collections() + if options.format == "plain": + for collection, data in colmap.items(): + print(f"{collection:40s} {data['version']}") + elif options.format == "json": + print(colmap) + else: + raise RuntimeError(f"Unknown format {options.format}") + elif options.command == "install": + subprocess.run( + ["ansible-galaxy", "collection", "install", *options.collections], + check=True, + ) + elif options.command == "uninstall": + # https://github.com/ansible/ansible/issues/67759 + colmap = get_local_collections() + for collection in options.collections: + if collection in colmap: + namespace, name = collection.split(".") + path = colmap[collection]["path"] + f"/{namespace}/{name}" + print(f"Going to remove {path}") + shutil.rmtree(path) + else: + print(f"Collection {collection} was not found locally.") + else: + print(options) + raise NotImplementedError() + print(f"pia {__version__} is not ready for consumption yet! {options}") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/src/pia/proc.py b/src/pia/proc.py new file mode 100644 index 0000000..115cdfb --- /dev/null +++ b/src/pia/proc.py @@ -0,0 +1,41 @@ +"""Module to work with processes.""" +from __future__ import annotations + +import json +import subprocess + +# noqa: FA100 +from typing import Mapping, Sequence, Union + +JsonValueType = Union[ + None, + str, + int, + float, + bool, + Sequence["JsonValueType"], + Mapping[str, "JsonValueType"], +] +JsonObjectType = Mapping[str, JsonValueType] + + +def json_from_cmd(cmd: list[str]) -> JsonObjectType: + """Return loaded JSON output from external command.""" + result = subprocess.run(cmd, check=True, capture_output=True) + data = json.loads(result.stdout) + if not isinstance(data, dict): + raise RuntimeError("Unexpected data.") + return data + + +def get_local_collections() -> dict[str, dict[str, str]]: + """Return locally installed collections with their version and path.""" + result = json_from_cmd(["ansible-galaxy", "collection", "list", "--format=json"]) + colmap = {} + if not isinstance(result, dict): + raise RuntimeError(f"Unexpected data {result}") + for path, collections in result.items(): + for k, value in collections.items(): + colmap[k] = {"version": value.get("version", None), "path": path} + colmap = dict(sorted(colmap.items())) + return colmap diff --git a/src/pia/py.typed b/src/pia/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e54d5e4 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +"""PIA module.""" diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..5f3e278 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,11 @@ +"""Tests for PIA.""" +import pytest + +from pia.__main__ import main + + +def test_version() -> None: + """Sample test.""" + with pytest.raises(SystemExit) as excinfo: + main(["--version"]) + assert excinfo.value.code == 0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..12c3de9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,144 @@ +# spell-checker:ignore linkcheck basepython changedir envdir envlist envname envsitepackagesdir passenv setenv testenv toxinidir toxworkdir usedevelop doctrees envpython posargs +[tox] +minversion = 4.0.0 +envlist = + lint + pkg + hook + docs + schemas + py + py-devel + eco +isolated_build = true +skip_missing_interpreters = True + +[testenv] +description = + Run the tests under {basepython} and + devel: ansible devel branch +extras = + test +deps = + devel: ansible-core @ git+https://github.com/ansible/ansible.git # GPLv3+ +commands = + # safety measure to assure we do not accidentally run tests with broken dependencies + {envpython} -m pip check + coverage run -m pytest {posargs:\ + -ra \ + --showlocals \ + --doctest-modules \ + --durations=5 \ + } +passenv = + CURL_CA_BUNDLE # https proxies, https://github.com/tox-dev/tox/issues/1437 + FORCE_COLOR + HOME + NO_COLOR + PYTEST_* # allows developer to define their own preferences + PYTEST_REQPASS # needed for CI + PYTHON* # PYTHONPYCACHEPREFIX, PYTHONIOENCODING, PYTHONBREAKPOINT,... + PY_COLORS + RTD_TOKEN + REQUESTS_CA_BUNDLE # https proxies + SETUPTOOLS_SCM_DEBUG + SSL_CERT_FILE # https proxies + LANG + LC_* +# recreate = True +setenv = + # Avoid runtime warning that might affect our devel testing + devel: ANSIBLE_DEVEL_WARNING = false + COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} + COVERAGE_PROCESS_START={toxinidir}/pyproject.toml + PIP_CONSTRAINT = {toxinidir}/.config/requirements.txt + devel,pkg: PIP_CONSTRAINT = /dev/null + PIP_DISABLE_PIP_VERSION_CHECK = 1 + PRE_COMMIT_COLOR = always + FORCE_COLOR = 1 +allowlist_externals = + git + sh + tox + rm + pwd +usedevelop = true +skip_install = false + +[testenv:lint] +description = Run all linters +# pip compile includes python version in output constraints, so we want to +# be sure that version does not change randomly. +basepython = python3.9 +deps = + pre-commit>=2.6.0 +skip_install = true +commands = + {envpython} -m pre_commit run --all-files --show-diff-on-failure {posargs:} +passenv = + {[testenv]passenv} + PRE_COMMIT_HOME +setenv = + {[testenv]setenv} + # avoid messing pre-commit with out own constraints + PIP_CONSTRAINT= + +[testenv:deps] +description = Bump all test dependencies +# we reuse the lint environment +envdir = {toxworkdir}/lint +skip_install = true +basepython = python3.9 +deps = + {[testenv:lint]deps} +setenv = + # without his upgrade would likely not do anything + PIP_CONSTRAINT = /dev/null +commands = + pre-commit run --all-files --show-diff-on-failure --hook-stage manual lock + pre-commit run --all-files --show-diff-on-failure --hook-stage manual up + # Update pre-commit hooks + pre-commit autoupdate + # We fail if files are modified at the end + git diff --exit-code + +[testenv:pkg] +description = + Build package, verify metadata, install package and assert behavior when ansible is missing. +deps = + build >= 0.9.0 + twine >= 4.0.1 +skip_install = true +# Ref: https://twitter.com/di_codes/status/1044358639081975813 +commands = + # build wheel and sdist using PEP-517 + {envpython} -c 'import os.path, shutil, sys; \ + dist_dir = os.path.join("{toxinidir}", "dist"); \ + os.path.isdir(dist_dir) or sys.exit(0); \ + print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \ + shutil.rmtree(dist_dir)' + {envpython} -m build --outdir {toxinidir}/dist/ {toxinidir} + # Validate metadata using twine + twine check --strict {toxinidir}/dist/* + # Install the wheel + sh -c 'python3 -m pip install "pia[lock] @ file://$(echo {toxinidir}/dist/*.whl)"' + # Uninstall it + python3 -m pip uninstall -y pia + +[testenv:clean] +skip_install = true +deps = +commands = + find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ + rm -rf .mypy_cache + +[testenv:coverage] +description = Combines and displays coverage results +commands = + sh -c "coverage combine -q .tox/.coverage.*" + # needed by codecov github actions: + -coverage xml + # just for humans running it: + coverage report +deps = + coverage[toml]