diff --git a/.bandit.yml b/.bandit.yml index 8ba42d1..2b618f6 100644 --- a/.bandit.yml +++ b/.bandit.yml @@ -10,4 +10,4 @@ tests: # - B102 skips: -# - B101 # skip "assert used" check since assertions are required in pytests + - B101 # skip "assert used" check since assertions are required in pytests diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5fe9a06..aba2d98 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,8 @@ updates: - dependency-name: actions/setup-python - dependency-name: hashicorp/setup-terraform - dependency-name: mxschmitt/action-tmate + # Managed by cisagov/skeleton-aws-lambda-python + # - dependency-name: actions/upload-artifact - package-ecosystem: "pip" directory: "/" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d17421..f668b91 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,7 @@ on: env: CURL_CACHE_DIR: ~/.cache/curl + DEFAULT_ARTIFACT_NAME: lambda_build.zip PIP_CACHE_DIR: ~/.cache/pip PRE_COMMIT_CACHE_DIR: ~/.cache/pre-commit RUN_TMATE: ${{ secrets.RUN_TMATE }} @@ -98,3 +99,71 @@ jobs: - name: Setup tmate debug session uses: mxschmitt/action-tmate@v3 if: env.RUN_TMATE + test: + runs-on: ubuntu-latest + needs: lint + steps: + - id: setup-env + uses: cisagov/setup-env-github-action@develop + - uses: actions/checkout@v3 + - id: setup-python + uses: actions/setup-python@v4 + 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 }} + key: "${{ env.BASE_CACHE_KEY }}\ + ${{ hashFiles('**/requirements-test.txt') }}-\ + ${{ hashFiles('**/requirements.txt') }}" + restore-keys: | + ${{ env.BASE_CACHE_KEY }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade --requirement requirements-test.txt + - name: Run tests + env: + GITHUB_RELEASE_TAG: ${{ github.event.release.tag_name }} + run: pytest + - name: Setup tmate debug session + uses: mxschmitt/action-tmate@v3 + if: env.RUN_TMATE + build: + runs-on: ubuntu-latest + needs: + - lint + - test + strategy: + matrix: + # Python runtime versions supported by AWS + python-version: + - "3.7" + - "3.8" + - "3.9" + steps: + - uses: actions/checkout@v3 + - name: Get the short SHA for the commit being used + run: | + echo "GH_SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + - name: Build the base Lambda Docker image + run: | + docker compose build \ + --build-arg PY_VERSION=${{ matrix.python-version }} \ + build_deployment_package + - name: Generate the Lambda deployment package + run: docker compose up build_deployment_package + - name: Upload the generated Lambda deployment package as an artifact + uses: actions/upload-artifact@v3 + with: + name: "${{ github.event.repository.name }}-\ + py${{ matrix.python-version }}-\ + ${{ env.GH_SHORT_SHA }}" + path: "${{ env.DEFAULT_ARTIFACT_NAME }}" + - 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 new file mode 100644 index 0000000..5e344d1 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +--- +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + # 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' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + # required for all workflows + security-events: write + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are go, javascript, csharp, python, cpp, and java + language: + - 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: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + # Autobuild attempts to build any compiled languages (C/C++, C#, or + # 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 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following + # three lines and modify them (or add more) to build your code if your + # project uses a compiled language + + # - run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 937e21d..56911e6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # Files already tracked by Git are not affected. # See: https://git-scm.com/docs/gitignore +## Project Specific ## +lambda_build.zip + ## Python ## __pycache__ .mypy_cache diff --git a/.isort.cfg b/.isort.cfg index 46d45f3..783da85 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -6,5 +6,7 @@ import_heading_stdlib=Standard Python Libraries import_heading_thirdparty=Third-Party Libraries import_heading_firstparty=cisagov Libraries +known_first_party=example + # Run isort under the black profile to align with our other Python linting profile=black diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25f8d1d..7f17511 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -81,12 +81,22 @@ repos: - id: shell-lint # Python hooks + # Run bandit on the tests directory with a custom configuration - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: - id: bandit + name: bandit (tests directory) + files: tests args: - --config=.bandit.yml + # Run bandit on everything but the tests directory + - repo: https://github.com/PyCQA/bandit + rev: 1.7.4 + hooks: + - id: bandit + name: bandit (everything but the tests directory) + exclude: tests - repo: https://github.com/psf/black rev: 22.10.0 hooks: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc501b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +ARG PY_VERSION=3.9 + +FROM amazon/aws-lambda-python:$PY_VERSION as install-stage + +# Declare it a second time so it's brought into this scope. +ARG PY_VERSION=3.9 + +# Install the Python packages necessary to install the Lambda dependencies. +RUN python3 -m pip install --no-cache-dir \ + pip \ + setuptools \ + wheel \ + # This version of pipenv is the minimum version to allow passing arguments + # to pip with the --extra-pip-args option. + && python3 -m pip install --no-cache-dir "pipenv>=2022.9.8" + +WORKDIR /tmp + +# Copy in the dependency files. +COPY src/py$PY_VERSION/ . + +# Install the Lambda dependencies. +# +# The --extra-pip-args option is used to pass necessary arguments to the +# underlying pip calls. +RUN pipenv sync --system --extra-pip-args="--no-cache-dir --target ${LAMBDA_TASK_ROOT}" + +FROM amazon/aws-lambda-python:$PY_VERSION as build-stage + +### +# For a list of pre-defined annotation keys and value types see: +# https://github.com/opencontainers/image-spec/blob/master/annotations.md +### +# github@cisa.dhs.gov is a very generic email distribution, and it is +# unlikely that anyone on that distribution is familiar with the +# particulars of your repository. It is therefore *strongly* +# suggested that you use an email address here that is specific to the +# person or group that maintains this repository; for example: +# LABEL org.opencontainers.image.authors="vm-fusion-dev-group@trio.dhs.gov" +LABEL org.opencontainers.image.authors="github@cisa.dhs.gov" +LABEL org.opencontainers.image.vendor="Cybersecurity and Infrastructure Security Agency" + +# Declare it a third time so it's brought into this scope. +ARG PY_VERSION=3.9 + +# This must be present in the image to generate a deployment artifact. +ENV BUILD_PY_VERSION=$PY_VERSION + +COPY --from=install-stage ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} + +WORKDIR ${LAMBDA_TASK_ROOT} + +# Copy in the handler. +COPY src/lambda_handler.py . + +# Ensure our handler is invoked when the image is used. +CMD ["lambda_handler.handler"] diff --git a/README.md b/README.md index 9c63e7f..02ae4a4 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,74 @@ [![GitHub Build Status](https://github.com/cisagov/skeleton-aws-lambda-python/workflows/build/badge.svg)](https://github.com/cisagov/skeleton-aws-lambda-python/actions) This is a generic skeleton project that can be used to quickly get a -new [cisagov](https://github.com/cisagov) GitHub project started. -This skeleton project contains [licensing information](LICENSE), as +new [cisagov](https://github.com/cisagov) GitHub +[AWS Lambda](https://aws.amazon.com/lambda/) project using the Python runtimes +started. This skeleton project contains [licensing information](LICENSE), as well as [pre-commit hooks](https://pre-commit.com) and [GitHub Actions](https://github.com/features/actions) configurations appropriate for the major languages that we use. -In many cases you will instead want to use one of the more specific -skeleton projects derived from this one. +## Building the base Lambda image ## + +The base Lambda image can be built with the following command: + +```console +docker compose build +``` + +This base image is used both to build a deployment package and to run the +Lambda locally. + +## Building a deployment package ## + +You can build a deployment zip file to use when creating a new AWS Lambda +function with the following command: + +```console +docker compose up build_deployment_package +``` + +This will output the deployment zip file in the root directory. + +## Running the Lambda locally ## + +The configuration in this repository allows you run the Lambda locally for +testing as long as you do not need explicit permissions for other AWS +services. This can be done with the following command: + +```console +docker compose up --detach run_lambda_locally +``` + +You can then invoke the Lambda using the following: + +```console + curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' +``` + +The `{}` in the command is the invocation event payload to send to the Lambda +and would be the value given as the `event` argument to the handler. + +Once you are finished you can stop the detached container with the following command: + +```console +docker compose down +``` + +## How to update Python dependencies ## + +The Python dependencies are maintained using a [Pipenv](https://github.com/pypa/pipenv) +configuration for each supported Python version. Changes to requirements +should be made to the respective `src/py/Pipfile`. More +information about the `Pipfile` format can be found [here](https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock). +The accompanying `Pipfile.lock` files contain the specific dependency versions +that will be installed. These files can be updated like so (using the Python +3.9 configuration as an example): + +```console +cd src/py3.9 +pipenv lock +``` ## New Repositories from a Skeleton ## diff --git a/bump_version.sh b/bump_version.sh new file mode 100755 index 0000000..45e813f --- /dev/null +++ b/bump_version.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# bump_version.sh (show|major|minor|patch|prerelease|build) + +set -o nounset +set -o errexit +set -o pipefail + +VERSION_FILE=src/version.txt + +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" +else + case $1 in + major | minor | patch | prerelease | build) + new_version=$(python -c "import semver; print(semver.bump_$1('$old_version'))") + echo Changing version from "$old_version" to "$new_version" + # 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_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 push + ;; + finalize) + new_version=$(python -c "import semver; print(semver.finalize_version('$old_version'))") + echo Changing version from "$old_version" to "$new_version" + # 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_regex/$new_version/" $VERSION_FILE > $tmp_file + mv $tmp_file $VERSION_FILE + git add $VERSION_FILE + git commit -m"Finalize version from $old_version to $new_version" + git push + ;; + show) + echo "$old_version" + ;; + *) + echo "$HELP_INFORMATION" + ;; + esac +fi diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6e46434 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +--- +version: '3.2' + +services: + build_deployment_package: + build: . + # This uses the value of the LAMBDA_TAG environment variable from + # the invoking environment but falls back to a default value. + image: cisagov/example_lambda:${LAMBDA_TAG:-latest} + entrypoint: /opt/build_artifact.sh + environment: + # This uses the value of the BUILD_FILE_NAME environment variable + # from the invoking environment but falls back to a default value. + - BUILD_FILE_NAME=${BUILD_FILE_NAME:-lambda_build.zip} + volumes: + - ./src/build_artifact.sh:/opt/build_artifact.sh + - .:/var/task/output + run_lambda_locally: + build: . + # This uses the value of the LAMBDA_TAG environment variable from + # the invoking environment but falls back to a default value. + image: cisagov/example_lambda:${LAMBDA_TAG:-latest} + ports: + - "9000:8080" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..45cf99a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +# Increase verbosity and display extra test summary info for tests that did not pass +addopts = --verbose -ra diff --git a/requirements-dev.txt b/requirements-dev.txt index d84ee68..8d26d98 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ --requirement requirements-test.txt ipython +pipenv diff --git a/requirements-test.txt b/requirements-test.txt index 66f74db..c418d9b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,3 @@ --requirement requirements.txt pre-commit +pytest diff --git a/src/build_artifact.sh b/src/build_artifact.sh new file mode 100755 index 0000000..c511859 --- /dev/null +++ b/src/build_artifact.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o pipefail + +ARTIFACT_FILE_NAME="${BUILD_FILE_NAME:-lambda_build.zip}" + +output_directory="${LAMBDA_TASK_ROOT}/output" +artifact_path="$output_directory/$ARTIFACT_FILE_NAME" + +# We need the zip utility to create a deployment package archive. +yum update --assumeyes --quiet +yum install --assumeyes --quiet zip + +# Build the deployment package by zipping the libraries and handler. We +# explicitly exclude the build/ and output/ subdirectories as they are +# not part of the deployment package. Since this is a deployment artifact +# we use the maximum compression setting (-9). +cd "${LAMBDA_TASK_ROOT}" +zip --recurse-paths --quiet -9 "$artifact_path" . \ + --exclude "build/*" \ + --exclude "output/*" diff --git a/src/lambda_handler.py b/src/lambda_handler.py new file mode 100644 index 0000000..e61ce6e --- /dev/null +++ b/src/lambda_handler.py @@ -0,0 +1,108 @@ +"""Simple AWS Lambda handler to verify functionality.""" + +# Standard Python Libraries +from datetime import datetime, timezone +import logging +from typing import Any, Optional, Union + +# Third-Party Libraries +import cowsay +import cowsay.characters + +# cisagov Libraries +from example import example_div + +logger = logging.getLogger() +logger.setLevel(logging.INFO) + + +def failed_task(result: dict[str, Any], error_msg: str) -> None: + """Update a given result because of a failure during processing.""" + result["success"] = False + result["error_message"] = error_msg + + +def task_default(event): + """Provide a result if no valid task was provided.""" + result = {} + error_msg = 'Provided task "%s" is not supported.' + + task = event.get("task", None) + logging.error(error_msg, task) + failed_task(result, error_msg % task) + + return result + + +def task_cowsay(event) -> dict[str, Union[Optional[str], bool]]: + """Generate an output message using the provided information.""" + result: dict[str, Union[Optional[str], bool]] = {"message": None, "success": True} + + character: str = event.get("character", "tux") + if character not in cowsay.characters.CHARS.keys(): + error_msg = 'Character "%s" is not valid.' + logging.error(error_msg, character) + failed_task(result, error_msg % character) + else: + contents: str = event.get("contents", "Hello from AWS Lambda!") + logger.info( + 'Creating output using "%s" with contents "%s"', character, contents + ) + result["message"] = cowsay.get_output_string(character, contents) + + return result + + +def task_divide(event) -> dict[str, Union[Optional[float], bool]]: + """Divide one number by another and provide the result.""" + result: dict[str, Union[Optional[float], bool]] = {"result": None, "success": True} + numerator: str = event.get("numerator", None) + denominator: str = event.get("denominator", None) + + if denominator is None or numerator is None: + error_msg = "Request must include both a numerator and a denominator." + logging.error(error_msg) + failed_task(result, error_msg) + else: + try: + variable_error_msg = "numerator: %s, denominator: %s" + result["result"] = example_div(int(numerator), int(denominator)) + except ValueError: + error_msg = "The provided values must be integers." + logging.error(error_msg) + logging.error(variable_error_msg, numerator, denominator) + failed_task(result, error_msg) + except ZeroDivisionError: + error_msg = "The denominator cannot be zero." + logging.error(error_msg) + logging.error(variable_error_msg, numerator, denominator) + failed_task(result, error_msg) + + return result + + +def handler(event, context) -> dict[str, Optional[str]]: + """Process the event and generate a response. + + The event should have a task member that is one of the supported tasks. + + :param event: The event dict that contains the parameters sent when the function + is invoked. + :param context: The context in which the function is called. + :return: The result of the action. + """ + response: dict[str, Optional[str]] = {"timestamp": str(datetime.now(timezone.utc))} + + task_name = f"task_{event.get('task')}" + task = globals().get(task_name, task_default) + + result: dict[str, Any] + if not callable(task): + logging.error("Provided task is not a callable.") + logging.error(task) + result = task_default(event) + else: + result = task(event) + + response.update(result) + return response diff --git a/src/py3.7/Pipfile b/src/py3.7/Pipfile new file mode 100644 index 0000000..e49bd0f --- /dev/null +++ b/src/py3.7/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] +python_version = "3.7" + +[packages] +cowsay = "*" +example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} diff --git a/src/py3.7/Pipfile.lock b/src/py3.7/Pipfile.lock new file mode 100644 index 0000000..e14bffd --- /dev/null +++ b/src/py3.7/Pipfile.lock @@ -0,0 +1,64 @@ +{ + "_meta": { + "hash": { + "sha256": "fb8e36b2dfbe5f058679f36466257570fc6c43f47b6d060d907073b831843d83" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "contextlib2": { + "hashes": [ + "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", + "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + ], + "markers": "python_version >= '3.6'", + "version": "==21.6.0" + }, + "cowsay": { + "hashes": [ + "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3" + ], + "index": "pypi", + "version": "==5.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "example": { + "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz", + "hashes": [ + "sha256:d4ae2105b555cb386daf39e06b05594596e881e67faffc46c69d9e7ce56c8c4c" + ], + "version": "==0.1.0" + }, + "schema": { + "hashes": [ + "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", + "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + ], + "version": "==0.7.5" + }, + "setuptools": { + "hashes": [ + "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", + "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + ], + "markers": "python_version >= '3.7'", + "version": "==65.6.3" + } + }, + "develop": {} +} diff --git a/src/py3.8/Pipfile b/src/py3.8/Pipfile new file mode 100644 index 0000000..b568dea --- /dev/null +++ b/src/py3.8/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] +python_version = "3.8" + +[packages] +cowsay = "*" +example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} diff --git a/src/py3.8/Pipfile.lock b/src/py3.8/Pipfile.lock new file mode 100644 index 0000000..087f37a --- /dev/null +++ b/src/py3.8/Pipfile.lock @@ -0,0 +1,64 @@ +{ + "_meta": { + "hash": { + "sha256": "e7d647bd6df129d143384648900fa34961e527257ff08634f04b9badb5fd87f4" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "contextlib2": { + "hashes": [ + "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", + "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + ], + "markers": "python_version >= '3.6'", + "version": "==21.6.0" + }, + "cowsay": { + "hashes": [ + "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3" + ], + "index": "pypi", + "version": "==5.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "example": { + "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz", + "hashes": [ + "sha256:d4ae2105b555cb386daf39e06b05594596e881e67faffc46c69d9e7ce56c8c4c" + ], + "version": "==0.1.0" + }, + "schema": { + "hashes": [ + "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", + "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + ], + "version": "==0.7.5" + }, + "setuptools": { + "hashes": [ + "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", + "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + ], + "markers": "python_version >= '3.7'", + "version": "==65.6.3" + } + }, + "develop": {} +} diff --git a/src/py3.9/Pipfile b/src/py3.9/Pipfile new file mode 100644 index 0000000..0e08b63 --- /dev/null +++ b/src/py3.9/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] +python_version = "3.9" + +[packages] +cowsay = "*" +example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz"} diff --git a/src/py3.9/Pipfile.lock b/src/py3.9/Pipfile.lock new file mode 100644 index 0000000..0e77805 --- /dev/null +++ b/src/py3.9/Pipfile.lock @@ -0,0 +1,64 @@ +{ + "_meta": { + "hash": { + "sha256": "0917b51635aa96ace981678b22ea817619fd4113a78afe4c2935281268631b7e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "contextlib2": { + "hashes": [ + "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", + "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + ], + "markers": "python_version >= '3.6'", + "version": "==21.6.0" + }, + "cowsay": { + "hashes": [ + "sha256:c00e02444f5bc7332826686bd44d963caabbaba9a804a63153822edce62bbbf3" + ], + "index": "pypi", + "version": "==5.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "example": { + "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.1.0.tar.gz", + "hashes": [ + "sha256:d4ae2105b555cb386daf39e06b05594596e881e67faffc46c69d9e7ce56c8c4c" + ], + "version": "==0.1.0" + }, + "schema": { + "hashes": [ + "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197", + "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c" + ], + "version": "==0.7.5" + }, + "setuptools": { + "hashes": [ + "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", + "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + ], + "markers": "python_version >= '3.7'", + "version": "==65.6.3" + } + }, + "develop": {} +} diff --git a/src/version.txt b/src/version.txt new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/src/version.txt @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/tag.sh b/tag.sh new file mode 100755 index 0000000..e1f7447 --- /dev/null +++ b/tag.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o pipefail + +version=$(./bump_version.sh show) + +git tag "v$version" && git push --tags diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4189122 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +"""pytest plugin configuration. + +https://docs.pytest.org/en/latest/writing_plugins.html#conftest-py-plugins +""" +# Third-Party Libraries +import pytest + + +def pytest_addoption(parser): + """Add new commandline options to pytest.""" + parser.addoption( + "--runslow", action="store_true", default=False, help="run slow tests" + ) + + +def pytest_collection_modifyitems(config, items): + """Modify collected tests based on custom marks and commandline options.""" + if config.getoption("--runslow"): + # --runslow given in cli: do not skip slow tests + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..b9f8251 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,26 @@ +#!/usr/bin/env pytest -vs +"""Version tests for AWS Lambda Python skeleton project.""" + +# Standard Python Libraries +import os + +# Third-Party Libraries +import pytest + +GITHUB_RELEASE_TAG = os.getenv("GITHUB_RELEASE_TAG") +VERSION_FILE = "src/version.txt" + + +@pytest.mark.skipif( + GITHUB_RELEASE_TAG in [None, ""], + reason="this is not a release (GITHUB_RELEASE_TAG not set)", +) +def test_release_version(): + """Verify that release tag version agrees with the module version.""" + pkg_vars = {} + with open(VERSION_FILE) as f: + exec(f.read(), pkg_vars) # nosec + project_version = pkg_vars["__version__"] + assert ( + GITHUB_RELEASE_TAG == f"v{project_version}" + ), "GITHUB_RELEASE_TAG does not match the project version"