From 6d5e243e734518bf3d796e718e4a8c8be331a614 Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Thu, 3 Aug 2023 09:56:28 -0400 Subject: [PATCH] Initial commit. --- .github/CODE_OF_CONDUCT.md | 133 +++++++++ .github/FUNDING.yml | 2 + .github/dependabot.yml | 6 + .github/workflows/ci.yml | 61 ++++ .github/workflows/codeql-analysis.yml | 38 +++ .github/workflows/delete-pr-image.yml | 23 ++ .github/workflows/lint.yml | 35 +++ .github/workflows/pypi-package.yml | 45 +++ .github/workflows/test.yml | 32 +++ .gitignore | 4 + .pre-commit-config.yaml | 5 + CHANGELOG.md | 33 +++ LICENSE | 201 ++++++++++++++ README.md | 92 +++++++ changelog.d/towncrier_template.md.jinja | 28 ++ pyproject.toml | 276 +++++++++++++++++++ src/jinjanator_plugin_format_xml/__init__.py | 0 src/jinjanator_plugin_format_xml/plugin.py | 33 +++ src/jinjanator_plugin_format_xml/py.typed | 0 tests/test_plugin.py | 37 +++ workflow-support/ci_paths.yml | 7 + workflow-support/lint_paths.yml | 5 + workflow-support/make_ci_image.sh | 71 +++++ workflow-support/versions.json | 1 + 24 files changed, 1168 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/delete-pr-image.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/pypi-package.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 changelog.d/towncrier_template.md.jinja create mode 100644 pyproject.toml create mode 100644 src/jinjanator_plugin_format_xml/__init__.py create mode 100644 src/jinjanator_plugin_format_xml/plugin.py create mode 100644 src/jinjanator_plugin_format_xml/py.typed create mode 100644 tests/test_plugin.py create mode 100644 workflow-support/ci_paths.yml create mode 100644 workflow-support/lint_paths.yml create mode 100755 workflow-support/make_ci_image.sh create mode 100644 workflow-support/versions.json diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1d8ad18 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..de9332e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: kpfleming +liberapay: kpfleming diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8156d69 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI checks + +on: + push: + branches: + - main + pull_request: + types: + - opened + - reopened + - synchronize + branches: + - main + schedule: + - cron: "35 4 * * 6" + +jobs: + preflight: + runs-on: ubuntu-22.04 + outputs: + image_base: ${{ steps.details.outputs.image_base }} + image_tag: ${{ steps.preflight.outputs.image_tag }} + versions: ${{ steps.get_versions.outputs.versions }} + need_ci: ${{ (steps.preflight.outputs.need_ci == 'true') || (steps.preflight.outputs.need_image == 'true') }} + steps: + - uses: actions/checkout@v3 + - id: get_versions + run: cat workflow-support/versions.json >> $GITHUB_OUTPUT + - id: details + uses: kpfleming/composite-actions/image-details@v2 + with: + base_image: python:bookworm-main + - id: preflight + uses: kpfleming/composite-actions/ci-preflight@v2 + with: + ci_paths: workflow-support/ci_paths.yml + files_hash: ${{ hashfiles('pyproject.toml', 'workflow-support/make_ci_image.sh', '.github/workflows/ci.yml') }} + base_image_hash: ${{ steps.details.outputs.base_image_hash }} + - id: make-ci-image + uses: kpfleming/composite-actions/make-ci-image@v2 + if: steps.preflight.outputs.need_image == 'true' + with: + build_args: ${{ vars.PYTHON_DISTRIBUTION_NAME }} + base_image: ${{ steps.details.outputs.base_image }} + image_name: ${{ steps.details.outputs.image_base }}:${{ steps.preflight.outputs.image_tag }} + image_cache_key: ${{ steps.preflight.outputs.image_cache_key }} + image_registry: ${{ steps.details.outputs.image_registry }} + registry_username: ${{ secrets.QUAY_BOT_NAME }} + registry_password: ${{ secrets.QUAY_BOT_PASSWORD }} + ci: + needs: + - preflight + strategy: + matrix: + python: ${{ fromJSON(needs.preflight.outputs.versions).python }} + fail-fast: false + uses: ./.github/workflows/test.yml + with: + if: ${{ needs.preflight.outputs.need_ci == 'true' }} + image: ${{ needs.preflight.outputs.image_base }}:${{ needs.preflight.outputs.image_tag }} + python: ${{ matrix.python }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2b6c3fa --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,38 @@ +--- +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: "30 22 * * 2" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: python + + - name: Perform analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/delete-pr-image.yml b/.github/workflows/delete-pr-image.yml new file mode 100644 index 0000000..94e9514 --- /dev/null +++ b/.github/workflows/delete-pr-image.yml @@ -0,0 +1,23 @@ +name: Delete PR Image + +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + clean-image: + runs-on: ubuntu-22.04 + steps: + - id: details + uses: kpfleming/composite-actions/image-details@v2 + with: + base_image: python:bookworm-main + - uses: kpfleming/composite-actions/delete-pr-image@v2 + with: + image_registry: ${{ steps.details.outputs.image_registry }} + registry_account: ${{ steps.details.outputs.registry_account }} + registry_token: ${{ secrets.QUAY_API_TOKEN }} + repo_name: ${{ steps.details.outputs.repo_name }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6ae6291 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint checks + +on: + pull_request: + branches: + - main + schedule: + - cron: "35 4 * * 6" + +jobs: + preflight: + runs-on: ubuntu-22.04 + outputs: + image_base: ${{ steps.details.outputs.image_base }} + need_lint: ${{ steps.preflight.outputs.need_lint == 'true' }} + steps: + - uses: actions/checkout@v3 + - id: details + uses: kpfleming/composite-actions/image-details@v2 + with: + base_image: python:bookworm-main + - id: preflight + uses: kpfleming/composite-actions/lint-preflight@v2 + with: + lint_paths: workflow-support/lint_paths.yml + lint: + if: needs.preflight.outputs.need_lint == 'true' + runs-on: ubuntu-22.04 + needs: + - preflight + container: + image: ${{ needs.preflight.outputs.image_base }}:main + steps: + - uses: actions/checkout@v3 + - uses: kpfleming/composite-actions/lint-hatch@v2 diff --git a/.github/workflows/pypi-package.yml b/.github/workflows/pypi-package.yml new file mode 100644 index 0000000..b0d4ee4 --- /dev/null +++ b/.github/workflows/pypi-package.yml @@ -0,0 +1,45 @@ +--- +name: Build and verify package, with optional upload to PyPI + +on: + push: + branches: [main] + tags: ["*"] + pull_request: + branches: [main] + release: + types: + - published + +permissions: + contents: read + id-token: write + +jobs: + build-package: + name: Build and verify package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: hynek/build-and-inspect-python-package@v1 + + release-pypi: + name: Publish released package to PyPI + environment: release-pypi + if: github.event.action == 'published' + runs-on: ubuntu-latest + needs: build-package + + steps: + - name: Download previously built packages + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + + - name: Upload packages + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..01fa883 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: CI tests + +on: + workflow_call: + inputs: + if: + required: false + default: true + type: boolean + image: + required: true + type: string + python: + required: true + type: string + +jobs: + test: + if: inputs.if + runs-on: ubuntu-22.04 + container: + image: ${{ inputs.image }} + steps: + - uses: actions/checkout@v3 + - name: build wheel + run: hatch build -t wheel + shell: bash + - name: install project + run: hatch run ci.py${{ inputs.python }}:pip install dist/*.whl + shell: bash + - name: run tests + run: hatch run ci.py${{ inputs.python }}:ci diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..095bb7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.coverage +*.egg-info +dist +build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2f1ba44 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.13.0" + hooks: + - id: pyproject-fmt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc74002 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [*Keep a +Changelog*](https://keepachangelog.com/en/1.0.0/) and this project +adheres to [*Calendar Versioning*](https://calver.org/). + +The **first number** of the version is the year. + +The **second number** is incremented with each release, starting at 1 +for each year. + +The **third number** is when we need to start branches for older +releases (only for emergencies). + +Committed changes for the next release can be found in the ["changelog.d" +directory](https://github.com/kpfleming/jinjanator-plugin-format-xml/tree/main/changelog.d) +in the project repository. + + + + + +## [23.1.0](https://github.com/kpfleming/jinjanator-plugin-format-xml/tree/23.1.0) - 2023-08-03 + +Initial release! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8357504 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# jinjanator-plugin-format-xml + +Open Source Initiative Approved License logo +[![CI](https://github.com/kpfleming/jinjanator-plugin-format-xml/workflows/CI%20checks/badge.svg)](https://github.com/kpfleming/jinjanator-plugin-format-xml/actions?query=workflow%3ACI%20checks) +[![Python](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/release/python-3912/) +[![License - Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-9400d3.svg)](https://spdx.org/licenses/Apache-2.0.html) +[![Code Style - Black](https://img.shields.io/badge/Code%20Style-Black-000000.svg)](https://github.com/psf/black) +[![Types - Mypy](https://img.shields.io/badge/Types-Mypy-blue.svg)](https://github.com/python/mypy) +[![Code Quality - Ruff](https://img.shields.io/badge/Code%20Quality-Ruff-red.svg)](https://github.com/astral-sh/ruff) +[![Project Management - Hatch](https://img.shields.io/badge/Project%20Management-Hatch-purple.svg)](https://github.com/pypa/hatch) +[![Testing - Pytest](https://img.shields.io/badge/Testing-Pytest-orange.svg)](https://github.com/pytest-dev/pytest) + +This repo contains `jinjanator-plugin-format-xml`, a plugin which +provides an XML parser for the [Jinjanator](https://github.com/kpfleming/jinjanator) tool. + +Open Source software: [Apache License 2.0](https://spdx.org/licenses/Apache-2.0.html) + +##   + + +This plugin allows Jinjanator to parse XML data for processing in +templates. The format can be selected using `--format xml` or +autoselected by using a data file with a name ending with `.xml`. + +## Installation + +``` +pip install jinjanator-plugin-format-xml +``` + +## Usage + +Suppose you have an NGINX configuration file template, `nginx.j2`: + +```jinja2 +server { + listen 80; + server_name {{ nginx.hostname }}; + + root {{ nginx.webroot }}; + index index.htm; +} +``` + +And you have a XML file with the data, `nginx.xml`: + +```xml + + + localhost + + + /var/www/project + + +``` + +This is how you render it into a working configuration file: + +```bash +$ jinjanate nginx.j2 nginx.xml > nginx.conf +``` + +## Options + +* `process-namespaces`: configures the XML parser to replace namespace + references in element names with the corresponding namespaces from + `xmlns` attributes in the top-level element in the document. + + +## Chat + +If you'd like to chat with the Jinjanator community, join us on +[Matrix](https://matrix.to/#/#jinjanator:km6g.us)! + +## Credits + +["Standing on the shoulders of +giants"](https://en.wikipedia.org/wiki/Standing_on_the_shoulders_of_giants) +could not be more true than it is in the Python community; this +project relies on many wonderful tools and libraries produced by the +global open source software community, in addition to Python +itself. I've listed many of them below, but if I've overlooked any +please do not be offended :-) + +* [Black](https://github.com/psf/black) +* [Hatch-Fancy-PyPI-Readme](https://github.com/hynek/hatch-fancy-pypi-readme) +* [Hatch](https://github.com/pypa/hatch) +* [Mypy](https://github.com/python/mypy) +* [Pytest](https://github.com/pytest-dev/pytest) +* [Ruff](https://github.com/astral-sh/ruff) +* [Towncrier](https://github.com/twisted/towncrier) diff --git a/changelog.d/towncrier_template.md.jinja b/changelog.d/towncrier_template.md.jinja new file mode 100644 index 0000000..98520b7 --- /dev/null +++ b/changelog.d/towncrier_template.md.jinja @@ -0,0 +1,28 @@ +{%- if versiondata["version"] == "main" -%} +## Changes for the Upcoming Release + +:::{warning} +These changes reflect the current [development progress](https://github.com/kpfleming/jinjanator-plugin-format-xml/tree/main) and are **not** included in a PyPI release yet. +::: +{% else -%} +## [{{ versiondata["version"] }}](https://github.com/kpfleming/jinjanator-plugin-format-xml/tree/{{ versiondata["version"] }}) - {{ versiondata["date"] }} +{%- endif %} + +{% for section, _ in sections.items() %} +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} + +### {{ definitions[category]['name'] }} + +{% for text, values in sections[section][category].items() %} +- {{ text }} + {{ values|join(',\n ') }} +{% endfor %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c9e2e9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,276 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatch-fancy-pypi-readme", + "hatch-vcs", + "hatchling", +] + +[project] +name = "jinjanator-plugin-format-xml" +description = "Plugin which provides Ansible filters and tests to the Jinjanator tool" +license = { text="Apache-2.0" } +authors = [ + { name="Kevin P. Fleming", email="jinjanator@kevin.km6g.us" }, +] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Typing :: Typed", +] +dynamic = [ + "readme", + "version", +] +dependencies = [ + "jinjanator-plugins==23.4.*", + "xmltodict", +] +[project.urls] +"Bug Tracker" = "https://github.com/kpfleming/jinjanator-plugin-format-xml/issues" +"Homepage" = "https://github.com/kpfleming/jinjanator-plugin-format-xml" +[project.entry-points.jinjanator] +format_xml = "jinjanator_plugin_format_xml.plugin" + +[tool.hatch.envs.changelog] +skip-install = true +dependencies = [ + "towncrier", +] + +[tool.hatch.envs.changelog.scripts] +draft = [ + "rm -f changelog.d/*~", + "towncrier build --version main --draft", +] +release = [ + "rm -f changelog.d/*~", + "towncrier build --yes --version {args}", +] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build] +exclude = [ + "*~", + ".github", +] + +[tool.hatch.build.targets.sdist] +include = [ + "src", + "tests", + "*.md", +] + +[tool.hatch.build.targets.wheel] +packages = [ + "src/jinjanator_plugin_format_xml", +] + +[tool.hatch.envs.default] +python = "3.11" + +[tool.hatch.envs.lint] +dependencies = [ + "black", + "ruff", + "mypy", + "pytest", # needed for type-checking tests + "types-xmltodict", +] + +[tool.hatch.envs.lint.scripts] +lint = [ + "black --preview src tests", + "ruff check --fix -- src tests", + "mypy --package jinjanator_plugin_format_xml", + "mypy tests", + "shellcheck workflow-support/*.sh", +] +lint-action = [ + "black --check --diff --preview src tests", + "ruff check --format=github -- src tests", + "mypy --package jinjanator_plugin_format_xml", + "mypy tests", + "shellcheck workflow-support/*.sh", +] + +[tool.hatch.envs.ci] +dependencies = [ + "attrs", + "coverage[toml]", + "jinjanator", + "pytest", + "pytest-cov", + "pytest-icdiff", +] + +[[tool.hatch.envs.ci.matrix]] +python = [ +"3.8", +"3.9", +"3.10", +"3.11", +"3.12", +] + +[tool.hatch.envs.ci.scripts] +ci = [ + "rm -f .coverage", + "pytest --verbose --cov-append --cov-branch --cov=jinjanator_plugin_format_xml", + "coverage report --show-missing --fail-under=100", +] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ +# *jinjanator-plugin-format-xml*: Provides XML format (data input) support for Jinjanator + +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" +start-after = "" +end-before = "" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ +## Release Information +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "CHANGELOG.md" +start-after = "" +# pattern = "\n(###.+?\n)## " + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = """ +--- +[→ Full Changelog](https://github.com/kpfleming/jinjanator-plugin-format-xml/blob/main/CHANGELOG.md) +""" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/kpfleming/jinjanator-plugin-format-xml/tree/main/\g<2>)' + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +pattern = "#(\\d+)" +replacement = "[#\\1](https://github.com/kpfleming/jinjanator-plugin-format-xml/issues/\\1)" + +[tool.black] +line-length = 90 + +[tool.ruff] +src = ["src", "tests"] +format = "grouped" +target-version = "py38" +select = ["ALL"] + +ignore = [ + "ANN", # Mypy is better at this. + "C901", # Leave complexity to me. + "COM", # Leave commas to Black. + "D", # We have different ideas about docstrings. + "E501", # leave line-length enforcement to Black + "PLR0912", # Leave complexity to me. + "TRY301", # Raise in try blocks can totally make sense. +] +unfixable = ["F401"] + +[tool.ruff.per-file-ignores] +"tests/*" = [ + "PLC1901", # empty strings are falsey, but are less specific in tests + "PT005", # we use always underscores and explicit names + "S101", # assert + "SIM300", # Yoda rocks in tests +] + +[tool.ruff.isort] +lines-between-types = 1 +lines-after-imports = 2 + +[tool.pytest.ini_options] +minversion = "6.0" +xfail_strict = true +testpaths = [ + "tests", +] +addopts = [ + "-ra", + "--strict-markers", +] + +[tool.mypy] +python_version = 3.8 +namespace_packages = true +explicit_package_bases = true +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +follow_imports = "normal" +no_implicit_optional = true +strict_equality = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unused_ignores = true + +[tool.towncrier] +name = "jinjanator-plugin-format-xml" +package = "jinjanator_plugin_format_xml" +directory = "changelog.d" +filename = "CHANGELOG.md" +start_string = "\n" +template = "changelog.d/towncrier_template.md.jinja" +title_format = "" +issue_format = "[#{issue}](https://github.com/kpfleming/jinjanator-plugin-format-xml/issues/{issue})" +underlines = ["", "", ""] + +[[tool.towncrier.section]] +path = "" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Backwards-incompatible Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecating" +name = "Deprecations" +showcontent = true + +[[tool.towncrier.type]] +directory = "adding" +name = "Additions" +showcontent = true + +[[tool.towncrier.type]] +directory = "changing" +name = "Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixing" +name = "Fixes" +showcontent = true diff --git a/src/jinjanator_plugin_format_xml/__init__.py b/src/jinjanator_plugin_format_xml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/jinjanator_plugin_format_xml/plugin.py b/src/jinjanator_plugin_format_xml/plugin.py new file mode 100644 index 0000000..dd76585 --- /dev/null +++ b/src/jinjanator_plugin_format_xml/plugin.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Any, Iterable, Mapping + +import xmltodict + +from jinjanator_plugins import ( + Formats, + plugin_formats_hook, +) + + +class XMLFormat: + name = "xml" + suffixes: Iterable[str] | None = ".xml" + option_names: Iterable[str] | None = "process-namespaces" + + def __init__(self, options: Iterable[str] | None) -> None: + self.process_namespaces = False + if options: + for _option in options: + self.process_namespaces = True + + def parse( + self, + data_string: str, + ) -> Mapping[str, Any]: + return xmltodict.parse(data_string, process_namespaces=self.process_namespaces) + + +@plugin_formats_hook +def plugin_formats() -> Formats: + return {XMLFormat.name: XMLFormat} diff --git a/src/jinjanator_plugin_format_xml/py.typed b/src/jinjanator_plugin_format_xml/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..917900d --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from jinjanator_plugin_format_xml import plugin + + +def test_formats_hook() -> None: + result = plugin.plugin_formats() + assert plugin.XMLFormat.name in result + assert result[plugin.XMLFormat.name] == plugin.XMLFormat + + +def test_format() -> None: + fmt = plugin.XMLFormat(None) + result = fmt.parse("bleu") + assert "testdoc" in result + assert "cheese" in result["testdoc"] + assert "bleu" == result["testdoc"]["cheese"] + + +def test_format_namespaces() -> None: + fmt = plugin.XMLFormat("process-namespaces") + result = fmt.parse( + 'bleu' + ) + assert "testdoc" in result + assert "http://a.com/:cheese" in result["testdoc"] + assert "bleu" == result["testdoc"]["http://a.com/:cheese"] + + +def test_format_ignore_namespaces() -> None: + fmt = plugin.XMLFormat(None) + result = fmt.parse( + 'bleu' + ) + assert "testdoc" in result + assert "a:cheese" in result["testdoc"] + assert "bleu" == result["testdoc"]["a:cheese"] diff --git a/workflow-support/ci_paths.yml b/workflow-support/ci_paths.yml new file mode 100644 index 0000000..2a64217 --- /dev/null +++ b/workflow-support/ci_paths.yml @@ -0,0 +1,7 @@ +paths: + - 'src/**/*.py' + - 'tests/**/*.py' + - 'pyproject.toml' + - 'workflow-support/versions.json' + - '.github/workflows/ci.yml' + - '.github/workflows/test.yml' diff --git a/workflow-support/lint_paths.yml b/workflow-support/lint_paths.yml new file mode 100644 index 0000000..5f8d092 --- /dev/null +++ b/workflow-support/lint_paths.yml @@ -0,0 +1,5 @@ +paths: + - 'src/**/*.py' + - 'tests/**/*.py' + - 'pyproject.toml' + - '.github/workflows/lint.yml' diff --git a/workflow-support/make_ci_image.sh b/workflow-support/make_ci_image.sh new file mode 100755 index 0000000..82f4568 --- /dev/null +++ b/workflow-support/make_ci_image.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +set -ex + +# Arguments: +# +# 1: registry, name, and tag of base image +# 2: registry, name, and tag of image to be created +# 3: name of Python distribution produced by this repo + +scriptdir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") +# source directory must be mounted at the same path inside the build +# container as it will be mounted when GitHub Actions launches the +# container, because Hatch uses the path as part of the identity of +# the virtual environments it creates +containersrcdir="/__w/${GITHUB_REPOSITORY##*/}/${GITHUB_REPOSITORY##*/}" +base_image=${1}; shift +image_name=${1}; shift +dist_name=${1}; shift + +lint_deps=(shellcheck) +proj_deps=(libsqlite3-0) +proj_build_deps=(build-essential libc6-dev pkg-config) + +hatchenvs=(lint ci) +cimatrix=(3.9 3.10 3.11 3.12) + +c=$(buildah from "${base_image}") + +build_cmd() { + buildah run --network host "${c}" -- "$@" +} + +build_cmd_with_source() { + buildah run --network host --volume "$(realpath "${scriptdir}/.."):${containersrcdir}" --workingdir "${containersrcdir}" "${c}" -- "$@" +} + +build_cmd apt update --quiet=2 +build_cmd apt install --yes --quiet=2 "${lint_deps[@]}" "${proj_deps[@]}" "${proj_build_deps[@]}" + +build_cmd pip3.11 install git+https://github.com/pypa/hatch + +for env in "${hatchenvs[@]}"; do + # this looks weird... but it causes Hatch to create the env, + # install all of the project's dependencies and the project, + # then runs pip to uninstall the project, leaving the env + # in place with the dependencies + case "${env}" in + ci*) + for py in "${cimatrix[@]}"; do + build_cmd_with_source hatch env create "${env}.py${py}" + build_cmd_with_source hatch -e "${env}.py${py}" run pip uninstall --yes "${dist_name}" + done + ;; + *) + build_cmd_with_source hatch env create "${env}" + build_cmd_with_source hatch -e "${env}" run pip uninstall --yes "${dist_name}" + ;; + esac +done + +build_cmd apt remove --yes --purge "${proj_build_deps[@]}" +build_cmd apt autoremove --yes --purge +build_cmd apt clean autoclean +build_cmd sh -c "rm -rf /var/lib/apt/lists/*" +build_cmd rm -rf /root/.cache + +if buildah images --quiet "${image_name}"; then + buildah rmi "${image_name}" +fi +buildah commit --squash --rm "${c}" "${image_name}" diff --git a/workflow-support/versions.json b/workflow-support/versions.json new file mode 100644 index 0000000..5fdb6f0 --- /dev/null +++ b/workflow-support/versions.json @@ -0,0 +1 @@ +versions={"python": ["3.9", "3.10", "3.11", "3.12"]}