diff --git a/.ansible-lint b/.ansible-lint index 0e80b05..4ffc0ef 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -1,10 +1,9 @@ --- -# See https://ansible-lint.readthedocs.io/en/latest/configuring.html -# for a list of the configuration elements that can exist in this -# file. +# See https://ansible-lint.readthedocs.io/configuring/ for a list of +# the configuration elements that can exist in this file. enable_list: # Useful checks that one must opt-into. See here for more details: - # https://ansible-lint.readthedocs.io/en/latest/rules.html + # https://ansible-lint.readthedocs.io/rules/ - fcqn-builtins - no-log-password - no-same-owner diff --git a/.bandit.yml b/.bandit.yml deleted file mode 100644 index 0b53a96..0000000 --- a/.bandit.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# Configuration file for the Bandit python security scanner -# https://bandit.readthedocs.io/en/latest/config.html -# This config is applied to bandit when scanning the "tests" tree - -# Tests are first included by `tests`, and then excluded by `skips`. -# If `tests` is empty, all tests are are considered included. - -tests: -# - B101 -# - B102 - -skips: - - B101 # skip "assert used" check since assertions are required in pytests diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 92ff826..0000000 --- a/.flake8 +++ /dev/null @@ -1,25 +0,0 @@ -[flake8] -max-line-length = 80 -# Select (turn on) -# * Complexity violations reported by mccabe (C) - -# http://flake8.pycqa.org/en/latest/user/error-codes.html#error-violation-codes -# * Documentation conventions compliance reported by pydocstyle (D) - -# http://www.pydocstyle.org/en/stable/error_codes.html -# * Default errors and warnings reported by pycodestyle (E and W) - -# https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes -# * Default errors reported by pyflakes (F) - -# http://flake8.pycqa.org/en/latest/glossary.html#term-pyflakes -# * Default warnings reported by flake8-bugbear (B) - -# https://github.com/PyCQA/flake8-bugbear#list-of-warnings -# * The B950 flake8-bugbear opinionated warning - -# https://github.com/PyCQA/flake8-bugbear#opinionated-warnings -select = C,D,E,F,W,B,B950 -# Ignore flake8's default warning about maximum line length, which has -# a hard stop at the configured value. Instead we use -# flake8-bugbear's B950, which allows up to 10% overage. -# -# Also ignore flake8's warning about line breaks before binary -# operators. It no longer agrees with PEP8. See, for example, here: -# https://github.com/ambv/black/issues/21. Guido agrees here: -# https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b. -ignore = E501,W503 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47ab168..11c019d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,8 +3,30 @@ # These owners will be the default owners for everything in the # repo. Unless a later match takes precedence, these owners will be # requested for review when someone opens a pull request. +<<<<<<< HEAD +<<<<<<< HEAD * @mcdonnnj @Pascal-0x90 +======= +* @dav3r @felddy @jasonodoom @jsf9k @mcdonnnj +>>>>>>> ea4912cef0f380adaf030ca8d8df5cfc3696bcbf +======= +* @dav3r @felddy @jsf9k @mcdonnnj +>>>>>>> 0da26c3a45b9a9c2a7d41ed2687b177a6f597116 # These folks own any files in the .github directory at the root of # the repository and any of its subdirectories. /.github/ @dav3r @felddy @jsf9k @mcdonnnj + +# These folks own all linting configuration files. +/.ansible-lint @dav3r @felddy @jsf9k @mcdonnnj +/.bandit.yml @dav3r @felddy @jsf9k @mcdonnnj +/.flake8 @dav3r @felddy @jsf9k @mcdonnnj +/.isort.cfg @dav3r @felddy @jsf9k @mcdonnnj +/.mdl_config.yaml @dav3r @felddy @jsf9k @mcdonnnj +/.pre-commit-config.yaml @dav3r @felddy @jsf9k @mcdonnnj +/.prettierignore @dav3r @felddy @jsf9k @mcdonnnj +/.yamllint @dav3r @felddy @jsf9k @mcdonnnj +/requirements.txt @dav3r @felddy @jsf9k @mcdonnnj +/requirements-dev.txt @dav3r @felddy @jsf9k @mcdonnnj +/requirements-test.txt @dav3r @felddy @jsf9k @mcdonnnj +/setup-env @dav3r @felddy @jsf9k @mcdonnnj diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 48e226b..0bedb40 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,23 +5,58 @@ # these updates when the pull request(s) in the appropriate skeleton are merged # and Lineage processes these changes. -version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" + - directory: / + groups: + upload-download-artifact: + patterns: + - actions/download-artifact + - actions/upload-artifact ignore: + # Managed by cisagov/skeleton-generic - dependency-name: actions/cache - dependency-name: actions/checkout + - dependency-name: actions/dependency-review-action + - dependency-name: actions/labeler + - dependency-name: actions/setup-go - dependency-name: actions/setup-python + - dependency-name: cisagov/action-job-preamble + - dependency-name: cisagov/setup-env-github-action + - dependency-name: crazy-max/ghaction-github-labeler + - dependency-name: github/codeql-action + - dependency-name: hashicorp/setup-packer + - dependency-name: hashicorp/setup-terraform + - dependency-name: mxschmitt/action-tmate + # # Managed by cisagov/skeleton-python-library + # - dependency-name: actions/download-artifact + # - dependency-name: actions/upload-artifact + labels: + # dependabot default we need to replicate + - dependencies + # This matches our label definition in .github/labels.yml as opposed to + # dependabot's default of `github_actions`. + - github-actions + package-ecosystem: github-actions + schedule: + interval: weekly - - package-ecosystem: "pip" - directory: "/" + - directory: / + # ignore: + # # Managed by cisagov/skeleton-python-library + # - dependency-name: build + # - dependency-name: coverage + # - dependency-name: coveralls + # - dependency-name: pre-commit + # - dependency-name: pytest-cov + # - dependency-name: pytest + # - dependency-name: setuptools + # - dependency-name: twine + package-ecosystem: pip schedule: - interval: "weekly" + interval: weekly - - package-ecosystem: "terraform" - directory: "/" + - directory: / + package-ecosystem: terraform schedule: - interval: "weekly" + interval: weekly +version: 2 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..eeea8f5 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,81 @@ +--- +# Each entry in this file is a label that will be applied to pull requests +# if there is a match based on the matching rules for the entry. Please see +# the actions/labeler documentation for more information: +# https://github.com/actions/labeler#match-object +# +# Note: Verify that the label you want to use is defined in the +# crazy-max/ghaction-github-labeler configuration file located at +# .github/labels.yml. + +ansible: + - changed-files: + - any-glob-to-any-file: + - "**/ansible/**" +dependencies: + - changed-files: + - any-glob-to-any-file: + # Add any dependency files used. + - .pre-commit-config.yaml + - requirements*.txt + - pyproject.toml +docker: + - changed-files: + - any-glob-to-any-file: + - "**/compose*.yml" + - "**/docker-compose*.yml" + - "**/Dockerfile*" +documentation: + - changed-files: + - any-glob-to-any-file: + - "**/*.md" +github-actions: + - changed-files: + - any-glob-to-any-file: + - .github/workflows/** +javascript: + - changed-files: + - any-glob-to-any-file: + - "**/*.js" +packer: + - changed-files: + - any-glob-to-any-file: + - "**/*.pkr.hcl" +python: + - changed-files: + - any-glob-to-any-file: + - "**/*.py" +shell script: + - changed-files: + - any-glob-to-any-file: + # If this project has any shell scripts that do not end in the ".sh" + # extension, add them below. + - "**/*.sh" + - bump-version + - setup-env +terraform: + - changed-files: + - any-glob-to-any-file: + - "**/*.tf" +test: + - changed-files: + - any-glob-to-any-file: + # Add any test-related files or paths. + - .ansible-lint + - .mdl_config.yaml + - .yamllint + - pyproject.toml + - tests/** +typescript: + - changed-files: + - any-glob-to-any-file: + - "**/*.ts" +upstream update: + - head-branch: + # Any Lineage pull requests should use this branch. + - lineage/skeleton +version bump: + - changed-files: + - any-glob-to-any-file: + # Ensure this matches your version tracking file(s). + - src/**/_version.py diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..69f0a2d --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,94 @@ +--- +# Rather than breaking up descriptions into multiline strings we disable that +# specific rule in yamllint for this file. +# yamllint disable rule:line-length +- color: ff5850 + description: Pull requests that update Ansible code + name: ansible +- color: eb6420 + description: This issue or pull request is awaiting the outcome of another issue or pull request + name: blocked +- color: "000000" + description: This issue or pull request involves changes to existing functionality + name: breaking change +- color: d73a4a + description: This issue or pull request addresses broken functionality + name: bug +- color: 07648d + description: This issue will be advertised on code.gov's Open Tasks page (https://code.gov/open-tasks) + name: code.gov +- color: 0366d6 + description: Pull requests that update a dependency file + name: dependencies +- color: 1d63ed + description: Pull requests that update Docker code + name: docker +- color: 5319e7 + description: This issue or pull request improves or adds to documentation + name: documentation +- color: cfd3d7 + description: This issue or pull request already exists or is covered in another issue or pull request + name: duplicate +- color: b005bc + description: A high-level objective issue encompassing multiple issues instead of a specific unit of work + name: epic +- color: "000000" + description: Pull requests that update GitHub Actions code + name: github-actions +- color: 0e8a16 + description: This issue or pull request is well-defined and good for newcomers + name: good first issue +- color: ff7518 + description: Pull request that should count toward Hacktoberfest participation + name: hacktoberfest-accepted +- color: a2eeef + description: This issue or pull request will add or improve functionality, maintainability, or ease of use + name: improvement +- color: fef2c0 + description: This issue or pull request is not applicable, incorrect, or obsolete + name: invalid +- color: f0db4f + description: Pull requests that update JavaScript code + name: javascript +- color: ce099a + description: This pull request is ready to merge during the next Lineage Kraken release + name: kraken 🐙 +- color: a4fc5d + description: This issue or pull request requires further information + name: need info +- color: fcdb45 + description: This pull request is awaiting an action or decision to move forward + name: on hold +- color: 02a8ef + description: Pull requests that update Packer code + name: packer +- color: 3776ab + description: Pull requests that update Python code + name: python +- color: ef476c + description: This issue is a request for information or needs discussion + name: question +- color: d73a4a + description: This issue or pull request addresses a security issue + name: security +- color: 4eaa25 + description: Pull requests that update shell scripts + name: shell script +- color: 7b42bc + description: Pull requests that update Terraform code + name: terraform +- color: 00008b + description: This issue or pull request adds or otherwise modifies test code + name: test +- color: 2678c5 + description: Pull requests that update TypeScript code + name: typescript +- color: 1d76db + description: This issue or pull request pulls in upstream updates + name: upstream update +- color: d4c5f9 + description: This issue or pull request increments the version number + name: version bump +- color: ffffff + description: This issue will not be incorporated + name: wontfix diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db8491c..efd704e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,51 +1,140 @@ --- name: build -on: - push: +on: # yamllint disable-line rule:truthy + merge_group: + types: + - checks_requested + # We use the default activity types for the pull_request event as specified here: + # https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request pull_request: + push: repository_dispatch: - types: [apb] + types: + - apb + +# Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, +# nounset, errexit, and pipefail. The `-x` will print all commands as they are +# run. Please see the GitHub Actions documentation for more information: +# https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs +defaults: + run: + shell: bash -Eueo pipefail -x {0} env: - CURL_CACHE_DIR: ~/.cache/curl PIP_CACHE_DIR: ~/.cache/pip PRE_COMMIT_CACHE_DIR: ~/.cache/pre-commit RUN_TMATE: ${{ secrets.RUN_TMATE }} + TERRAFORM_DOCS_REPO_BRANCH_NAME: cisagov + TERRAFORM_DOCS_REPO_DEPTH: 1 + TERRAFORM_DOCS_REPO_URL: https://github.com/mcdonnnj/terraform-docs.git jobs: + diagnostics: + name: Run diagnostics + # This job does not need any permissions + permissions: {} + runs-on: ubuntu-latest + steps: + # Note that a duplicate of this step must be added at the top of + # each job. + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + check_github_status: "true" + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + output_workflow_context: "true" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} lint: + needs: + - diagnostics + permissions: + # actions/checkout needs this to fetch code + contents: read runs-on: ubuntu-latest steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} - id: setup-env - uses: cisagov/setup-env-github-action@develop - - uses: actions/checkout@v3 + uses: cisagov/setup-env-github-action@v1 + - uses: actions/checkout@v6 - id: setup-python - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: ${{ steps.setup-env.outputs.python-version }} # We need the Go version and Go cache location for the actions/cache step, # so the Go installation must happen before that. - - uses: actions/setup-go@v2 + - id: setup-go + uses: actions/setup-go@v6 with: - go-version: "1.16" - - name: Store installed Go version - id: go-version - run: | - echo "::set-output name=version::"\ - "$(go version | sed 's/^go version go\([0-9.]\+\) .*/\1/')" - - name: Lookup Go cache directory - id: go-cache + # There is no expectation for actual Go code so we disable caching as + # it relies on the existence of a go.sum file. + cache: false + go-version: ${{ steps.setup-env.outputs.go-version }} + - id: go-cache + name: Lookup Go cache directory run: | - echo "::set-output name=dir::$(go env GOCACHE)" - - uses: actions/cache@v3 + echo "dir=$(go env GOCACHE)" >> $GITHUB_OUTPUT + - uses: actions/cache@v5 env: - BASE_CACHE_KEY: "${{ github.job }}-${{ runner.os }}-\ - py${{ steps.setup-python.outputs.python-version }}-\ - go${{ steps.go-version.outputs.version }}-\ - packer${{ steps.setup-env.outputs.packer-version }}-\ - tf${{ steps.setup-env.outputs.terraform-version }}-" + BASE_CACHE_KEY: >- + ${{ github.job }}-${{ runner.os + }}-py${{ steps.setup-python.outputs.python-version + }}-go${{ steps.setup-go.outputs.go-version + }}-packer${{ steps.setup-env.outputs.packer-version + }}-tf${{ steps.setup-env.outputs.terraform-version }}- with: + key: >- + ${{ env.BASE_CACHE_KEY }}${{ + hashFiles('**/requirements-test.txt')}}-${{ + hashFiles('**/requirements.txt') }}-${{ + hashFiles('**/.pre-commit-config.yaml') }}-${{ + hashFiles('pyproject.toml') }} # Note that the .terraform directory IS NOT included in the # cache because if we were caching, then we would need to use # the `-upgrade=true` option. This option blindly pulls down the @@ -55,49 +144,58 @@ jobs: path: | ${{ env.PIP_CACHE_DIR }} ${{ env.PRE_COMMIT_CACHE_DIR }} - ${{ env.CURL_CACHE_DIR }} ${{ steps.go-cache.outputs.dir }} - # We do not use '**/setup.py' in the cache key so only the 'setup.py' - # file in the root of the repository is used. This is in case a Python - # package were to have a 'setup.py' as part of its internal codebase. - key: "${{ env.BASE_CACHE_KEY }}\ - ${{ hashFiles('**/requirements-test.txt') }}-\ - ${{ hashFiles('**/requirements.txt') }}-\ - ${{ hashFiles('**/.pre-commit-config.yaml') }}-\ - ${{ hashFiles('setup.py') }}" restore-keys: | ${{ env.BASE_CACHE_KEY }} - - name: Setup curl cache - run: mkdir -p ${{ env.CURL_CACHE_DIR }} - - name: Install Packer - env: - PACKER_VERSION: ${{ steps.setup-env.outputs.packer-version }} - run: | - PACKER_ZIP="packer_${PACKER_VERSION}_linux_amd64.zip" - curl --output ${{ env.CURL_CACHE_DIR }}/"${PACKER_ZIP}" \ - --time-cond ${{ env.CURL_CACHE_DIR }}/"${PACKER_ZIP}" \ - --location \ - "https://releases.hashicorp.com/packer/${PACKER_VERSION}/${PACKER_ZIP}" - sudo unzip -d /opt/packer \ - ${{ env.CURL_CACHE_DIR }}/"${PACKER_ZIP}" - sudo mv /usr/local/bin/packer /usr/local/bin/packer-default - sudo ln -s /opt/packer/packer /usr/local/bin/packer - - uses: hashicorp/setup-terraform@v1 + - uses: hashicorp/setup-packer@v3 + with: + version: ${{ steps.setup-env.outputs.packer-version }} + - uses: hashicorp/setup-terraform@v4 with: terraform_version: ${{ steps.setup-env.outputs.terraform-version }} - - name: Install shfmt + - name: Install go-critic + env: + PACKAGE_URL: github.com/go-critic/go-critic/cmd/go-critic + PACKAGE_VERSION: ${{ steps.setup-env.outputs.go-critic-version }} + run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} + - name: Install goimports + env: + PACKAGE_URL: golang.org/x/tools/cmd/goimports + PACKAGE_VERSION: ${{ steps.setup-env.outputs.goimports-version }} + run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} + - name: Install gosec env: - PACKAGE_URL: mvdan.cc/sh/v3/cmd/shfmt - PACKAGE_VERSION: ${{ steps.setup-env.outputs.shfmt-version }} + PACKAGE_URL: github.com/securego/gosec/v2/cmd/gosec + PACKAGE_VERSION: ${{ steps.setup-env.outputs.gosec-version }} run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} - - name: Install Terraform-docs + - name: Install staticcheck env: - PACKAGE_URL: github.com/terraform-docs/terraform-docs - PACKAGE_VERSION: ${{ steps.setup-env.outputs.terraform-docs-version }} + PACKAGE_URL: honnef.co/go/tools/cmd/staticcheck + PACKAGE_VERSION: ${{ steps.setup-env.outputs.staticcheck-version }} run: go install ${PACKAGE_URL}@${PACKAGE_VERSION} + # TODO: https://github.com/cisagov/skeleton-generic/issues/165 + # We are temporarily using a branch of @mcdonnnj's fork of terraform-docs that + # groups changes from his PRs until they are approved and merged: + # https://github.com/terraform-docs/terraform-docs/pull/745 + # https://github.com/terraform-docs/terraform-docs/pull/901 + # This temporary fix will allow for ATX header support when terraform-docs is run + # during linting and output delimiter rows with cell spacing that passes + # Markdownlint's MD060/table-column-style rule. + - name: Clone ATX headers branch from terraform-docs fork + run: | + git clone \ + --branch $TERRAFORM_DOCS_REPO_BRANCH_NAME \ + --depth $TERRAFORM_DOCS_REPO_DEPTH \ + --single-branch \ + $TERRAFORM_DOCS_REPO_URL /tmp/terraform-docs + - name: Build and install terraform-docs binary + run: | + go build \ + -C /tmp/terraform-docs \ + -o $(go env GOPATH)/bin/terraform-docs - name: Install dependencies run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip setuptools pip install --upgrade --requirement requirements-test.txt - name: Set up pre-commit hook environments run: pre-commit install-hooks @@ -106,36 +204,75 @@ jobs: - name: Setup tmate debug session uses: mxschmitt/action-tmate@v3 if: env.RUN_TMATE - test: - runs-on: ubuntu-latest + test-source: + name: test source - py${{ matrix.python-version }} - ${{ matrix.platform }} + needs: + - diagnostics + permissions: + # actions/checkout needs this to fetch code + contents: read + runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: + # We test on all of the latest platforms available to use with GitHub- + # hosted runners for public repositories. + platform: + - macos-latest + - ubuntu-24.04-arm + - ubuntu-latest + - windows-latest python-version: - - "3.6" - - "3.7" - - "3.8" - - "3.9" - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - uses: actions/checkout@v3 + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-python-library#149 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - uses: actions/checkout@v6 - id: setup-python - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v5 env: - BASE_CACHE_KEY: "${{ github.job }}-${{ runner.os }}-\ - py${{ steps.setup-python.outputs.python-version }}-" + BASE_CACHE_KEY: >- + ${{ github.job }}-${{ + runner.os }}-${{ runner.arch }}-py${{ + steps.setup-python.outputs.python-version }}- with: path: ${{ env.PIP_CACHE_DIR }} - # We do not use '**/setup.py' in the cache key so only the 'setup.py' - # file in the root of the repository is used. This is in case a Python - # package were to have a 'setup.py' as part of its internal codebase. - key: "${{ env.BASE_CACHE_KEY }}\ - ${{ hashFiles('**/requirements-test.txt') }}-\ - ${{ hashFiles('**/requirements.txt') }}-\ - ${{ hashFiles('setup.py') }}" + key: >- + ${{ env.BASE_CACHE_KEY }}${{ + hashFiles('**/requirements-test.txt') }}-${{ + hashFiles('**/requirements.txt') }}-${{ + hashFiles('pyproject.toml') }} restore-keys: | ${{ env.BASE_CACHE_KEY }} - name: Install dependencies @@ -147,94 +284,221 @@ jobs: RELEASE_TAG: ${{ github.event.release.tag_name }} run: pytest - name: Upload coverage report - run: coveralls - env: - COVERALLS_FLAG_NAME: "py${{ matrix.python-version }}" - COVERALLS_PARALLEL: true - COVERALLS_SERVICE_NAME: github - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: coverallsapp/github-action@v2 + with: + flag-name: py${{ matrix.python-version }} - ${{ matrix.platform }} + parallel: true if: success() - name: Setup tmate debug session uses: mxschmitt/action-tmate@v3 if: env.RUN_TMATE coveralls-finish: + permissions: + # actions/checkout needs this to fetch code + contents: read runs-on: ubuntu-latest - needs: test + needs: + - diagnostics + - test-source steps: - - uses: actions/checkout@v3 - - id: setup-python - uses: actions/setup-python@v3 + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 with: - python-version: "3.10" - - uses: actions/cache@v3 - env: - BASE_CACHE_KEY: "${{ github.job }}-${{ runner.os }}-\ - py${{ steps.setup-python.outputs.python-version }}-" - with: - path: ${{ env.PIP_CACHE_DIR }} - # We do not use '**/setup.py' in the cache key so only the 'setup.py' - # file in the root of the repository is used. This is in case a Python - # package were to have a 'setup.py' as part of its internal codebase. - key: "${{ env.BASE_CACHE_KEY }}\ - ${{ hashFiles('**/requirements-test.txt') }}-\ - ${{ hashFiles('**/requirements.txt') }}-\ - ${{ hashFiles('setup.py') }}" - restore-keys: | - ${{ env.BASE_CACHE_KEY }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade --requirement requirements-test.txt + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-python-library#149 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - uses: actions/checkout@v6 - name: Finished coveralls reports - run: coveralls --finish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true - name: Setup tmate debug session uses: mxschmitt/action-tmate@v3 if: env.RUN_TMATE - build: + build-wheel: + name: build wheel - py${{ matrix.python-version }} + needs: + - diagnostics + permissions: + # actions/checkout needs this to fetch code + contents: read runs-on: ubuntu-latest - needs: [lint, test] strategy: fail-fast: false matrix: python-version: - - "3.6" - - "3.7" - - "3.8" - - "3.9" - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - uses: actions/checkout@v3 + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-python-library#149 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - uses: actions/checkout@v6 - id: setup-python - uses: actions/setup-python@v3 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 + - uses: actions/cache@v5 env: - BASE_CACHE_KEY: "${{ github.job }}-${{ runner.os }}-\ - py${{ steps.setup-python.outputs.python-version }}-" + BASE_CACHE_KEY: >- + ${{ github.job }}-${{ runner.os }}-py${{ + steps.setup-python.outputs.python-version }}- with: path: ${{ env.PIP_CACHE_DIR }} - # We do not use '**/setup.py' in the cache key so only the 'setup.py' - # file in the root of the repository is used. This is in case a Python - # package were to have a 'setup.py' as part of its internal codebase. - key: "${{ env.BASE_CACHE_KEY }}\ - ${{ hashFiles('**/requirements.txt') }}-\ - ${{ hashFiles('setup.py') }}" + key: >- + ${{ env.BASE_CACHE_KEY }}${{ + hashFiles('**/requirements.txt') }}-${{ + hashFiles('pyproject.toml') }} restore-keys: | ${{ env.BASE_CACHE_KEY }} - name: Install build dependencies run: | - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip setuptools python -m pip install --upgrade build - name: Build artifacts run: python -m build - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v7 + with: + name: dist-${{ matrix.python-version }} + path: dist + - name: Setup tmate debug session + uses: mxschmitt/action-tmate@v3 + if: env.RUN_TMATE + test-wheel: + name: test built wheel - py${{ matrix.python-version }} - ${{ matrix.platform }} + needs: + - diagnostics + - build-wheel + - test-source + permissions: + # actions/checkout needs this to fetch code + contents: read + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + # We test on all of the latest platforms available to use with GitHub- + # hosted runners for public repositories. + platform: + - macos-latest + - ubuntu-24.04-arm + - ubuntu-latest + - windows-latest + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-python-library#149 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - uses: actions/checkout@v6 + - id: setup-python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v5 + env: + BASE_CACHE_KEY: >- + ${{ github.job }}-${{ + runner.os }}-${{ runner.arch }}-py${{ + steps.setup-python.outputs.python-version }}- + with: + path: ${{ env.PIP_CACHE_DIR }} + key: >- + ${{ env.BASE_CACHE_KEY }}${{ + hashFiles('**/requirements.txt') }}-${{ + hashFiles('pyproject.toml') }} + restore-keys: | + ${{ env.BASE_CACHE_KEY }} + - name: Retrieve the built wheel + uses: actions/download-artifact@v8 with: name: dist-${{ matrix.python-version }} path: dist + - id: find-wheel + name: Get the name of the retrieved wheel (there should only be one) + run: echo "wheel=$(ls dist/*whl)" >> $GITHUB_OUTPUT + - name: Update core Python packages + run: python -m pip install --upgrade pip setuptools + - name: Install the built wheel (along with testing dependencies) + run: python -m pip install ${{ steps.find-wheel.outputs.wheel }}[test] + - name: Run tests + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: pytest - name: Setup tmate debug session uses: mxschmitt/action-tmate@v3 if: env.RUN_TMATE diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5e344d1..cd672f6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -4,45 +4,123 @@ # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. -name: "CodeQL" +name: CodeQL +# The use of on here as a key is part of the GitHub actions syntax. +# yamllint disable-line rule:truthy on: + merge_group: + types: + - checks_requested + # We use the default activity types for the pull_request event as specified here: + # https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request + pull_request: + # The branches here must be a subset of the ones in the push key + branches: + - develop push: - # Dependabot triggered push events have read-only access, but uploading code + # Dependabot-triggered push events have read-only access, but uploading code # scanning requires write access. branches-ignore: - dependabot/** - pull_request: - # The branches below must be a subset of the branches above - branches: - - develop schedule: - - cron: '0 14 * * 6' + - cron: 0 14 * * 6 jobs: + diagnostics: + name: Run diagnostics + # This job does not need any permissions + permissions: {} + runs-on: ubuntu-latest + steps: + # Note that a duplicate of this step must be added at the top of + # each job. + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + check_github_status: "true" + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + output_workflow_context: "true" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} analyze: name: Analyze - runs-on: ubuntu-latest + needs: + - diagnostics permissions: + # actions/checkout needs this to fetch code + contents: read # required for all workflows security-events: write + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are go, javascript, csharp, python, cpp, and java + # Override automatic language detection by changing the below + # list + # + # Supported options are actions, c-cpp, csharp, go, + # java-kotlin, javascript-typescript, python, ruby, and swift. language: + - actions - python # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} @@ -50,7 +128,7 @@ jobs: # Java). If this step fails, then you should remove it and run the build # manually (see below). - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +142,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..2b71638 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,97 @@ +--- +name: Dependency review + +on: # yamllint disable-line rule:truthy + merge_group: + types: + - checks_requested + # We use the default activity types for the pull_request event as specified here: + # https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request + pull_request: + +# Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, +# nounset, errexit, and pipefail. The `-x` will print all commands as they are +# run. Please see the GitHub Actions documentation for more information: +# https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs +defaults: + run: + shell: bash -Eueo pipefail -x {0} + +jobs: + diagnostics: + name: Run diagnostics + # This job does not need any permissions + permissions: {} + runs-on: ubuntu-latest + steps: + # Note that a duplicate of this step must be added at the top of + # each job. + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + check_github_status: "true" + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + output_workflow_context: "true" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + dependency-review: + name: Dependency review + needs: + - diagnostics + permissions: + # actions/checkout needs this to fetch code + contents: read + runs-on: ubuntu-latest + steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - id: checkout-repo + name: Checkout the repository + uses: actions/checkout@v6 + - id: dependency-review + name: Review dependency changes for vulnerabilities and license changes + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/label-prs.yml b/.github/workflows/label-prs.yml new file mode 100644 index 0000000..45d317b --- /dev/null +++ b/.github/workflows/label-prs.yml @@ -0,0 +1,90 @@ +--- +name: Label pull requests + +on: # yamllint disable-line rule:truthy + # We use the default activity types for the pull_request event as specified here: + # https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request + pull_request: + +# Set a default shell for any run steps. The `-Eueo pipefail` sets errtrace, +# nounset, errexit, and pipefail. The `-x` will print all commands as they are +# run. Please see the GitHub Actions documentation for more information: +# https://docs.github.com/en/actions/using-jobs/setting-default-values-for-jobs +defaults: + run: + shell: bash -Eueo pipefail -x {0} + +jobs: + diagnostics: + name: Run diagnostics + # This job does not need any permissions + permissions: {} + runs-on: ubuntu-latest + steps: + # Note that a duplicate of this step must be added at the top of + # each job. + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + check_github_status: "true" + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + output_workflow_context: "true" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + label: + needs: + - diagnostics + permissions: + # Permissions required by actions/labeler + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - name: Apply suitable labels to a pull request + uses: actions/labeler@v6 diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..a8d01be --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,94 @@ +--- +name: sync-labels + +on: # yamllint disable-line rule:truthy + push: + paths: + - .github/labels.yml + - .github/workflows/sync-labels.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + diagnostics: + name: Run diagnostics + # This job does not need any permissions + permissions: {} + runs-on: ubuntu-latest + steps: + # Note that a duplicate of this step must be added at the top of + # each job. + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + check_github_status: "true" + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + output_workflow_context: "true" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + labeler: + needs: + - diagnostics + permissions: + # actions/checkout needs this to fetch code + contents: read + # crazy-max/ghaction-github-labeler needs this to manage repository labels + issues: write + runs-on: ubuntu-latest + steps: + - name: Apply standard cisagov job preamble + uses: cisagov/action-job-preamble@v1 + with: + # This functionality is poorly implemented and has been + # causing problems due to the MITM implementation hogging or + # leaking memory. As a result we disable it by default. If + # you want to temporarily enable it, simply set + # monitor_permissions equal to "true". + # + # TODO: Re-enable this functionality when practical. See + # cisagov/skeleton-generic#207 for more details. + monitor_permissions: "false" + # Use a variable to specify the permissions monitoring + # configuration. By default this will yield the + # configuration stored in the cisagov organization-level + # variable, but if you want to use a different configuration + # then simply: + # 1. Create a repository-level variable with the name + # ACTIONS_PERMISSIONS_CONFIG. + # 2. Set this new variable's value to the configuration you + # want to use for this repository. + # + # Note in particular that changing the permissions + # monitoring configuration *does not* require you to modify + # this workflow. + permissions_monitoring_config: ${{ vars.ACTIONS_PERMISSIONS_CONFIG }} + - uses: actions/checkout@v6 + - name: Sync repository labels + if: success() + uses: crazy-max/ghaction-github-labeler@v6 + with: + # This is a hideous ternary equivalent so we only do a dry run unless + # this workflow is triggered by the develop branch. + dry-run: ${{ github.ref_name == 'develop' && 'false' || 'true' }} diff --git a/.gitignore b/.gitignore index 242b4aa..579601b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ __pycache__ .pytest_cache .python-version *.egg-info +build dist diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 46d45f3..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[settings] -combine_star=true -force_sort_within_sections=true - -import_heading_stdlib=Standard Python Libraries -import_heading_thirdparty=Third-Party Libraries -import_heading_firstparty=cisagov Libraries - -# Run isort under the black profile to align with our other Python linting -profile=black diff --git a/.mdl_config.yaml b/.mdl_config.yaml index 4a650c1..1b48994 100644 --- a/.mdl_config.yaml +++ b/.mdl_config.yaml @@ -6,12 +6,12 @@ default: true # MD003/heading-style/header-style - Heading style MD003: # Enforce the ATX-closed style of header - style: "atx_closed" + style: atx_closed # MD004/ul-style - Unordered list style MD004: # Enforce dashes for unordered lists - style: "dash" + style: dash # MD013/line-length - Line length MD013: @@ -30,7 +30,7 @@ MD024: # MD029/ol-prefix - Ordered list item prefix MD029: # Enforce the `1.` style for ordered lists - style: "one" + style: one # MD033/no-inline-html - Inline HTML MD033: @@ -42,19 +42,19 @@ MD033: # MD035/hr-style - Horizontal rule style MD035: # Enforce dashes for horizontal rules - style: "---" + style: --- # MD046/code-block-style - Code block style MD046: # Enforce the fenced style for code blocks - style: "fenced" + style: fenced # MD049/emphasis-style - Emphasis style should be consistent MD049: # Enforce asterisks as the style to use for emphasis - style: "asterisk" + style: asterisk # MD050/strong-style - Strong style should be consistent MD050: # Enforce asterisks as the style to use for strong - style: "asterisk" + style: asterisk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 955e9c8..816fa90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,39 @@ --- +ci: + # Do not commit changes from running pre-commit for pull requests. + autofix_prs: false + # Autoupdate hooks weekly (this is the default). + autoupdate_schedule: weekly + default_language_version: # force all unspecified python hooks to run python3 python: python3 repos: + # Check the pre-commit configuration + - repo: meta + hooks: + - id: check-useless-excludes + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v6.0.0 hooks: - id: check-case-conflict - id: check-executables-have-shebangs - id: check-json - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks - id: check-toml + - id: check-vcs-permalinks - id: check-xml - id: debug-statements + - id: destroyed-symlinks - id: detect-aws-credentials args: - --allow-missing-credentials - id: detect-private-key - id: end-of-file-fixer - exclude: files/(issue|motd) - id: mixed-line-ending args: - --fix=lf @@ -31,17 +45,17 @@ repos: # Text file hooks - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.31.1 + rev: v0.48.0 hooks: - id: markdownlint args: - --config=.mdl_config.yaml - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.1 + - repo: https://github.com/rbubley/mirrors-prettier + rev: v3.8.1 hooks: - id: prettier - repo: https://github.com/adrienverge/yamllint - rev: v1.26.3 + rev: v1.38.0 hooks: - id: yamllint args: @@ -49,102 +63,214 @@ repos: # GitHub Actions hooks - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.14.2 + rev: 0.37.0 hooks: - id: check-github-actions - id: check-github-workflows # pre-commit hooks - repo: https://github.com/pre-commit/pre-commit - rev: v2.17.0 + rev: v4.5.1 hooks: - id: validate_manifest + # Go hooks + - repo: https://github.com/TekWizely/pre-commit-golang + rev: v1.0.0-rc.4 + hooks: + # Go Build + - id: go-build-repo-mod + # Style Checkers + - id: go-critic + # goimports + - id: go-imports-repo + args: + # Write changes to files + - -w + # Go Mod Tidy + - id: go-mod-tidy-repo + # GoSec + - id: go-sec-repo-mod + # StaticCheck + - id: go-staticcheck-repo-mod + # Go Test + - id: go-test-repo-mod + # Go Vet + - id: go-vet-repo-mod + # Nix hooks + - repo: https://github.com/nix-community/nixpkgs-fmt + rev: v1.3.0 + hooks: + - id: nixpkgs-fmt + # Shell script hooks - - repo: https://github.com/cisagov/pre-commit-shfmt - rev: v0.0.2 + - repo: https://github.com/scop/pre-commit-shfmt + rev: v3.13.0-1 hooks: - id: shfmt args: + # List files that will be formatted + - --list + # Write result to file instead of stdout + - --write # Indent by two spaces - - -i - - '2' + - --indent + - "2" # Binary operators may start a line - - -bn + - --binary-next-line # Switch cases are indented - - -ci + - --case-indent # Redirect operators are followed by a space - - -sr - - repo: https://github.com/detailyang/pre-commit-shell - rev: 1.0.5 + - --space-redirects + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 hooks: - - id: shell-lint + - id: shellcheck # Python hooks - # Run bandit on "tests" tree with a configuration + # Run bandit on the "tests" tree - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.9.4 hooks: - id: bandit name: bandit (tests tree) files: tests args: - - --config=.bandit.yml - # Run bandit everything but tests directory + # Skip "assert used" check since assertions are used + # frequently in pytests. + - --skip=B101 + # Run bandit on everything except the "tests" tree - repo: https://github.com/PyCQA/bandit - rev: 1.7.0 + rev: 1.9.4 hooks: - id: bandit name: bandit (everything else) exclude: tests - - repo: https://github.com/psf/black - rev: 22.3.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 26.3.1 hooks: - id: black - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings + - dlint==0.16.0 + - flake8-bugbear==25.11.29 + - flake8-comprehensions==3.17.0 + - flake8-docstrings==1.7.0 + - flake8-noqa==1.5.0 + # This is necessary to read the flake8 configuration from + # the pyproject.toml file. + - flake8-pyproject==1.2.3 + - pep8-naming==0.15.1 - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 8.0.1 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.942 + rev: v1.19.1 hooks: - id: mypy + # IMPORTANT: Keep type hinting-related dependencies of the + # mypy pre-commit hook additional_dependencies in sync with + # the dev section of pyproject.toml to avoid discrepancies in + # type checking between environments. additional_dependencies: - - types-setuptools + - types-docopt + - repo: https://github.com/pypa/pip-audit + rev: v2.10.0 + hooks: + - id: pip-audit + args: + # We have to ignore this vulnerability for now since an + # update for pygments has not yet been released. + # + # In any event, this vulnerability is unlikely to cause us + # any problems since we don't feed any regexes to pygments + # directly. pygments is pulled in as a dependency of + # pytest. + # + # See also: + # - https://nvd.nist.gov/vuln/detail/CVE-2026-4539 + # - https://github.com/pygments/pygments/issues/3058 + # + # TODO: Remove this when it becomes possible. See + # cisagov/skeleton-generic#257 for more details. + - --ignore-vuln + - CVE-2026-4539 + # Add any pip requirements files to scan + - --requirement + - requirements-dev.txt + - --requirement + - requirements-test.txt + - --requirement + - requirements.txt - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v3.21.2 hooks: - id: pyupgrade + args: + # Python 3.10 is currently the oldest non-EOL version of + # Python, so we want to apply all rules that apply to this + # version or later. See here for more details: + # https://www.gyford.com/phil/writing/2025/08/26/how-to-use-pyupgrade/ + - --py310-plus # Ansible hooks - - repo: https://github.com/ansible-community/ansible-lint - rev: v5.4.0 + - repo: https://github.com/ansible/ansible-lint + # We need to stay on this version because we are still using Python 3.13 in + # our GitHub Actions configuration. Later versions require Python 3.14 for + # the hook to run. + rev: v26.1.1 hooks: - id: ansible-lint - # files: molecule/default/playbook.yml + additional_dependencies: + # On its own ansible-lint does not pull in ansible, only + # ansible-core. Therefore, if an Ansible module lives in + # ansible instead of ansible-core, the linter will complain + # that the module is unknown. In these cases it is + # necessary to add the ansible package itself as an + # additional dependency, with the same pinning as is done in + # requirements-test.txt of cisagov/skeleton-ansible-role. + # + # Version 10 is required because the pip-audit pre-commit + # hook identifies a vulnerability in ansible-core 2.16.13, + # but all versions of ansible 9 have a dependency on + # ~=2.16.X. + # - ansible>=10,<11 + # ansible-core<2.17.7 suffers from GHSA-99w6-3xph-cx78. + # + # Note that any changes made to this dependency must also be + # made in requirements.txt in cisagov/skeleton-packer and + # requirements-test.txt in cisagov/skeleton-ansible-role. + - ansible-core>=2.17.7 # Terraform hooks - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.64.0 + rev: v1.105.0 hooks: - id: terraform_fmt - id: terraform_validate + # This needs to run after the terraform_validate hook so that any Terraform + # configurations are initialized. + - id: terraform_providers_lock + args: + - --args=-platform=darwin_amd64 + - --args=-platform=darwin_arm64 + - --args=-platform=linux_amd64 + - --args=-platform=linux_arm64 + - --hook-config=--mode=always-regenerate-lockfile # Docker hooks - repo: https://github.com/IamTheFij/docker-pre-commit - rev: v2.1.0 + rev: v3.0.1 hooks: - id: docker-compose-check # Packer hooks - repo: https://github.com/cisagov/pre-commit-packer - rev: v0.0.2 + rev: v0.3.1 hooks: - - id: packer_validate - id: packer_fmt + - id: packer_validate diff --git a/.yamllint b/.yamllint index 76a1cce..00e85a6 100644 --- a/.yamllint +++ b/.yamllint @@ -2,12 +2,61 @@ extends: default rules: + braces: + # Do not allow non-empty flow mappings + forbid: non-empty + # Allow up to one space inside braces. This is required for Ansible compatibility. + max-spaces-inside: 1 + + brackets: + # Do not allow non-empty flow sequences + forbid: non-empty + + comments: + # Ensure that inline comments have at least one space before the preceding content. + # This is required for Ansible compatibility. + min-spaces-from-content: 1 + # yamllint does not like it when you comment out different parts of # dictionaries in a list. You can see # https://github.com/adrienverge/yamllint/issues/384 for some examples of # this behavior. comments-indentation: disable - # yamllint doesn't like when we use yes and no for true and false, - # but that's pretty standard in Ansible. - truthy: disable + indentation: + # Ensure that block sequences inside of a mapping are indented + indent-sequences: true + # Enforce a specific number of spaces + spaces: 2 + + # yamllint does not allow inline mappings that exceed the line length by + # default. There are many scenarios where the inline mapping may be a key, + # hash, or other long value that would exceed the line length but cannot + # reasonably be broken across lines. + line-length: + # This rule implies the allow-non-breakable-words rule + allow-non-breakable-inline-mappings: true + # Allows a 10% overage from the default limit of 80 + max: 88 + + # Using anything other than strings to express octal values can lead to unexpected + # and potentially unsafe behavior. Ansible strongly recommends against such practices + # and these rules are needed for Ansible compatibility. Please see the following for + # more information: + # https://ansible.readthedocs.io/projects/lint/rules/risky-octal/ + octal-values: + # Do not allow explicit octal values (those beginning with a leading 0o). + forbid-explicit-octal: true + # Do not allow implicit octal values (those beginning with a leading 0). + forbid-implicit-octal: true + + quoted-strings: + # Allow disallowed quotes (single quotes) for strings that contain allowed quotes + # (double quotes). + allow-quoted-quotes: true + # Apply these rules to keys in mappings as well + check-keys: true + # We prefer double quotes for strings when they are needed + quote-type: double + # Only require quotes when they are necessary for proper processing + required: only-when-needed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2315b7..6c2a208 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,9 +46,13 @@ There are a few ways to do this, but we prefer to use create and manage a Python virtual environment specific to this project. -If you already have `pyenv` and `pyenv-virtualenv` configured you can -take advantage of the `setup-env` tool in this repo to automate the -entire environment configuration process. +We recommend using the `setup-env` script located in this repository, +as it automates the entire environment configuration process. The +dependencies required to run this script are +[GNU `getopt`](https://github.com/util-linux/util-linux/blob/master/misc-utils/getopt.1.adoc), +[`pyenv`](https://github.com/pyenv/pyenv), and [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv). +If these tools are already configured on your system, you can simply run the +following command: ```console ./setup-env @@ -57,13 +61,18 @@ entire environment configuration process. Otherwise, follow the steps below to manually configure your environment. -#### Installing and using `pyenv` and `pyenv-virtualenv` #### +#### Installing and using GNU `getopt`, `pyenv`, and `pyenv-virtualenv` #### -On the Mac, we recommend installing [brew](https://brew.sh/). Then -installation is as simple as `brew install pyenv pyenv-virtualenv` and +On macOS, we recommend installing [brew](https://brew.sh/). Then +installation is as simple as `brew install gnu-getopt pyenv pyenv-virtualenv` and adding this to your profile: ```bash +# GNU getopt must be explicitly added to the path since it is +# keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean) +export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH" + +# Setup pyenv export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init --path)" @@ -71,13 +80,15 @@ eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" ``` -For Linux, Windows Subsystem for Linux (WSL), or on the Mac (if you +For Linux, Windows Subsystem for Linux (WSL), or macOS (if you don't want to use `brew`) you can use [pyenv/pyenv-installer](https://github.com/pyenv/pyenv-installer) to install the necessary tools. Before running this ensure that you have installed the prerequisites for your platform according to the [`pyenv` wiki page](https://github.com/pyenv/pyenv/wiki/common-build-problems). +GNU `getopt` is included in most Linux distributions as part of the +[`util-linux`](https://github.com/util-linux/util-linux) package. On WSL you should treat your platform as whatever Linux distribution you've chosen to install. @@ -121,11 +132,10 @@ you can begin to use `pyenv`. For a list of Python versions that are already installed and ready to use with `pyenv`, use the command `pyenv versions`. To see a list of the Python versions available to be installed and used with `pyenv` -use the command `pyenv install --list`. You can read more -[here](https://github.com/pyenv/pyenv/blob/master/COMMANDS.md) about -the many things that `pyenv` can do. See -[here](https://github.com/pyenv/pyenv-virtualenv#usage) for the -additional capabilities that pyenv-virtualenv adds to the `pyenv` +use the command `pyenv install --list`. You can read more about +the [many things that `pyenv` can do](https://github.com/pyenv/pyenv/blob/master/COMMANDS.md). +See the [usage information](https://github.com/pyenv/pyenv-virtualenv#usage) +for the additional capabilities that pyenv-virtualenv adds to the `pyenv` command. #### Creating the Python virtual environment #### diff --git a/README.md b/README.md index be25df4..5555961 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # findcdn # +<<<<<<< HEAD [![GitHub Build Status](https://github.com/cisagov/findcdn/workflows/build/badge.svg)](https://github.com/cisagov/findcdn/actions) [![Coverage Status](https://coveralls.io/repos/github/cisagov/findcdn/badge.svg?branch=develop)](https://coveralls.io/github/cisagov/findcdn?branch=develop) [![Total alerts](https://img.shields.io/lgtm/alerts/g/cisagov/findcdn.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/cisagov/findcdn/alerts/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/cisagov/findcdn.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/cisagov/findcdn/context:python) [![Known Vulnerabilities](https://snyk.io/test/github/cisagov/findcdn/develop/badge.svg)](https://snyk.io/test/github/cisagov/findcdn) +======= +[![GitHub Build Status](https://github.com/cisagov/skeleton-python-library/workflows/build/badge.svg)](https://github.com/cisagov/skeleton-python-library/actions) +[![License](https://img.shields.io/github/license/cisagov/skeleton-python-library)](https://spdx.org/licenses/) +[![CodeQL](https://github.com/cisagov/skeleton-python-library/workflows/CodeQL/badge.svg)](https://github.com/cisagov/skeleton-python-library/actions/workflows/codeql-analysis.yml) +[![Coverage Status](https://coveralls.io/repos/github/cisagov/skeleton-python-library/badge.svg?branch=develop)](https://coveralls.io/github/cisagov/skeleton-python-library?branch=develop) +<<<<<<< HEAD +<<<<<<< HEAD +[![Known Vulnerabilities](https://snyk.io/test/github/cisagov/skeleton-python-library/develop/badge.svg)](https://snyk.io/test/github/cisagov/skeleton-python-library) +>>>>>>> 65604c7e134491f497f612f6eaebded8f411bec5 +======= +>>>>>>> cf0d937626ca5213720b7dd074735bc960d174b0 +======= +[![Code Style](https://img.shields.io/badge/Code%20Style-black-black)](https://github.com/psf/black) +>>>>>>> d4df8360bbcbc12ae68b0ad011f72a442b1040b8 `findcdn`, is a tool that can scan and detect the kind of [Content Distribution Network (CDN)](https://en.wikipedia.org/wiki/Content_delivery_network) diff --git a/bump-version b/bump-version new file mode 100755 index 0000000..4fc1452 --- /dev/null +++ b/bump-version @@ -0,0 +1,172 @@ +#!/usr/bin/env bash + +# bump-version [--push] [--label LABEL] (major | minor | patch | prerelease | build | finalize | show) +# bump-version --list-files + +set -o nounset +set -o errexit +set -o pipefail + +# Stores the canonical version for the project. +VERSION_FILE=src/example/_version.py +# Files that should be updated with the new version. +VERSION_FILES=("$VERSION_FILE") + +USAGE=$( + cat << END_OF_LINE +Update the version of the project. + +Usage: + ${0##*/} [--push] [--label LABEL] (major | minor | patch | prerelease | build | finalize | show) + ${0##*/} --list-files + ${0##*/} (-h | --help) + +Options: + -h | --help Show this message. + --push Perform a \`git push\` after updating the version. + --label LABEL Specify the label to use when updating the build or prerelease version. + --list-files List the files that will be updated when the version is bumped. +END_OF_LINE +) + +old_version=$(sed -n "s/^__version__ = \"\(.*\)\"$/\1/p" $VERSION_FILE) +# Comment out periods so they are interpreted as periods and don't +# just match any character +old_version_regex=${old_version//\./\\\.} +new_version="$old_version" + +bump_part="" +label="" +commit_prefix="Bump" +with_push=false +commands_with_label=("build" "prerelease") +commands_with_prerelease=("major" "minor" "patch") +with_prerelease=false + +####################################### +# Display an error message, the help information, and exit with a non-zero status. +# Arguments: +# Error message. +####################################### +function invalid_option() { + echo "$1" + echo "$USAGE" + exit 1 +} + +####################################### +# Bump the version using the provided command. +# Arguments: +# The version to bump. +# The command to bump the version. +# Returns: +# The new version. +####################################### +function bump_version() { + local temp_version + temp_version=$(python -c "import semver; print(semver.parse_version_info('$1').${2})") + echo "$temp_version" +} + +if [ $# -eq 0 ]; then + echo "$USAGE" + exit 1 +else + while [ $# -gt 0 ]; do + case $1 in + --push) + if [ "$with_push" = true ]; then + invalid_option "Push has already been set." + fi + + with_push=true + shift + ;; + --label) + if [ -n "$label" ]; then + invalid_option "Label has already been set." + fi + + label="$2" + shift 2 + ;; + build | finalize | major | minor | patch) + if [ -n "$bump_part" ]; then + invalid_option "Only one version part should be bumped at a time." + fi + + bump_part="$1" + shift + ;; + prerelease) + with_prerelease=true + shift + ;; + show) + echo "$old_version" + exit 0 + ;; + -h | --help) + echo "$USAGE" + exit 0 + ;; + --list-files) + printf '%s\n' "${VERSION_FILES[@]}" + exit 0 + ;; + *) + invalid_option "Invalid option: $1" + ;; + esac + done +fi + +if [ -n "$label" ] && [ "$with_prerelease" = false ] && [[ ! " ${commands_with_label[*]} " =~ [[:space:]]${bump_part}[[:space:]] ]]; then + invalid_option "Setting the label is only allowed for the following commands: ${commands_with_label[*]}" +fi + +if [ "$with_prerelease" = true ] && [ -n "$bump_part" ] && [[ ! " ${commands_with_prerelease[*]} " =~ [[:space:]]${bump_part}[[:space:]] ]]; then + invalid_option "Changing the prerelease is only allowed in conjunction with the following commands: ${commands_with_prerelease[*]}" +fi + +label_option="" +if [ -n "$label" ]; then + label_option="token='$label'" +fi + +if [ -n "$bump_part" ]; then + if [ "$bump_part" = "finalize" ]; then + commit_prefix="Finalize" + bump_command="finalize_version()" + elif [ "$bump_part" = "build" ]; then + bump_command="bump_${bump_part}($label_option)" + else + bump_command="bump_${bump_part}()" + fi + new_version=$(bump_version "$old_version" "$bump_command") + echo Changing version from "$old_version" to "$new_version" +fi + +if [ "$with_prerelease" = true ]; then + bump_command="bump_prerelease($label_option)" + temp_version=$(bump_version "$new_version" "$bump_command") + echo Changing version from "$new_version" to "$temp_version" + new_version="$temp_version" +fi + +tmp_file=/tmp/version.$$ +for version_file in "${VERSION_FILES[@]}"; do + if [ ! -f "$version_file" ]; then + echo Missing expected file: "$version_file" + exit 1 + fi + sed "s/$old_version_regex/$new_version/" "$version_file" > $tmp_file + mv $tmp_file "$version_file" +done + +git add "${VERSION_FILES[@]}" +git commit --message "$commit_prefix version from $old_version to $new_version" + +if [ "$with_push" = true ]; then + git push +fi diff --git a/bump_version.sh b/bump_version.sh index 28d9169..73e7e24 100755 --- a/bump_version.sh +++ b/bump_version.sh @@ -11,6 +11,9 @@ VERSION_FILE=src/findcdn/_version.py HELP_INFORMATION="bump_version.sh (show|major|minor|patch|prerelease|build|finalize)" old_version=$(sed -n "s/^__version__ = \"\(.*\)\"$/\1/p" $VERSION_FILE) +# Comment out periods so they are interpreted as periods and don't +# just match any character +old_version_regex=${old_version//\./\\\.} if [ $# -ne 1 ]; then echo "$HELP_INFORMATION" @@ -22,7 +25,7 @@ else # A temp file is used to provide compatability with macOS development # as a result of macOS using the BSD version of sed tmp_file=/tmp/version.$$ - sed "s/$old_version/$new_version/" $VERSION_FILE > $tmp_file + sed "s/$old_version_regex/$new_version/" $VERSION_FILE > $tmp_file mv $tmp_file $VERSION_FILE git add $VERSION_FILE git commit -m"Bump version from $old_version to $new_version" @@ -34,10 +37,10 @@ else # A temp file is used to provide compatability with macOS development # as a result of macOS using the BSD version of sed tmp_file=/tmp/version.$$ - sed "s/$old_version/$new_version/" $VERSION_FILE > $tmp_file + sed "s/$old_version_regex/$new_version/" $VERSION_FILE > $tmp_file mv $tmp_file $VERSION_FILE git add $VERSION_FILE - git commit -m"Bump version from $old_version to $new_version" + git commit -m"Finalize version from $old_version to $new_version" git push ;; show) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..705b249 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,148 @@ +# For more information about configuring project metadata for the +# setuptools build backend, please see +# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + # 61.0.0 was the first version of setuptools to offer a full-fledged + # backend that uses pyproject.toml for metadata configuration (in + # compliance with PEP 621): + # https://setuptools.pypa.io/en/stable/history.html#v61-0-0 + # + # 77.0.0 was the first version of setuptools to support license + # expressions (in compliance with PEP 639): + # https://setuptools.pypa.io/en/stable/history.html#v77-0-0 + "setuptools>=77.0.0" +] + +[project] +authors = [ + { name = "Cybersecurity and Infrastructure Security Agency", email = "github@cisa.dhs.gov" } +] +# See https://pypi.python.org/pypi?%3Aaction=list_classifiers +classifiers = [ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 3 - Alpha", + "Environment :: Console", + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", +] +dependencies = [ + "docopt", + "schema", +] +description = "Example Python library" +dynamic = ["readme", "version"] +keywords = ["skeleton"] +license = "CC0-1.0" +name = "example" +requires-python = ">=3.10" + +[project.optional-dependencies] +# IMPORTANT: Keep type hinting-related dependencies of the dev section +# in sync with the mypy pre-commit hook configuration (see +# .pre-commit-config.yaml). Any changes to type hinting-related +# dependencies here should be reflected in the additional_dependencies +# field of the mypy pre-commit hook to avoid discrepancies in type +# checking between environments. +dev = [ + "build", + "twine", + "types-docopt", +] +test = [ + "coverage", + "coveralls", + "pre-commit", + "pytest-cov", + "pytest", +] + +[project.scripts] +example = "example.example:main" + +[project.urls] +homepage = "https://github.com/cisagov/skeleton-python-library" +issues = "https://github.com/cisagov/skeleton-python-library/issues" +# Landing page for CISA's cybersecurity mission +mission = "https://www.cisa.gov/cybersecurity" +source = "https://github.com/cisagov/skeleton-python-library" + +[tool.flake8] +max-line-length = 80 +# Select (turn on) +# * C: Complexity violations reported by mccabe - +# https://flake8.pycqa.org/en/latest/user/error-codes.html#error-violation-codes +# * C4: Default errors and warnings reported by flake8-comprehensions - +# https://github.com/adamchainz/flake8-comprehensions#rules +# * D: Documentation conventions compliance reported by pydocstyle - +# https://github.com/PyCQA/pydocstyle/blob/master/docs/error_codes.rst +# * DUO: Default errors and warnings reported by dlint - +# https://github.com/dlint-py/dlint/tree/master/docs +# * E: Default errors reported by pycodestyle - +# https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes +# * F: Default errors reported by pyflakes - +# https://flake8.pycqa.org/en/latest/glossary.html#term-pyflakes +# * N: Default errors and warnings reported by pep8-naming - +# https://github.com/PyCQA/pep8-naming#error-codes +# * NQA: Default errors and warnings reported by flake8-noqa - +# https://github.com/plinss/flake8-noqa#error-codes +# * W: Default warnings reported by pycodestyle - +# https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes +# * B: Default warnings reported by flake8-bugbear - +# https://github.com/PyCQA/flake8-bugbear#list-of-warnings +# * B950: Bugbear opinionated warning for line too long - +# https://github.com/PyCQA/flake8-bugbear#opinionated-warnings +select = ["C", "C4", "D", "DUO", "E", "F", "N", "NQA", "W", "B", "B950"] +# Ignore +# * E203: pycodestyle's default warning about whitespace before ':' because Black enforces +# an equal amount of whitespace around slice operators (':'). +# * E501: pycodestyle's default warning about maximum line length, which has a hard stop +# at the configured value. Instead we use flake8-bugbear's B950, which +# allows up to 10% overage. +# * W503: pycodestyle's warning about line breaks before binary operators. It no longer +# agrees with PEP8. See, for example, here: +# https://github.com/ambv/black/issues/21 +# Guido agrees here: +# https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b +extend-ignore = ["E203", "E501", "W503"] + +[tool.isort] +combine_star = true +force_sort_within_sections = true + +import_heading_stdlib = "Standard Python Libraries" +import_heading_thirdparty = "Third-Party Libraries" +import_heading_firstparty = "cisagov Libraries" + +# Run isort under the black profile to align with our other Python +# linting +profile = "black" + +[tool.pytest.ini_options] +# Increase verbosity, display extra test summary info for tests that +# did not pass, display code coverage results, and enable debug +# logging. +addopts = "--verbose -ra --cov --log-cli-level=DEBUG" + +[tool.setuptools.dynamic] +readme = {file = ["README.md"], content-type = "text/markdown"} +version = {attr = "example._version.__version__"} + +[tool.setuptools.package-data] +example = ["data/*.txt"] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index ed958e0..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = -v -ra --cov diff --git a/requirements-dev.txt b/requirements-dev.txt index 1d7e302..1a6ea6a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,6 @@ +--editable .[dev] --requirement requirements-test.txt ipython mypy -semver +# The bump-version script requires at least version 3 of semver. +semver>=3 diff --git a/requirements.txt b/requirements.txt index 8b75fe9..75ae487 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -# Note: Add any additional requirements to setup.py's install_requires field +# Note: Add any additional requirements to pyproject.toml's +# dependencies entry --editable . -wheel diff --git a/setup-env b/setup-env index 3392f60..bd8d5e2 100755 --- a/setup-env +++ b/setup-env @@ -9,68 +9,131 @@ USAGE=$( Configure a development environment for this repository. It does the following: + - Allows the user to specify the Python version to use for the virtual environment. + - Allows the user to specify a name for the virtual environment. - Verifies pyenv and pyenv-virtualenv are installed. - - Creates a Python virtual environment. + - Creates the Python virtual environment. - Configures the activation of the virtual enviroment for the repo directory. - Installs the requirements needed for development (including mypy type stubs). - Installs git pre-commit hooks. - - Configures git upstream remote "lineage" repositories. + - Configures git remotes for upstream "lineage" repositories. Usage: - setup-env [options] [virt_env_name] + setup-env [--venv-name venv_name] [--python-version python_version] setup-env (-h | --help) Options: - -f --force Delete virtual enviroment if it already exists. - -h --help Show this message. - -i --install-hooks Install hook environments for all environments in the - pre-commit config file. + -f | --force Delete virtual enviroment if it already exists. + -h | --help Show this message. + -i | --install-hooks Install hook environments for all environments in the + pre-commit config file. + -l | --list-versions List available Python versions and select one interactively. + -v | --venv-name Specify the name of the virtual environment. + -p | --python-version Specify the Python version for the virtual environment. END_OF_LINE ) +# Display pyenv's installed Python versions +python_versions() { + pyenv versions --bare --skip-aliases --skip-envs +} + +check_python_version() { + local version=$1 + + # This is a valid regex for semantically correct Python version strings. + # For more information see here: + # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + # Break down the regex into readable parts major.minor.patch + local major="0|[1-9]\d*" + local minor="0|[1-9]\d*" + local patch="0|[1-9]\d*" + + # Splitting the prerelease part for readability + # Start of the prerelease + local prerelease="(?:-" + # Numeric or alphanumeric identifiers + local prerelease+="(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)" + # Additional dot-separated identifiers + local prerelease+="(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*" + # End of the prerelease, making it optional + local prerelease+=")?" + # Optional build metadata + local build="(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?" + + # Final regex composed of parts + local regex="^($major)\.($minor)\.($patch)$prerelease$build$" + + # This checks if the Python version does not match the regex pattern specified in $regex, + # using Perl for regex matching. If the pattern is not found, then prompt the user with + # the invalid version message. + if ! echo "$version" | perl -ne "exit(!/$regex/)"; then + echo "Invalid version of Python: Python follows semantic versioning," \ + "so any version string that is not a valid semantic version is an" \ + "invalid version of Python." + exit 1 + # Else if the Python version isn't installed then notify the user. + # grep -E is used for searching through text lines that match the + # specific version. + elif ! python_versions | grep -E "^${version}$" > /dev/null; then + echo "Error: Python version $version is not installed." + echo "Installed Python versions are:" + python_versions + exit 1 + else + echo "Using Python version $version" + fi +} + # Flag to force deletion and creation of virtual environment FORCE=0 -# Positional parameters -PARAMS="" +# Initialize the other flags +INSTALL_HOOKS=0 +LIST_VERSIONS=0 +PYTHON_VERSION="" +VENV_NAME="" -# Parse command line arguments -while (("$#")); do - case "$1" in - -f | --force) - FORCE=1 - shift - ;; - -h | --help) - echo "${USAGE}" - exit 0 - ;; - -i | --install-hooks) - INSTALL_HOOKS=1 - shift - ;; - -*) # unsupported flags - echo "Error: Unsupported flag $1" >&2 - exit 1 - ;; - *) # preserve positional arguments - PARAMS="$PARAMS $1" - shift - ;; - esac -done +# Define long options +LONGOPTS="force,help,install-hooks,list-versions,python-version:,venv-name:" + +# Define short options for getopt +SHORTOPTS="fhilp:v:" -# set positional arguments in their proper place -eval set -- "$PARAMS" +# Check for GNU getopt by testing for long option support. GNU getopt supports +# the "--test" option and will return exit code 4 while POSIX/BSD getopt does +# not and will return exit code 0. +if getopt --test > /dev/null 2>&1; then + cat << 'END_OF_LINE' + + Please note, this script requires GNU getopt due to its enhanced + functionality and compatibility with certain script features that + are not supported by the POSIX getopt found in some systems, particularly + those with a non-GNU version of getopt. This distinction is crucial + as a system might have a non-GNU version of getopt installed by default, + which could lead to unexpected behavior. + + On macOS, we recommend installing brew (https://brew.sh/). Then installation + is as simple as `brew install gnu-getopt` and adding this to your + profile: + + export PATH="$(brew --prefix)/opt/gnu-getopt/bin:$PATH" + + GNU getopt must be explicitly added to the PATH since it + is keg-only (https://docs.brew.sh/FAQ#what-does-keg-only-mean). + +END_OF_LINE + exit 1 +fi # Check to see if pyenv is installed -if [ -z "$(command -v pyenv)" ] || [ -z "$(command -v pyenv-virtualenv)" ]; then +if [ -z "$(command -v pyenv)" ] || { [ -z "$(command -v pyenv-virtualenv)" ] && [ ! -f "$(pyenv root)/plugins/pyenv-virtualenv/bin/pyenv-virtualenv" ]; }; then echo "pyenv and pyenv-virtualenv are required." if [[ "$OSTYPE" == "darwin"* ]]; then cat << 'END_OF_LINE' - On the Mac, we recommend installing brew, https://brew.sh/. Then installation + On macOS, we recommend installing brew, https://brew.sh/. Then installation is as simple as `brew install pyenv pyenv-virtualenv` and adding this to your profile: @@ -81,7 +144,7 @@ END_OF_LINE fi cat << 'END_OF_LINE' - For Linux, Windows Subsystem for Linux (WSL), or on the Mac (if you don't want + For Linux, Windows Subsystem for Linux (WSL), or macOS (if you don't want to use "brew") you can use https://github.com/pyenv/pyenv-installer to install the necessary tools. Before running this ensure that you have installed the prerequisites for your platform according to the pyenv wiki page, @@ -100,16 +163,72 @@ END_OF_LINE exit 1 fi -set +o nounset +# Use GNU getopt to parse options +if ! PARSED=$(getopt --options $SHORTOPTS --longoptions $LONGOPTS --name "$0" -- "$@"); then + echo "Error parsing options" + exit 1 +fi +eval set -- "$PARSED" + +while true; do + case "$1" in + -f | --force) + FORCE=1 + shift + ;; + -h | --help) + echo "$USAGE" + exit 0 + ;; + -i | --install-hooks) + INSTALL_HOOKS=1 + shift + ;; + -l | --list-versions) + LIST_VERSIONS=1 + shift + ;; + -p | --python-version) + PYTHON_VERSION="$2" + shift 2 + # Check the Python version being passed in. + check_python_version "$PYTHON_VERSION" + ;; + -v | --venv-name) + VENV_NAME="$2" + shift 2 + ;; + --) + shift + break + ;; + *) + # Unreachable due to GNU getopt handling all options + echo "Programming error" + exit 64 + ;; + esac +done + # Determine the virtual environment name -if [ "$1" ]; then +if [ -n "$VENV_NAME" ]; then # Use the user-provided environment name - env_name=$1 + env_name="$VENV_NAME" else # Set the environment name to the last part of the working directory. env_name=${PWD##*/} fi -set -o nounset + +# List Python versions and select one interactively. +if [ $LIST_VERSIONS -ne 0 ]; then + echo Available Python versions: + python_versions + # Read the user's desired Python version. + # -r: treat backslashes as literal, -p: display prompt before input. + read -r -p "Enter the desired Python version: " PYTHON_VERSION + # Check the Python version being passed in. + check_python_version "$PYTHON_VERSION" +fi # Remove any lingering local configuration. if [ $FORCE -ne 0 ]; then @@ -118,7 +237,7 @@ if [ $FORCE -ne 0 ]; then elif [[ -f .python-version ]]; then cat << 'END_OF_LINE' An existing .python-version file was found. Either remove this file yourself - or re-run with --force option to have it deleted along with the associated + or re-run with the --force option to have it deleted along with the associated virtual environment. rm .python-version @@ -128,10 +247,18 @@ END_OF_LINE fi # Create a new virtual environment for this project -if ! pyenv virtualenv "${env_name}"; then +# +# If $PYTHON_VERSION is undefined then the current pyenv Python version will be used. +# +# We can't quote ${PYTHON_VERSION:=} below since if the variable is +# undefined then we want nothing to appear; this is the reason for the +# "shellcheck disable" line below. +# +# shellcheck disable=SC2086 +if ! pyenv virtualenv ${PYTHON_VERSION:=} "${env_name}"; then cat << END_OF_LINE An existing virtual environment named $env_name was found. Either delete this - environment yourself or re-run with --force option to have it deleted. + environment yourself or re-run with the --force option to have it deleted. pyenv virtualenv-delete ${env_name} @@ -144,7 +271,7 @@ fi pyenv local "${env_name}" # Upgrade pip and friends -python3 -m pip install --upgrade pip setuptools wheel +python3 -m pip install --upgrade pip setuptools # Find a requirements file (if possible) and install for req_file in "requirements-dev.txt" "requirements-test.txt" "requirements.txt"; do @@ -154,9 +281,6 @@ for req_file in "requirements-dev.txt" "requirements-test.txt" "requirements.txt fi done -# Install all necessary mypy type stubs -mypy --install-types src/ - # Install git pre-commit hooks now or later. pre-commit install ${INSTALL_HOOKS:+"--install-hooks"} @@ -189,5 +313,8 @@ else: END_OF_LINE )" -# Qapla +# Install all necessary mypy type stubs +mypy --install-types --non-interactive src/ + +# Qapla' echo "Success!" diff --git a/setup.py b/setup.py index c978428..1500c01 100644 --- a/setup.py +++ b/setup.py @@ -74,19 +74,23 @@ def get_version(version_file): # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", ], - python_requires=">=3.6", + python_requires=">=3.9", # What does your project relate to? keywords="cdn", packages=find_packages(where="src"), package_dir={"": "src"}, py_modules=[splitext(basename(path))[0] for path in glob("src/*.py")], include_package_data=True, +<<<<<<< HEAD install_requires=[ "dnspython", "docopt", @@ -97,20 +101,27 @@ def get_version(version_file): "urllib3", "validators", ], +======= + install_requires=["docopt", "schema", "setuptools"], +>>>>>>> 0da26c3a45b9a9c2a7d41ed2687b177a6f597116 extras_require={ + # IMPORTANT: Keep type hinting-related dependencies of the dev section + # in sync with the mypy pre-commit hook configuration (see + # .pre-commit-config.yaml). Any changes to type hinting-related + # dependencies here should be reflected in the additional_dependencies + # field of the mypy pre-commit hook to avoid discrepancies in type + # checking between environments. + "dev": [ + "types-docopt", + "types-setuptools", + ], "test": [ "coverage", - # coveralls 1.11.0 added a service number for calls from - # GitHub Actions. This caused a regression which resulted in a 422 - # response from the coveralls API with the message: - # Unprocessable Entity for url: https://coveralls.io/api/v1/jobs - # 1.11.1 fixed this issue, but to ensure expected behavior we'll pin - # to never grab the regression version. - "coveralls != 1.11.0", + "coveralls", "pre-commit", "pytest-cov", "pytest", - ] + ], }, # Conveniently allows one to run the CLI tool as `findcdn` entry_points={"console_scripts": ["findcdn = findcdn.findcdn:interactive"]}, diff --git a/src/example/example.py b/src/example/example.py new file mode 100644 index 0000000..78dd72e --- /dev/null +++ b/src/example/example.py @@ -0,0 +1,104 @@ +"""example is an example Python library and tool. + +Divide one integer by another and log the result. Also log some information +from an environment variable and a package resource. + +EXIT STATUS + This utility exits with one of the following values: + 0 Calculation completed successfully. + >0 An error occurred. + +Usage: + example [--log-level=LEVEL] + example (-h | --help) + +Options: + -h --help Show this message. + --log-level=LEVEL If specified, then the log level will be set to + the specified value. Valid values are "debug", "info", + "warning", "error", and "critical". [default: info] +""" + +# Standard Python Libraries +from importlib.resources import files +import logging +import os +import sys +from typing import Any + +# Third-Party Libraries +import docopt + +# There are no type stubs for the schema library, so mypy requires the type: +# ignore hint. +from schema import And, Schema, SchemaError, Use # type: ignore + +from ._version import __version__ + +DEFAULT_ECHO_MESSAGE: str = "Hello World from the example default!" + + +def example_div(dividend: int, divisor: int) -> float: + """Print some logging messages.""" + logging.debug("This is a debug message") + logging.info("This is an info message") + logging.warning("This is a warning message") + logging.error("This is an error message") + logging.critical("This is a critical message") + return dividend / divisor + + +def main() -> None: + """Set up logging and call the example function.""" + args: dict[str, str] = docopt.docopt(__doc__, version=__version__) + # Validate and convert arguments as needed + schema: Schema = Schema( + { + "--log-level": And( + str, + Use(str.lower), + lambda n: n in ("debug", "info", "warning", "error", "critical"), + error="Possible values for --log-level are " + + "debug, info, warning, error, and critical.", + ), + "": Use(int, error=" must be an integer."), + "": And( + Use(int), + lambda n: n != 0, + error=" must be an integer that is not 0.", + ), + str: object, # Don't care about other keys, if any + } + ) + + try: + validated_args: dict[str, Any] = schema.validate(args) + except SchemaError as err: + # Exit because one or more of the arguments were invalid + print(err, file=sys.stderr) + sys.exit(1) + + # Assign validated arguments to variables + dividend: int = validated_args[""] + divisor: int = validated_args[""] + log_level: str = validated_args["--log-level"] + + # Set up logging + logging.basicConfig( + format="%(asctime)-15s %(levelname)s %(message)s", level=log_level.upper() + ) + + logging.info("%d / %d == %f", dividend, divisor, example_div(dividend, divisor)) + + # Access some data from an environment variable + message: str = os.getenv("ECHO_MESSAGE", DEFAULT_ECHO_MESSAGE) + logging.info('ECHO_MESSAGE="%s"', message) + + # Access some data from our package data (see pyproject.toml) + secret_message: str = ( + files(__package__).joinpath("data", "secret.txt").read_text().strip() + ) + logging.info('Secret="%s"', secret_message) + + # Stop logging and clean up + logging.shutdown() diff --git a/src/findcdn/__init__.py b/src/findcdn/__init__.py index 0d4a92b..18d4ce6 100644 --- a/src/findcdn/__init__.py +++ b/src/findcdn/__init__.py @@ -1,4 +1,9 @@ +<<<<<<< HEAD:src/findcdn/__init__.py """The findcdn library.""" +======= +"""The example library.""" + +>>>>>>> 272917ac0cd708e1201cbefb6824e84fa777dabd:src/example/__init__.py # We disable a Flake8 check for "Module imported but unused (F401)" here because # although this import is not directly used, it populates the value # package_name.__version__, which is used to get version information about this diff --git a/src/findcdn/_version.py b/src/findcdn/_version.py index 5eb9b0e..f8d70f6 100644 --- a/src/findcdn/_version.py +++ b/src/findcdn/_version.py @@ -1,2 +1,3 @@ """This file defines the version of this module.""" -__version__ = "0.1.0" + +__version__ = "1.0.0" diff --git a/tests/conftest.py b/tests/conftest.py index 39e2be2..ba89c85 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ https://docs.pytest.org/en/latest/writing_plugins.html#conftest-py-plugins """ + # Third-Party Libraries import pytest diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..96f4560 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,143 @@ +"""Tests for example.""" + +# Standard Python Libraries +import logging +import os +import sys +from unittest.mock import patch + +# Third-Party Libraries +import pytest + +# cisagov Libraries +import example + +div_params = [ + (1, 1, 1), + (2, 2, 1), + (0, 1, 0), + (8, 2, 4), +] + +log_levels = ( + "debug", + "info", + "warning", + "error", + "critical", +) + +# define sources of version strings +RELEASE_TAG = os.getenv("RELEASE_TAG") +PROJECT_VERSION = example.__version__ + + +def test_stdout_version(capsys): + """Verify that version string sent to stdout agrees with the module version.""" + with pytest.raises(SystemExit): + with patch.object(sys, "argv", ["bogus", "--version"]): + example.example.main() + captured = capsys.readouterr() + assert ( + captured.out == f"{PROJECT_VERSION}\n" + ), "standard output by '--version' should agree with module.__version__" + + +def test_running_as_module(capsys): + """Verify that the __main__.py file loads correctly.""" + with pytest.raises(SystemExit): + with patch.object(sys, "argv", ["bogus", "--version"]): + # F401 is a "Module imported but unused" warning. This import + # emulates how this project would be run as a module. The only thing + # being done by __main__ is importing the main entrypoint of the + # package and running it, so there is nothing to use from this + # import. As a result, we can safely ignore this warning. + # cisagov Libraries + import example.__main__ # noqa: F401 + captured = capsys.readouterr() + assert ( + captured.out == f"{PROJECT_VERSION}\n" + ), "standard output by '--version' should agree with module.__version__" + + +@pytest.mark.skipif( + RELEASE_TAG in [None, ""], reason="this is not a release (RELEASE_TAG not set)" +) +def test_release_version(): + """Verify that release tag version agrees with the module version.""" + assert ( + RELEASE_TAG == f"v{PROJECT_VERSION}" + ), "RELEASE_TAG does not match the project version" + + +@pytest.mark.parametrize("level", log_levels) +def test_log_levels(level): + """Validate commandline log-level arguments.""" + with patch.object(sys, "argv", ["bogus", f"--log-level={level}", "1", "1"]): + with patch.object(logging.root, "handlers", []): + assert ( + logging.root.hasHandlers() is False + ), "root logger should not have handlers yet" + return_code = None + try: + example.example.main() + except SystemExit as sys_exit: + return_code = sys_exit.code + assert return_code is None, "main() should return success" + assert ( + logging.root.hasHandlers() is True + ), "root logger should now have a handler" + assert ( + logging.getLevelName(logging.root.getEffectiveLevel()) == level.upper() + ), f"root logger level should be set to {level.upper()}" + assert return_code is None, "main() should return success" + + +def test_bad_log_level(): + """Validate bad log-level argument returns error.""" + with patch.object(sys, "argv", ["bogus", "--log-level=emergency", "1", "1"]): + return_code = None + try: + example.example.main() + except SystemExit as sys_exit: + return_code = sys_exit.code + assert return_code == 1, "main() should exit with error" + + +@pytest.mark.parametrize("dividend, divisor, quotient", div_params) +def test_division(dividend, divisor, quotient): + """Verify division results.""" + result = example.example_div(dividend, divisor) + assert result == quotient, "result should equal quotient" + + +@pytest.mark.slow +def test_slow_division(): + """Example of using a custom marker. + + This test will only be run if --runslow is passed to pytest. + Look in conftest.py to see how this is implemented. + """ + # Standard Python Libraries + import time + + result = example.example_div(256, 16) + time.sleep(4) + assert result == 16, "result should equal be 16" + + +def test_zero_division(): + """Verify that division by zero throws the correct exception.""" + with pytest.raises(ZeroDivisionError): + example.example_div(1, 0) + + +def test_zero_divisor_argument(): + """Verify that a divisor of zero is handled as expected.""" + with patch.object(sys, "argv", ["bogus", "1", "0"]): + return_code = None + try: + example.example.main() + except SystemExit as sys_exit: + return_code = sys_exit.code + assert return_code == 1, "main() should exit with error"