diff --git a/.flake8 b/.flake8 index 6c0c78fa967..c7d6486ed48 100644 --- a/.flake8 +++ b/.flake8 @@ -3,6 +3,9 @@ exclude = docs, .eggs, setup.py, example, .aws-sam, .git, dist, *.md, *.yaml, ex ignore = E203, E266, W503, BLK100, W291, I004 max-line-length = 120 max-complexity = 15 +per-file-ignores = + tests/e2e/utils/data_builder/__init__.py:F401 + tests/e2e/utils/data_fetcher/__init__.py:F401 [isort] multi_line_output = 3 diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index 0e2b7a2dc81..ecd67fc20d9 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -37,14 +37,13 @@ labelPRBasedOnFilePath: area/feature_flags: - aws_lambda_powertools/feature_flags/* - aws_lambda_powertools/feature_flags/**/* - area/jmespath_util: + area/jmespath: - aws_lambda_powertools/utilities/jmespath_utils/* area/typing: - aws_lambda_powertools/utilities/typing/* - mypy.ini - area/utilities: - - aws_lambda_powertools/utilities/* - - aws_lambda_powertools/utilities/**/* + area/commons: + - aws_lambda_powertools/shared/* documentation: - docs/* diff --git a/.github/workflows/build_changelog.yml b/.github/workflows/build_changelog.yml new file mode 100644 index 00000000000..f0501083048 --- /dev/null +++ b/.github/workflows/build_changelog.yml @@ -0,0 +1,10 @@ +# Standalone workflow to update changelog if necessary +name: Build changelog + +on: + workflow_dispatch: + +jobs: + changelog: + needs: release + uses: ./.github/workflows/reusable_publish_changelog.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/on_release_notes.yml similarity index 94% rename from .github/workflows/publish.yml rename to .github/workflows/on_release_notes.yml index 3eaabc7fab6..563d1fefc79 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/on_release_notes.yml @@ -20,6 +20,9 @@ name: Publish to PyPi # See MAINTAINERS.md "Releasing a new version" for release mechanisms +env: + BRANCH: develop + on: release: types: [published] @@ -119,10 +122,13 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Setup git client + - name: Git client setup and refresh tip run: | git config user.name "Release bot" - git config user.email aws-devax-open-source@amazon.com + git config user.email "aws-devax-open-source@amazon.com" + git config pull.rebase true + git config remote.origin.url >&- || git remote add origin https://github.com/$origin # Git Detached mode (release notes) doesn't have origin + git pull origin $BRANCH - name: Install poetry run: pipx install poetry - name: Set up Python diff --git a/.github/workflows/publish_layer.yml b/.github/workflows/publish_layer.yml index 2ad83624111..564cbfad9de 100644 --- a/.github/workflows/publish_layer.yml +++ b/.github/workflows/publish_layer.yml @@ -28,6 +28,8 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + - name: Install poetry + run: pipx install poetry - name: Setup Node.js uses: actions/setup-node@v3 with: @@ -37,6 +39,12 @@ jobs: with: python-version: "3.9" cache: "pip" + - name: Resolve and install project dependencies + # CDK spawns system python when compiling stack + # therefore it ignores both activated virtual env and cached interpreter by GH + run: | + poetry export --format requirements.txt --output requirements.txt + pip install -r requirements.txt - name: Set release notes tag run: | RELEASE_INPUT=${{ inputs.latest_published_version }} @@ -47,9 +55,6 @@ jobs: run: | npm install -g aws-cdk@2.29.0 cdk --version - - name: install deps - run: | - pip install -r requirements.txt - name: CDK build run: cdk synth --context version=$RELEASE_TAG_VERSION -o cdk.out - name: zip output diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 54d8c5ea723..e1d0ddce268 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,6 +10,6 @@ jobs: update_release_draft: runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@ac463ffd9cc4c6ad5682af93dc3e3591c4657ee3 # v5.20.0 + - uses: release-drafter/release-drafter@06a49bf28488e030d35ca2ac6dbf7f408a481779 # v5.20.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable_deploy_layer_stack.yml b/.github/workflows/reusable_deploy_layer_stack.yml index 705ef530853..e1190e19873 100644 --- a/.github/workflows/reusable_deploy_layer_stack.yml +++ b/.github/workflows/reusable_deploy_layer_stack.yml @@ -55,6 +55,8 @@ jobs: steps: - name: checkout uses: actions/checkout@v3 + - name: Install poetry + run: pipx install poetry - name: aws credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -69,13 +71,18 @@ jobs: with: python-version: "3.9" cache: "pip" + - name: Resolve and install project dependencies + # CDK spawns system python when compiling stack + # therefore it ignores both activated virtual env and cached interpreter by GH + run: | + poetry export --format requirements.txt --output requirements.txt + pip install -r requirements.txt - name: install cdk and deps run: | npm install -g aws-cdk@2.29.0 cdk --version - name: install deps - run: | - pip install -r requirements.txt + run: poetry install - name: Download artifact uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/reusable_publish_changelog.yml b/.github/workflows/reusable_publish_changelog.yml index 16fd2c06d18..2cb786ed86a 100644 --- a/.github/workflows/reusable_publish_changelog.yml +++ b/.github/workflows/reusable_publish_changelog.yml @@ -6,6 +6,9 @@ on: permissions: contents: write +env: + BRANCH: develop + jobs: publish_changelog: # Force Github action to run only a single job at a time (based on the group name) @@ -23,11 +26,13 @@ jobs: git config user.name "Release bot" git config user.email "aws-devax-open-source@amazon.com" git config pull.rebase true - git pull --rebase + git config remote.origin.url >&- || git remote add origin https://github.com/$origin # Git Detached mode (release notes) doesn't have origin + git pull origin $BRANCH - name: "Generate latest changelog" run: make changelog - name: Update Changelog in trunk run: | git add CHANGELOG.md - git commit -m "chore(ci): update changelog with latest changes" - git push origin HEAD:refs/heads/develop + git commit -m "update changelog with latest changes" + git pull origin $BRANCH # prevents concurrent branch update failing push + git push origin HEAD:refs/heads/$BRANCH diff --git a/.github/workflows/run-e2e-tests.yml b/.github/workflows/run-e2e-tests.yml index a020410823c..5b1372d54aa 100644 --- a/.github/workflows/run-e2e-tests.yml +++ b/.github/workflows/run-e2e-tests.yml @@ -1,9 +1,17 @@ -name: run-e2e-tests +name: Run end-to-end tests + on: workflow_dispatch: + + push: + branches: [develop] + +# Maintenance: Add support for triggering on `run-e2e` label +# and enforce repo origin to prevent abuse + env: AWS_DEFAULT_REGION: us-east-1 - E2E_TESTS_PATH: tests/e2e/ + jobs: run: runs-on: ubuntu-latest @@ -12,7 +20,9 @@ jobs: contents: read strategy: matrix: + # Maintenance: disabled until we discover concurrency lock issue with multiple versions and tmp version: ["3.7", "3.8", "3.9"] + # version: ["3.7"] steps: - name: "Checkout" uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f0b6819d8..20c156aec8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,70 @@ ## Bug Fixes +* **ci:** calculate parallel jobs based on infrastructure needs ([#1475](https://github.com/awslabs/aws-lambda-powertools-python/issues/1475)) +* **ci:** del flake8 direct dep over py3.6 conflicts and docs failure +* **ci:** move from pip-tools to poetry on layers reusable workflow +* **ci:** move from pip-tools to poetry on layers to fix conflicts +* **ci:** typo and bust gh actions cache +* **ci:** use poetry to resolve layer deps; pip for CDK +* **ci:** disable poetry venv for layer workflow as cdk ignores venv +* **ci:** add cdk v2 dep for layers workflow +* **ci:** move from pip-tools to poetry on layers +* **ci:** temporarily disable changelog upon release +* **ci:** add explicit origin to fix release detached head +* **jmespath_util:** snappy as dev dep and typing example ([#1446](https://github.com/awslabs/aws-lambda-powertools-python/issues/1446)) + +## Documentation + +* **apigateway:** removes duplicate admonition ([#1426](https://github.com/awslabs/aws-lambda-powertools-python/issues/1426)) +* **home:** fix discord syntax and add Discord badge +* **home:** add discord invitation link ([#1471](https://github.com/awslabs/aws-lambda-powertools-python/issues/1471)) +* **jmespath_util:** snippets split, improved, and lint ([#1419](https://github.com/awslabs/aws-lambda-powertools-python/issues/1419)) +* **layer:** upgrade to 1.27.0 +* **layer:** upgrade to 1.27.0 +* **middleware-factory:** snippets split, improved, and lint ([#1451](https://github.com/awslabs/aws-lambda-powertools-python/issues/1451)) +* **parser:** minor grammar fix ([#1427](https://github.com/awslabs/aws-lambda-powertools-python/issues/1427)) +* **typing:** snippets split, improved, and lint ([#1465](https://github.com/awslabs/aws-lambda-powertools-python/issues/1465)) +* **validation:** snippets split, improved, and lint ([#1449](https://github.com/awslabs/aws-lambda-powertools-python/issues/1449)) + +## Features + +* **parser:** add support for Lambda Function URL ([#1442](https://github.com/awslabs/aws-lambda-powertools-python/issues/1442)) + +## Maintenance + +* **batch:** deprecate sqs_batch_processor ([#1463](https://github.com/awslabs/aws-lambda-powertools-python/issues/1463)) +* **ci:** prevent concurrent git update in critical workflows ([#1478](https://github.com/awslabs/aws-lambda-powertools-python/issues/1478)) +* **ci:** disable e2e py version matrix due to concurrent locking +* **ci:** revert e2e py version matrix +* **ci:** temp disable e2e matrix +* **ci:** update changelog with latest changes +* **ci:** update changelog with latest changes +* **ci:** reduce payload and only send prod notification +* **ci:** remove area/utilities conflicting label +* **ci:** include py version in stack and cache lock +* **ci:** remove conventional changelog commit to reduce noise +* **ci:** update changelog with latest changes +* **deps:** bump release-drafter/release-drafter from 5.20.0 to 5.20.1 ([#1458](https://github.com/awslabs/aws-lambda-powertools-python/issues/1458)) +* **deps:** bump pydantic from 1.9.1 to 1.9.2 ([#1448](https://github.com/awslabs/aws-lambda-powertools-python/issues/1448)) +* **deps-dev:** bump flake8-bugbear from 22.8.22 to 22.8.23 ([#1473](https://github.com/awslabs/aws-lambda-powertools-python/issues/1473)) +* **deps-dev:** bump types-requests from 2.28.7 to 2.28.8 ([#1423](https://github.com/awslabs/aws-lambda-powertools-python/issues/1423)) +* **maintainer:** add Leandro as maintainer ([#1468](https://github.com/awslabs/aws-lambda-powertools-python/issues/1468)) +* **tests:** build and deploy Lambda Layer stack once ([#1466](https://github.com/awslabs/aws-lambda-powertools-python/issues/1466)) +* **tests:** refactor E2E test mechanics to ease maintenance, writing tests and parallelization ([#1444](https://github.com/awslabs/aws-lambda-powertools-python/issues/1444)) +* **tests:** enable end-to-end test workflow ([#1470](https://github.com/awslabs/aws-lambda-powertools-python/issues/1470)) +* **tests:** refactor E2E logger to ease maintenance, writing tests and parallelization ([#1460](https://github.com/awslabs/aws-lambda-powertools-python/issues/1460)) +* **tests:** refactor E2E tracer to ease maintenance, writing tests and parallelization ([#1457](https://github.com/awslabs/aws-lambda-powertools-python/issues/1457)) + + + +## [v1.27.0] - 2022-08-05 +## Bug Fixes + * **ci:** changelog workflow must receive git tags too * **ci:** add additional input to accurately describe intent on skip * **ci:** job permissions +* **event_sources:** add test for Function URL AuthZ ([#1421](https://github.com/awslabs/aws-lambda-powertools-python/issues/1421)) ## Documentation @@ -24,6 +85,8 @@ ## Maintenance +* **ci:** sync area labels to prevent dedup +* **ci:** update changelog with latest changes * **ci:** update changelog with latest changes * **ci:** add manual trigger for docs * **ci:** update changelog with latest changes @@ -35,13 +98,13 @@ * **ci:** readd changelog step on release * **ci:** move changelog generation to rebuild_latest_doc workflow * **ci:** drop 3.6 from workflows -* **deps:** bump jsii from 1.57.0 to 1.63.2 ([#1400](https://github.com/awslabs/aws-lambda-powertools-python/issues/1400)) -* **deps:** bump constructs from 10.1.1 to 10.1.65 ([#1407](https://github.com/awslabs/aws-lambda-powertools-python/issues/1407)) +* **deps:** bump constructs from 10.1.1 to 10.1.60 ([#1399](https://github.com/awslabs/aws-lambda-powertools-python/issues/1399)) * **deps:** bump constructs from 10.1.1 to 10.1.66 ([#1414](https://github.com/awslabs/aws-lambda-powertools-python/issues/1414)) +* **deps:** bump jsii from 1.57.0 to 1.63.2 ([#1400](https://github.com/awslabs/aws-lambda-powertools-python/issues/1400)) * **deps:** bump constructs from 10.1.1 to 10.1.64 ([#1405](https://github.com/awslabs/aws-lambda-powertools-python/issues/1405)) * **deps:** bump attrs from 21.4.0 to 22.1.0 ([#1397](https://github.com/awslabs/aws-lambda-powertools-python/issues/1397)) * **deps:** bump constructs from 10.1.1 to 10.1.63 ([#1402](https://github.com/awslabs/aws-lambda-powertools-python/issues/1402)) -* **deps:** bump constructs from 10.1.1 to 10.1.60 ([#1399](https://github.com/awslabs/aws-lambda-powertools-python/issues/1399)) +* **deps:** bump constructs from 10.1.1 to 10.1.65 ([#1407](https://github.com/awslabs/aws-lambda-powertools-python/issues/1407)) * **deps-dev:** bump types-requests from 2.28.5 to 2.28.6 ([#1401](https://github.com/awslabs/aws-lambda-powertools-python/issues/1401)) * **deps-dev:** bump types-requests from 2.28.6 to 2.28.7 ([#1406](https://github.com/awslabs/aws-lambda-powertools-python/issues/1406)) * **docs:** remove pause sentence from roadmap ([#1409](https://github.com/awslabs/aws-lambda-powertools-python/issues/1409)) @@ -2193,7 +2256,8 @@ * Merge pull request [#5](https://github.com/awslabs/aws-lambda-powertools-python/issues/5) from jfuss/feat/python38 -[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.26.7...HEAD +[Unreleased]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.27.0...HEAD +[v1.27.0]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.26.7...v1.27.0 [v1.26.7]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.26.6...v1.26.7 [v1.26.6]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.26.5...v1.26.6 [v1.26.5]: https://github.com/awslabs/aws-lambda-powertools-python/compare/v1.26.4...v1.26.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bef5fa2052f..b6d29d7b00a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,20 @@ -[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/) + +# Table of contents + +- [Contributing Guidelines](#contributing-guidelines) + - [Reporting Bugs/Feature Requests](#reporting-bugsfeature-requests) + - [Contributing via Pull Requests](#contributing-via-pull-requests) + - [Dev setup](#dev-setup) + - [Local documentation](#local-documentation) + - [Conventions](#conventions) + - [General terminology and practices](#general-terminology-and-practices) + - [Testing definition](#testing-definition) + - [Finding contributions to work on](#finding-contributions-to-work-on) + - [Code of Conduct](#code-of-conduct) + - [Security issue notifications](#security-issue-notifications) + - [Troubleshooting](#troubleshooting) + - [API reference documentation](#api-reference-documentation) + - [Licensing](#licensing) # Contributing Guidelines @@ -25,9 +41,11 @@ Contributions via pull requests are much appreciated. Before sending us a pull r ### Dev setup +[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/) + Firstly, [fork the repository](https://github.com/awslabs/aws-lambda-powertools-python/fork). -To setup your development environment, we recommend using our pre-configured Cloud environment: https://gitpod.io/#https://github.com/YOUR_USERNAME/aws-lambda-powertools-python. Replace YOUR_USERNAME with your GitHub username or organization so the Cloud environment can target your fork accordingly. +To setup your development environment, we recommend using our pre-configured Cloud environment: . Replace YOUR_USERNAME with your GitHub username or organization so the Cloud environment can target your fork accordingly. Alternatively, you can use `make dev` within your local virtual environment. @@ -47,22 +65,38 @@ GitHub provides additional document on [forking a repository](https://help.githu You might find useful to run both the documentation website and the API reference locally while contributing: -* **API reference**: `make docs-api-local` -* **Docs website**: `make docs-local` - - If you prefer using Docker: `make docs-local-docker` +- **API reference**: `make docs-api-local` +- **Docs website**: `make docs-local` + - If you prefer using Docker: `make docs-local-docker` + +## Conventions + +### General terminology and practices -### Conventions +| Category | Convention | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Docstring** | We use a slight variation of Numpy convention with markdown to help generate more readable API references. | +| **Style guide** | We use black as well as flake8 extensions to enforce beyond good practices [PEP8](https://pep8.org/). We use type annotations and enforce static type checking at CI (mypy). | +| **Core utilities** | Core utilities use a Class, always accept `service` as a constructor parameter, can work in isolation, and are also available in other languages implementation. | +| **Utilities** | Utilities are not as strict as core and focus on solving a developer experience problem while following the project [Tenets](https://awslabs.github.io/aws-lambda-powertools-python/#tenets). | +| **Exceptions** | Specific exceptions live within utilities themselves and use `Error` suffix e.g. `MetricUnitError`. | +| **Git commits** | We follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). We do not enforce conventional commits on contributors to lower the entry bar. Instead, we enforce a conventional PR title so our label automation and changelog are generated correctly. | +| **API documentation** | API reference docs are generated from docstrings which should have Examples section to allow developers to have what they need within their own IDE. Documentation website covers the wider usage, tips, and strive to be concise. | +| **Documentation** | We treat it like a product. We sub-divide content aimed at getting started (80% of customers) vs advanced usage (20%). We also ensure customers know how to unit test their code when using our features. | -Category | Convention -------------------------------------------------- | --------------------------------------------------------------------------------- -**Docstring** | We use a slight variation of Numpy convention with markdown to help generate more readable API references. -**Style guide** | We use black as well as flake8 extensions to enforce beyond good practices [PEP8](https://pep8.org/). We use type annotations and enforce static type checking at CI (mypy). -**Core utilities** | Core utilities use a Class, always accept `service` as a constructor parameter, can work in isolation, and are also available in other languages implementation. -**Utilities** | Utilities are not as strict as core and focus on solving a developer experience problem while following the project [Tenets](https://awslabs.github.io/aws-lambda-powertools-python/#tenets). -**Exceptions** | Specific exceptions live within utilities themselves and use `Error` suffix e.g. `MetricUnitError`. -**Git commits** | We follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). We do not enforce conventional commits on contributors to lower the entry bar. Instead, we enforce a conventional PR title so our label automation and changelog are generated correctly. -**API documentation** | API reference docs are generated from docstrings which should have Examples section to allow developers to have what they need within their own IDE. Documentation website covers the wider usage, tips, and strive to be concise. -**Documentation** | We treat it like a product. We sub-divide content aimed at getting started (80% of customers) vs advanced usage (20%). We also ensure customers know how to unit test their code when using our features. +### Testing definition + +We group tests in different categories + +| Test | When to write | Notes | Speed | +| ----------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Unit tests | Verify the smallest possible unit works. | Networking access is prohibited. Prefer Functional tests given our complexity. | Lightning fast (nsec to ms) | +| Functional tests | Guarantee functionality works as expected. It's a subset of integration test covering multiple units. | No external dependency. Prefer Fake implementations (in-memory) over Mocks and Stubs. | Fast (ms to few seconds at worst) | +| Integration tests | Gain confidence that code works with one or more external dependencies. | No need for a Lambda function. Use our code base against an external dependency _e.g., fetch an existing SSM parameter_. | Moderate to slow (a few minutes) | +| End-to-end tests | Gain confidence that a Lambda function with our code operates as expected. | It simulates how customers configure, deploy, and run their Lambda function - Event Source configuration, IAM permissions, etc. | Slow (minutes) | +| Performance tests | Ensure critical operations won't increase latency and costs to customers. | CI arbitrary hardware can make it flaky. We'll resume writing perf test after our new Integ/End have significant coverage. | Fast to moderate (a few seconds to a few minutes) | + +**NOTE**: Functional tests are mandatory. We have plans to create a guide on how to create these different tests. Maintainers will help indicate whether additional tests are necessary and provide assistance as required. ## Finding contributions to work on @@ -75,6 +109,7 @@ For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of opensource-codeofconduct@amazon.com with any additional questions or comments. ## Security issue notifications + If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## Troubleshooting @@ -85,9 +120,9 @@ When you are working on the codebase and you use the local API reference documen This happens when: -* You did not install the local dev environment yet +- You did not install the local dev environment yet - You can install dev deps with `make dev` command -* The code in the repository is raising an exception while the `pdoc` is scanning the codebase +- The code in the repository is raising an exception while the `pdoc` is scanning the codebase - Unfortunately, this exception is not shown to you, but if you run, `poetry run pdoc --pdf aws_lambda_powertools`, the exception is shown and you can prevent the exception from being raised - Once resolved the documentation should load correctly again diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 16bc8ab1161..63f2b3eb8f8 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -15,6 +15,8 @@ - [Releasing a new version](#releasing-a-new-version) - [Drafting release notes](#drafting-release-notes) - [Run end to end tests](#run-end-to-end-tests) + - [Structure](#structure) + - [Workflow](#workflow) - [Releasing a documentation hotfix](#releasing-a-documentation-hotfix) - [Maintain Overall Health of the Repo](#maintain-overall-health-of-the-repo) - [Manage Roadmap](#manage-roadmap) @@ -37,13 +39,14 @@ This is document explains who the maintainers are (see below), what they do in t ## Current Maintainers -| Maintainer | GitHub ID | Affiliation | -| ---------------- | ----------------------------------------------- | ----------- | -| Heitor Lessa | [heitorlessa](https://github.com/heitorlessa) | Amazon | -| Alexander Melnyk | [am29d](https://github.com/am29d) | Amazon | -| Michal Ploski | [mploski](https://github.com/mploski) | Amazon | -| Simon Thulbourn | [sthulb](https://github.com/sthulb) | Amazon | -| Ruben Fonseca | [rubenfonseca](https://github.com/rubenfonseca) | Amazon | +| Maintainer | GitHub ID | Affiliation | +| ----------------- | ------------------------------------------------------- | ----------- | +| Heitor Lessa | [heitorlessa](https://github.com/heitorlessa) | Amazon | +| Alexander Melnyk | [am29d](https://github.com/am29d) | Amazon | +| Michal Ploski | [mploski](https://github.com/mploski) | Amazon | +| Simon Thulbourn | [sthulb](https://github.com/sthulb) | Amazon | +| Ruben Fonseca | [rubenfonseca](https://github.com/rubenfonseca) | Amazon | +| Leandro Damascena | [leandrodamascena](https://github.com/leandrodamascena) | Amazon | ## Emeritus @@ -211,11 +214,85 @@ This will kick off the [Publishing workflow](https://github.com/awslabs/aws-lamb ### Run end to end tests -In order to run end to end tests you need to install CDK CLI first and bootstrap your account with `cdk bootstrap` command. For additional details follow [documentation](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). - -To run locally, export `AWS_PROFILE` environment variable and run `make e2e tests`. To run from GitHub Actions, use [run-e2e-tests workflow](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/run-e2e-tests.yml) and pick the branch you want to run tests against. - -**NOTE**: E2E tests are run as part of each merge to `develop` branch. +E2E tests are run on every push to `develop` or manually via [run-e2e-tests workflow](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/run-e2e-tests.yml). + +To run locally, you need [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_prerequisites) and an [account bootstrapped](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) (`cdk bootstrap`). With a default AWS CLI profile configured, or `AWS_PROFILE` environment variable set, run `make e2e tests`. + +#### Structure + +Our E2E framework relies on pytest fixtures to coordinate infrastructure and test parallelization (see [Workflow](#workflow)). You'll notice multiple `conftest.py`, `infrastructure.py`, and `handlers`. + +- **`infrastructure`**. Uses CDK to define what a Stack for a given feature should look like. It inherits from `BaseInfrastructure` to handle all boilerplate and deployment logic necessary. +- **`conftest.py`**. Imports and deploys a given feature Infrastructure. Hierarchy matters. Top-level `conftest` deploys stacks only once and blocks I/O across all CPUs. Feature-level `conftest` deploys stacks in parallel, and once complete run all tests in parallel. +- **`handlers`**. Lambda function handlers that will be automatically deployed and exported as PascalCase for later use. + +```shell +. +├── __init__.py +├── conftest.py # deploys Lambda Layer stack +├── logger +│ ├── __init__.py +│ ├── conftest.py # deploys LoggerStack +│ ├── handlers +│ │ └── basic_handler.py +│ ├── infrastructure.py # LoggerStack definition +│ └── test_logger.py +├── metrics +│ ├── __init__.py +│ ├── conftest.py # deploys MetricsStack +│ ├── handlers +│ │ ├── basic_handler.py +│ │ └── cold_start.py +│ ├── infrastructure.py # MetricsStack definition +│ └── test_metrics.py +├── tracer +│ ├── __init__.py +│ ├── conftest.py # deploys TracerStack +│ ├── handlers +│ │ ├── async_capture.py +│ │ └── basic_handler.py +│ ├── infrastructure.py # TracerStack definition +│ └── test_tracer.py +└── utils + ├── Dockerfile + ├── __init__.py + ├── data_builder # build_service_name(), build_add_dimensions_input, etc. + ├── data_fetcher # get_traces(), get_logs(), get_lambda_response(), etc. + ├── infrastructure.py # base infrastructure like deploy logic, Layer Stack, etc. +``` + +#### Workflow + +We parallelize our end-to-end tests to benefit from speed and isolate Lambda functions to ease assessing side effects (e.g., traces, logs, etc.). The following diagram demonstrates the process we take every time you use `make e2e`: + +```mermaid +graph TD + A[make e2e test] -->Spawn{"Split and group tests
by feature and CPU"} + + Spawn -->|Worker0| Worker0_Start["Load tests"] + Spawn -->|Worker1| Worker1_Start["Load tests"] + Spawn -->|WorkerN| WorkerN_Start["Load tests"] + + Worker0_Start -->|Wait| LambdaLayerStack["Lambda Layer Stack Deployment"] + Worker1_Start -->|Wait| LambdaLayerStack["Lambda Layer Stack Deployment"] + WorkerN_Start -->|Wait| LambdaLayerStack["Lambda Layer Stack Deployment"] + + LambdaLayerStack -->|Worker0| Worker0_Deploy["Launch feature stack"] + LambdaLayerStack -->|Worker1| Worker1_Deploy["Launch feature stack"] + LambdaLayerStack -->|WorkerN| WorkerN_Deploy["Launch feature stack"] + + Worker0_Deploy -->|Worker0| Worker0_Tests["Run tests"] + Worker1_Deploy -->|Worker1| Worker1_Tests["Run tests"] + WorkerN_Deploy -->|WorkerN| WorkerN_Tests["Run tests"] + + Worker0_Tests --> ResultCollection + Worker1_Tests --> ResultCollection + WorkerN_Tests --> ResultCollection + + ResultCollection{"Wait for workers
Collect test results"} + ResultCollection --> TestEnd["Report results"] + ResultCollection --> DeployEnd["Delete Stacks"] +``` ### Releasing a documentation hotfix diff --git a/Makefile b/Makefile index 94f9fc975b8..7a212738c53 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ unit-test: poetry run pytest tests/unit e2e-test: - poetry run pytest -rP -n 3 --dist loadscope --durations=0 --durations-min=1 tests/e2e + python parallel_run_e2e.py coverage-html: poetry run pytest -m "not perf" --ignore tests/e2e --cov=aws_lambda_powertools --cov-report=html diff --git a/README.md b/README.md index 81cd3f3ce7f..2065a983342 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # AWS Lambda Powertools for Python -![Build](https://github.com/awslabs/aws-lambda-powertools/workflows/Powertools%20Python/badge.svg?branch=master) +[![Build](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/python_build.yml/badge.svg)](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/python_build.yml) [![codecov.io](https://codecov.io/github/awslabs/aws-lambda-powertools-python/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/awslabs/aws-lambda-powertools-python) ![PythonSupport](https://img.shields.io/static/v1?label=python&message=3.6%20|%203.7|%203.8|%203.9&color=blue?style=flat-square&logo=python) ![PyPI version](https://badge.fury.io/py/aws-lambda-powertools.svg) ![PyPi monthly downloads](https://img.shields.io/pypi/dm/aws-lambda-powertools) +[![Join our Discord](https://dcbadge.vercel.app/api/server/B8zZKbbyET)](https://discord.gg/B8zZKbbyET) A suite of Python utilities for AWS Lambda functions to ease adopting best practices such as tracing, structured logging, custom metrics, and more. (AWS Lambda Powertools [Java](https://github.com/awslabs/aws-lambda-powertools-java) and [Typescript](https://github.com/awslabs/aws-lambda-powertools-typescript) is also available). @@ -27,12 +28,10 @@ A suite of Python utilities for AWS Lambda functions to ease adopting best pract * **[Idempotency](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/idempotency/)** - Convert your Lambda functions into idempotent operations which are safe to retry * **[Feature Flags](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/feature_flags/)** - A simple rule engine to evaluate when one or multiple features should be enabled depending on the input - ### Installation With [pip](https://pip.pypa.io/en/latest/index.html) installed, run: ``pip install aws-lambda-powertools`` - ## Tutorial and Examples * [Tutorial](https://awslabs.github.io/aws-lambda-powertools-python/latest/tutorial) @@ -47,7 +46,9 @@ With [pip](https://pip.pypa.io/en/latest/index.html) installed, run: ``pip insta * Powertools idea [DAZN Powertools](https://github.com/getndazn/dazn-lambda-powertools/) ## Connect -**Email**: aws-lambda-powertools-feedback@amazon.com + +* **AWS Lambda Powertools on Discord**: `#python` - **[Invite link](https://discord.gg/B8zZKbbyET)** +* **Email**: aws-lambda-powertools-feedback@amazon.com ## Security disclosures diff --git a/aws_lambda_powertools/event_handler/appsync.py b/aws_lambda_powertools/event_handler/appsync.py index 896b303cd08..4ddc51cd102 100644 --- a/aws_lambda_powertools/event_handler/appsync.py +++ b/aws_lambda_powertools/event_handler/appsync.py @@ -1,5 +1,4 @@ import logging -from abc import ABC from typing import Any, Callable, Optional, Type, TypeVar from aws_lambda_powertools.utilities.data_classes import AppSyncResolverEvent @@ -10,7 +9,7 @@ AppSyncResolverEventT = TypeVar("AppSyncResolverEventT", bound=AppSyncResolverEvent) -class BaseRouter(ABC): +class BaseRouter: current_event: AppSyncResolverEventT # type: ignore[valid-type] lambda_context: LambdaContext diff --git a/aws_lambda_powertools/shared/constants.py b/aws_lambda_powertools/shared/constants.py index 45b46d236f9..48d94d88f1d 100644 --- a/aws_lambda_powertools/shared/constants.py +++ b/aws_lambda_powertools/shared/constants.py @@ -23,3 +23,12 @@ XRAY_SDK_CORE_MODULE: str = "aws_xray_sdk.core" IDEMPOTENCY_DISABLED_ENV: str = "POWERTOOLS_IDEMPOTENCY_DISABLED" + +LOGGER_LAMBDA_CONTEXT_KEYS = [ + "function_arn", + "function_memory_size", + "function_name", + "function_request_id", + "cold_start", + "xray_trace_id", +] diff --git a/aws_lambda_powertools/utilities/batch/sqs.py b/aws_lambda_powertools/utilities/batch/sqs.py index 0848e327ea6..7b234c1372e 100644 --- a/aws_lambda_powertools/utilities/batch/sqs.py +++ b/aws_lambda_powertools/utilities/batch/sqs.py @@ -6,6 +6,7 @@ import logging import math import sys +import warnings from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Any, Callable, Dict, List, Optional, Tuple, cast @@ -77,6 +78,14 @@ def __init__( self.suppress_exception = suppress_exception self.max_message_batch = 10 + warnings.warn( + "The sqs_batch_processor decorator and PartialSQSProcessor class are now deprecated, " + "and will be removed in the next major version. " + "Please follow the upgrade guide at " + "https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/batch/#legacy " + "to use the native batch_processor decorator or BatchProcessor class." + ) + super().__init__() def _get_queue_url(self) -> Optional[str]: diff --git a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py index 2b88918f17b..01c1a83f5db 100644 --- a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py +++ b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py @@ -7,7 +7,7 @@ class LambdaFunctionUrlEvent(APIGatewayProxyEventV2): Notes: ----- Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. - + Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`). Documentation: diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py index 1b118d28117..7d42fd81ad6 100644 --- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py +++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py @@ -5,6 +5,7 @@ from .dynamodb import DynamoDBStreamEnvelope from .event_bridge import EventBridgeEnvelope from .kinesis import KinesisDataStreamEnvelope +from .lambda_function_url import LambdaFunctionUrlEnvelope from .sns import SnsEnvelope, SnsSqsEnvelope from .sqs import SqsEnvelope @@ -15,6 +16,7 @@ "DynamoDBStreamEnvelope", "EventBridgeEnvelope", "KinesisDataStreamEnvelope", + "LambdaFunctionUrlEnvelope", "SnsEnvelope", "SnsSqsEnvelope", "SqsEnvelope", diff --git a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py new file mode 100644 index 00000000000..e54fb081b65 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py @@ -0,0 +1,32 @@ +import logging +from typing import Any, Dict, Optional, Type, Union + +from ..models import LambdaFunctionUrlModel +from ..types import Model +from .base import BaseEnvelope + +logger = logging.getLogger(__name__) + + +class LambdaFunctionUrlEnvelope(BaseEnvelope): + """Lambda function URL envelope to extract data within body key""" + + def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]: + """Parses data found with model provided + + Parameters + ---------- + data : Dict + Lambda event to be parsed + model : Type[Model] + Data model provided to parse after extracting data using envelope + + Returns + ------- + Any + Parsed detail payload with model provided + """ + logger.debug(f"Parsing incoming data with Lambda function URL model {LambdaFunctionUrlModel}") + parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data) + logger.debug(f"Parsing event payload in `detail` with {model}") + return self._parse(data=parsed_envelope.body, model=model) diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py index e3fb50a2d5d..11ab6501fa9 100644 --- a/aws_lambda_powertools/utilities/parser/models/__init__.py +++ b/aws_lambda_powertools/utilities/parser/models/__init__.py @@ -18,6 +18,7 @@ from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel from .event_bridge import EventBridgeModel from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload +from .lambda_function_url import LambdaFunctionUrlModel from .s3 import S3Model, S3RecordModel from .s3_object_event import ( S3ObjectConfiguration, @@ -66,6 +67,7 @@ "KinesisDataStreamModel", "KinesisDataStreamRecord", "KinesisDataStreamRecordPayload", + "LambdaFunctionUrlModel", "S3Model", "S3RecordModel", "S3ObjectLambdaEvent", diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py index f97dad3bcb0..cb1f830bb47 100644 --- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py +++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py @@ -20,7 +20,7 @@ class RequestContextV2AuthorizerIam(BaseModel): principalOrgId: Optional[str] userArn: Optional[str] userId: Optional[str] - cognitoIdentity: RequestContextV2AuthorizerIamCognito + cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito] class RequestContextV2AuthorizerJwt(BaseModel): diff --git a/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py new file mode 100644 index 00000000000..2088ab9fa04 --- /dev/null +++ b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model + + +class LambdaFunctionUrlModel(APIGatewayProxyEventV2Model): + """AWS Lambda Function URL model + + Notes: + ----- + Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. + + Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`). + + Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + """ + + pass diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 1358f545eb8..f4f45a051f8 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -135,9 +135,6 @@ Each dynamic route you set must be part of your function signature. This allows ???+ tip You can also nest dynamic paths, for example `/todos//`. -???+ tip - You can also nest dynamic paths, for example `/todos//`. - #### Catch-all routes ???+ note diff --git a/docs/index.md b/docs/index.md index 17808c89917..95ce2c2a707 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ A suite of utilities for AWS Lambda functions to ease adopting best practices su Powertools is available in the following formats: -* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:28**](#){: .copyMe}:clipboard: +* **Lambda Layer**: [**arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:29**](#){: .copyMe}:clipboard: * **PyPi**: **`pip install aws-lambda-powertools`** ???+ hint "Support this project by using Lambda Layers :heart:" @@ -32,23 +32,23 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: | Region | Layer ARN | | ---------------- | -------------------------------------------------------------------------------------------------------- | - | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | - | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:28](#){: .copyMe}:clipboard: | + | `us-east-1` | [arn:aws:lambda:us-east-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `us-east-2` | [arn:aws:lambda:us-east-2:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `us-west-1` | [arn:aws:lambda:us-west-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `us-west-2` | [arn:aws:lambda:us-west-2:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-south-1` | [arn:aws:lambda:ap-south-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-northeast-1` | [arn:aws:lambda:ap-northeast-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-northeast-2` | [arn:aws:lambda:ap-northeast-2:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-northeast-3` | [arn:aws:lambda:ap-northeast-3:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-southeast-1` | [arn:aws:lambda:ap-southeast-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ap-southeast-2` | [arn:aws:lambda:ap-southeast-2:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `eu-central-1` | [arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `eu-west-1` | [arn:aws:lambda:eu-west-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `eu-west-2` | [arn:aws:lambda:eu-west-2:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `eu-west-3` | [arn:aws:lambda:eu-west-3:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `eu-north-1` | [arn:aws:lambda:eu-north-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `ca-central-1` | [arn:aws:lambda:ca-central-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | + | `sa-east-1` | [arn:aws:lambda:sa-east-1:017000801446:layer:AWSLambdaPowertoolsPython:29](#){: .copyMe}:clipboard: | ??? question "Can't find our Lambda Layer for your preferred AWS region?" You can use [Serverless Application Repository (SAR)](#sar) method, our [CDK Layer Construct](https://github.com/aws-samples/cdk-lambda-powertools-python-layer){target="_blank"}, or PyPi like you normally would for any other library. @@ -62,7 +62,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: Type: AWS::Serverless::Function Properties: Layers: - - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:28 + - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:29 ``` === "Serverless framework" @@ -72,7 +72,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: hello: handler: lambda_function.lambda_handler layers: - - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:28 + - arn:aws:lambda:${aws:region}:017000801446:layer:AWSLambdaPowertoolsPython:29 ``` === "CDK" @@ -88,7 +88,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: powertools_layer = aws_lambda.LayerVersion.from_layer_version_arn( self, id="lambda-powertools", - layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:28" + layer_version_arn=f"arn:aws:lambda:{env.region}:017000801446:layer:AWSLambdaPowertoolsPython:29" ) aws_lambda.Function(self, 'sample-app-lambda', @@ -137,7 +137,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: role = aws_iam_role.iam_for_lambda.arn handler = "index.test" runtime = "python3.9" - layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:28"] + layers = ["arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:29"] source_code_hash = filebase64sha256("lambda_function_payload.zip") } @@ -156,7 +156,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: ? Do you want to configure advanced settings? Yes ... ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:28 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:29 ❯ amplify push -y @@ -167,7 +167,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: - Name: ? Which setting do you want to update? Lambda layers configuration ? Do you want to enable Lambda layers for this function? Yes - ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:28 + ? Enter up to 5 existing Lambda layer ARNs (comma-separated): arn:aws:lambda:eu-central-1:017000801446:layer:AWSLambdaPowertoolsPython:29 ? Do you want to edit the local lambda function now? No ``` @@ -175,7 +175,7 @@ You can include Lambda Powertools Lambda Layer using [AWS Lambda Console](https: Change {region} to your AWS region, e.g. `eu-west-1` ```bash title="AWS CLI" - aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:28 --region {region} + aws lambda get-layer-version-by-arn --arn arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPython:29 --region {region} ``` The pre-signed URL to download this Lambda Layer will be within `Location` key. diff --git a/docs/media/middleware_factory_tracer_1.png b/docs/media/middleware_factory_tracer_1.png new file mode 100644 index 00000000000..70c1a0c1da1 Binary files /dev/null and b/docs/media/middleware_factory_tracer_1.png differ diff --git a/docs/media/middleware_factory_tracer_2.png b/docs/media/middleware_factory_tracer_2.png new file mode 100644 index 00000000000..54f89179565 Binary files /dev/null and b/docs/media/middleware_factory_tracer_2.png differ diff --git a/docs/media/utilities_typing.png b/docs/media/utilities_typing.png deleted file mode 100644 index 0f293abb6ec..00000000000 Binary files a/docs/media/utilities_typing.png and /dev/null differ diff --git a/docs/media/utilities_typing_1.png b/docs/media/utilities_typing_1.png new file mode 100644 index 00000000000..8476b9f093e Binary files /dev/null and b/docs/media/utilities_typing_1.png differ diff --git a/docs/media/utilities_typing_2.png b/docs/media/utilities_typing_2.png new file mode 100644 index 00000000000..2a045df0a07 Binary files /dev/null and b/docs/media/utilities_typing_2.png differ diff --git a/docs/media/utilities_typing_3.png b/docs/media/utilities_typing_3.png new file mode 100644 index 00000000000..88888f506c3 Binary files /dev/null and b/docs/media/utilities_typing_3.png differ diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 0af326afb24..e7908bbfa32 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,5 +1,10 @@ {% extends "base.html" %} +{% block announce %} + 👋 Powertools for Python v2 is coming soon! + We encourage you to add your feedback and follow the progress on the RFC. +{% endblock %} + {% block outdated %} You're not viewing the latest version. diff --git a/docs/utilities/batch.md b/docs/utilities/batch.md index ce2e76e25d4..6241179ed4e 100644 --- a/docs/utilities/batch.md +++ b/docs/utilities/batch.md @@ -5,6 +5,11 @@ description: Utility The batch processing utility handles partial failures when processing batches from Amazon SQS, Amazon Kinesis Data Streams, and Amazon DynamoDB Streams. +???+ warning + The legacy `sqs_batch_processor` decorator and `PartialSQSProcessor` class are deprecated and are going to be removed soon. + + Please check the [migration guide](#migration-guide) for more information. + ## Key Features * Reports batch item failures to reduce number of retries for a record upon errors diff --git a/docs/utilities/jmespath_functions.md b/docs/utilities/jmespath_functions.md index eee88c13cfb..209bf4fffe9 100644 --- a/docs/utilities/jmespath_functions.md +++ b/docs/utilities/jmespath_functions.md @@ -3,6 +3,8 @@ title: JMESPath Functions description: Utility --- + + ???+ tip JMESPath is a query language for JSON used by AWS CLI, AWS Python SDK, and AWS Lambda Powertools for Python. @@ -12,221 +14,164 @@ Built-in [JMESPath](https://jmespath.org/){target="_blank"} Functions to easily * Deserialize JSON from JSON strings, base64, and compressed data * Use JMESPath to extract and combine data recursively +* Provides commonly used JMESPath expression with popular event sources ## Getting started +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + You might have events that contains encoded JSON payloads as string, base64, or even in compressed format. It is a common use case to decode and extract them partially or fully as part of your Lambda function invocation. -Lambda Powertools also have utilities like [validation](validation.md), [idempotency](idempotency.md), or [feature flags](feature_flags.md) where you might need to extract a portion of your data before using them. +Powertools also have utilities like [validation](validation.md), [idempotency](idempotency.md), or [feature flags](feature_flags.md) where you might need to extract a portion of your data before using them. -???+ info - **Envelope** is the terminology we use for the JMESPath expression to extract your JSON object from your data input. +???+ info "Terminology" + **Envelope** is the terminology we use for the **JMESPath expression** to extract your JSON object from your data input. We might use those two terms interchangeably. ### Extracting data -You can use the `extract_data_from_envelope` function along with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}. - -=== "app.py" - - ```python hl_lines="1 7" - from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope - - from aws_lambda_powertools.utilities.typing import LambdaContext +You can use the `extract_data_from_envelope` function with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank"}. +???+ tip + Another common use case is to fetch deeply nested data, filter, flatten, and more. - def handler(event: dict, context: LambdaContext): - payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)") - customer = payload.get("customerId") # now deserialized - ... - ``` +=== "extract_data_from_envelope.py" + ```python hl_lines="1 6 10" + --8<-- "examples/jmespath_functions/src/extract_data_from_envelope.py" + ``` -=== "event.json" +=== "extract_data_from_envelope.json" ```json - { - "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}" - } + --8<-- "examples/jmespath_functions/src/extract_data_from_envelope.json" ``` ### Built-in envelopes -We provide built-in envelopes for popular JMESPath expressions used when looking to decode/deserialize JSON objects within AWS Lambda Event Sources. +We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects. -=== "app.py" +=== "extract_data_from_builtin_envelope.py" - ```python hl_lines="1 7" - from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope, envelopes - - from aws_lambda_powertools.utilities.typing import LambdaContext - - - def handler(event: dict, context: LambdaContext): - payload = extract_data_from_envelope(data=event, envelope=envelopes.SNS) - customer = payload.get("customerId") # now deserialized - ... + ```python hl_lines="1 6" + --8<-- "examples/jmespath_functions/src/extract_data_from_builtin_envelope.py" ``` -=== "event.json" - - ```json hl_lines="6" - { - "Records": [ - { - "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", - "receiptHandle": "MessageReceiptHandle", - "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\",\"booking\":{\"id\":\"5b2c4803-330b-42b7-811a-c68689425de1\",\"reference\":\"ySz7oA\",\"outboundFlightId\":\"20c0d2f2-56a3-4068-bf20-ff7703db552d\"},\"payment\":{\"receipt\":\"https:\/\/pay.stripe.com\/receipts\/acct_1Dvn7pF4aIiftV70\/ch_3JTC14F4aIiftV700iFq2CHB\/rcpt_K7QsrFln9FgFnzUuBIiNdkkRYGxUL0X\",\"amount\":100}}", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1523232000000", - "SenderId": "123456789012", - "ApproximateFirstReceiveTimestamp": "1523232000001" - }, - "messageAttributes": {}, - "md5OfBody": "7b270e59b47ff90a553787216d55d91d", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", - "awsRegion": "us-east-1" - } - ] - } +=== "extract_data_from_builtin_envelope.json" + + ```json hl_lines="6 15" + --8<-- "examples/jmespath_functions/src/extract_data_from_builtin_envelope.json" ``` These are all built-in envelopes you can use along with their expression as a reference: -Envelope | JMESPath expression -------------------------------------------------- | --------------------------------------------------------------------------------- -**`API_GATEWAY_REST`** | `powertools_json(body)` -**`API_GATEWAY_HTTP`** | `API_GATEWAY_REST` -**`SQS`** | `Records[*].powertools_json(body)` -**`SNS`** | `Records[0].Sns.Message | powertools_json(@)` -**`EVENTBRIDGE`** | `detail` -**`CLOUDWATCH_EVENTS_SCHEDULED`** | `EVENTBRIDGE` -**`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))` -**`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]` +| Envelope | JMESPath expression | +| --------------------------------- | ------------------------------------------------------------- | +| **`API_GATEWAY_REST`** | `powertools_json(body)` | +| **`API_GATEWAY_HTTP`** | `API_GATEWAY_REST` | +| **`SQS`** | `Records[*].powertools_json(body)` | +| **`SNS`** | `Records[0].Sns.Message | powertools_json(@)` | +| **`EVENTBRIDGE`** | `detail` | +| **`CLOUDWATCH_EVENTS_SCHEDULED`** | `EVENTBRIDGE` | +| **`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))` | +| **`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]` | ## Advanced ### Built-in JMESPath functions -You can use our built-in JMESPath functions within your expressions to do exactly that to decode JSON Strings, base64, and uncompress gzip data. +You can use our built-in JMESPath functions within your envelope expression. They handle deserialization for common data formats found in AWS Lambda event sources such as JSON strings, base64, and uncompress gzip data. ???+ info - We use these for built-in envelopes to easily decode and unwrap events from sources like API Gateway, Kinesis, CloudWatch Logs, etc. + We use these everywhere in Powertools to easily decode and unwrap events from Amazon API Gateway, Amazon Kinesis, AWS CloudWatch Logs, etc. #### powertools_json function -Use `powertools_json` function to decode any JSON String anywhere a JMESPath expression is allowed. +Use `powertools_json` function to decode any JSON string anywhere a JMESPath expression is allowed. > **Validation scenario** -This sample will decode the value within the `data` key into a valid JSON before we can validate it. +This sample will deserialize the JSON string within the `data` key before validation. === "powertools_json_jmespath_function.py" - ```python hl_lines="9" - from aws_lambda_powertools.utilities.validation import validate - - import schemas + ```python hl_lines="5 8 34 45 48 51" + --8<-- "examples/jmespath_functions/src/powertools_json_jmespath_function.py" + ``` - sample_event = { - 'data': '{"payload": {"message": "hello hello", "username": "blah blah"}}' - } +=== "powertools_json_jmespath_schema.py" - validate(event=sample_event, schema=schemas.INPUT, envelope="powertools_json(data)") + ```python hl_lines="7 8 10 12 17 19 24 26 31 33 38 40" + --8<-- "examples/jmespath_functions/src/powertools_json_jmespath_schema.py" ``` -=== "schemas.py" +=== "powertools_json_jmespath_payload.json" - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json + --8<-- "examples/jmespath_functions/src/powertools_json_jmespath_payload.json" ``` > **Idempotency scenario** -This sample will decode the value within the `body` key of an API Gateway event into a valid JSON object to ensure the Idempotency utility processes a JSON object instead of a string. - -```python hl_lines="7" title="Deserializing JSON before using as idempotency key" -import json -from aws_lambda_powertools.utilities.idempotency import ( - IdempotencyConfig, DynamoDBPersistenceLayer, idempotent -) - -persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") -config = IdempotencyConfig(event_key_jmespath="powertools_json(body)") - -@idempotent(config=config, persistence_store=persistence_layer) -def handler(event:APIGatewayProxyEvent, context): - body = json.loads(event['body']) - payment = create_subscription_payment( - user=body['user'], - product=body['product_id'] - ) - ... - return { - "payment_id": payment.id, - "message": "success", - "statusCode": 200 - } -``` +This sample will deserialize the JSON string within the `body` key before [Idempotency](./idempotency.md){target="_blank"} processes it. + +=== "powertools_json_idempotency_jmespath.py" + + ```python hl_lines="12" + --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py" + ``` + +=== "powertools_json_idempotency_jmespath.json" + + ```json hl_lines="28" + --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json" + ``` #### powertools_base64 function Use `powertools_base64` function to decode any base64 data. -This sample will decode the base64 value within the `data` key, and decode the JSON string into a valid JSON before we can validate it. +This sample will decode the base64 value within the `data` key, and deserialize the JSON string before validation. -=== "powertools_json_jmespath_function.py" - - ```python hl_lines="12" - from aws_lambda_powertools.utilities.validation import validate +=== "powertools_base64_jmespath_function.py" - import schemas + ```python hl_lines="7 10 37 49 53 55 57" + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_function.py" + ``` - sample_event = { - "data": "eyJtZXNzYWdlIjogImhlbGxvIGhlbGxvIiwgInVzZXJuYW1lIjogImJsYWggYmxhaCJ9=" - } +=== "powertools_base64_jmespath_schema.py" - validate( - event=sample_event, - schema=schemas.INPUT, - envelope="powertools_json(powertools_base64(data))" - ) + ```python hl_lines="7 8 10 12 17 19 24 26 31 33 38 40" + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_schema.py" ``` -=== "schemas.py" +=== "powertools_base64_jmespath_payload.json" - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_payload.json" ``` #### powertools_base64_gzip function Use `powertools_base64_gzip` function to decompress and decode base64 data. -This sample will decompress and decode base64 data, then use JMESPath pipeline expression to pass the result for decoding its JSON string. +This sample will decompress and decode base64 data from Cloudwatch Logs, then use JMESPath pipeline expression to pass the result for decoding its JSON string. -=== "powertools_json_jmespath_function.py" - - ```python hl_lines="12" - from aws_lambda_powertools.utilities.validation import validate +=== "powertools_base64_gzip_jmespath_function.py" - import schemas + ```python hl_lines="6 10 15 29 31 33 35" + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py" + ``` - sample_event = { - "data": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==" - } +=== "powertools_base64_gzip_jmespath_schema.py" - validate( - event=sample_event, - schema=schemas.INPUT, - envelope="powertools_base64_gzip(data) | powertools_json(@)" - ) + ```python hl_lines="7-15 17 19 24 26 31 33 38 40" + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py" ``` -=== "schemas.py" +=== "powertools_base64_gzip_jmespath_payload.json" - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json" ``` ### Bring your own JMESPath function @@ -234,35 +179,18 @@ This sample will decompress and decode base64 data, then use JMESPath pipeline e ???+ warning This should only be used for advanced use cases where you have special formats not covered by the built-in functions. -For special binary formats that you want to decode before applying JSON Schema validation, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"} and any additional option via `jmespath_options` param. - -In order to keep the built-in functions from Powertools, you can subclass from `PowertoolsFunctions`: - -=== "custom_jmespath_function.py" - - ```python hl_lines="2-3 6-9 11 17" - from aws_lambda_powertools.utilities.jmespath_utils import ( - PowertoolsFunctions, extract_data_from_envelope) - from jmespath.functions import signature - +For special binary formats that you want to decode before applying JSON Schema validation, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank"} and any additional option via `jmespath_options` param. To keep Powertools built-in functions, you can subclass from `PowertoolsFunctions`. - class CustomFunctions(PowertoolsFunctions): - @signature({'types': ['string']}) # Only decode if value is a string - def _func_special_decoder(self, s): - return my_custom_decoder_logic(s) +Here is an example of how to decompress messages using [snappy](https://github.com/andrix/python-snappy){target="_blank"}: - custom_jmespath_options = {"custom_functions": CustomFunctions()} +=== "powertools_custom_jmespath_function.py" - def handler(event, context): - # use the custom name after `_func_` - extract_data_from_envelope(data=event, - envelope="special_decoder(body)", - jmespath_options=**custom_jmespath_options) - ... + ```python hl_lines="8 11 14-15 20 31 36 38 40" + --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.py" ``` -=== "event.json" +=== "powertools_custom_jmespath_function.json" ```json - {"body": "custom_encoded_data"} + --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.json" ``` diff --git a/docs/utilities/middleware_factory.md b/docs/utilities/middleware_factory.md index 6133fb3c8af..70157ca1286 100644 --- a/docs/utilities/middleware_factory.md +++ b/docs/utilities/middleware_factory.md @@ -3,14 +3,23 @@ title: Middleware factory description: Utility --- + + Middleware factory provides a decorator factory to create your own middleware to run logic before, and after each Lambda invocation synchronously. ## Key features * Run logic before, after, and handle exceptions -* Trace each middleware when requested +* Built-in tracing opt-in capability + +## Getting started + +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + +You might need a custom middleware to abstract non-functional code. These are often custom authorization or any reusable logic you might need to run before/after a Lambda function invocation. -## Middleware with no params +### Middleware with no params You can create your own middleware using `lambda_handler_decorator`. The decorator factory expects 3 arguments in your function signature: @@ -18,74 +27,120 @@ You can create your own middleware using `lambda_handler_decorator`. The decorat * **event** - Lambda function invocation event * **context** - Lambda function context object -```python hl_lines="3-4 10" title="Creating your own middleware for before/after logic" -from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +### Middleware with before logic + +=== "getting_started_middleware_before_logic_function.py" + ```python hl_lines="5 23 24 29 30 32 37 38" + --8<-- "examples/middleware_factory/src/getting_started_middleware_before_logic_function.py" + ``` + +=== "getting_started_middleware_before_logic_payload.json" + + ```json hl_lines="9-13" + --8<-- "examples/middleware_factory/src/getting_started_middleware_before_logic_payload.json" + ``` -@lambda_handler_decorator -def middleware_before_after(handler, event, context): - # logic_before_handler_execution() - response = handler(event, context) - # logic_after_handler_execution() - return response +### Middleware with after logic -@middleware_before_after -def lambda_handler(event, context): - ... -``` +=== "getting_started_middleware_after_logic_function.py" + ```python hl_lines="7 14 15 21-23 37" + --8<-- "examples/middleware_factory/src/getting_started_middleware_after_logic_function.py" + ``` -## Middleware with params +=== "getting_started_middleware_after_logic_payload.json" + + ```json + --8<-- "examples/middleware_factory/src/getting_started_middleware_after_logic_payload.json" + ``` + +### Middleware with params You can also have your own keyword arguments after the mandatory arguments. -```python hl_lines="2 12" title="Accepting arbitrary keyword arguments" -@lambda_handler_decorator -def obfuscate_sensitive_data(handler, event, context, fields: List = None): - # Obfuscate email before calling Lambda handler - if fields: - for field in fields: - if field in event: - event[field] = obfuscate(event[field]) +=== "getting_started_middleware_with_params_function.py" + ```python hl_lines="6 27 28 29 33 49" + --8<-- "examples/middleware_factory/src/getting_started_middleware_with_params_function.py" + ``` + +=== "getting_started_middleware_with_params_payload.json" - return handler(event, context) + ```json hl_lines="18 19 20" + --8<-- "examples/middleware_factory/src/getting_started_middleware_with_params_payload.json" + ``` + +## Advanced + +For advanced use cases, you can instantiate [Tracer](../core/tracer.md) inside your middleware, and add annotations as well as metadata for additional operational insights. -@obfuscate_sensitive_data(fields=["email"]) -def lambda_handler(event, context): - ... -``` +=== "advanced_middleware_tracer_function.py" + ```python hl_lines="7 9 12 16 17 19 25 42" + --8<-- "examples/middleware_factory/src/advanced_middleware_tracer_function.py" + ``` -## Tracing middleware execution +=== "advanced_middleware_tracer_payload.json" + + ```json + --8<-- "examples/middleware_factory/src/advanced_middleware_tracer_payload.json" + ``` + +![Middleware advanced Tracer](../media/middleware_factory_tracer_2.png) + +### Tracing middleware **execution** If you are making use of [Tracer](../core/tracer.md), you can trace the execution of your middleware to ease operations. This makes use of an existing Tracer instance that you may have initialized anywhere in your code. -```python hl_lines="3" title="Tracing custom middlewares with Tracer" -from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +???+ warning + You must [enable Active Tracing](../core/tracer/#permissions) in your Lambda function when using this feature, otherwise Lambda cannot send traces to XRay. -@lambda_handler_decorator(trace_execution=True) -def my_middleware(handler, event, context): - return handler(event, context) +=== "getting_started_middleware_tracer_function.py" + ```python hl_lines="8 14 15 36" + --8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_function.py" + ``` -@my_middleware -def lambda_handler(event, context): - ... -``` +=== "getting_started_middleware_tracer_payload.json" -When executed, your middleware name will [appear in AWS X-Ray Trace details as](../core/tracer.md) `## middleware_name`. + ```json hl_lines="18 19 20" + --8<-- "examples/middleware_factory/src/getting_started_middleware_tracer_payload.json" + ``` -For advanced use cases, you can instantiate [Tracer](../core/tracer.md) inside your middleware, and add annotations as well as metadata for additional operational insights. +When executed, your middleware name will [appear in AWS X-Ray Trace details as](../core/tracer.md) `## middleware_name`, in this example the middleware name is `## middleware_with_tracing`. + +![Middleware simple Tracer](../media/middleware_factory_tracer_1.png) + +### Combining Powertools utilities + +You can create your own middleware and combine many features of Lambda Powertools such as [trace](../core/logger.md), [logs](../core/logger.md), [feature flags](feature_flags.md), [validation](validation.md), [jmespath_functions](jmespath_functions.md) and others to abstract non-functional code. + +In the example below, we create a Middleware with the following features: + +* Logs and traces +* Validate if the payload contains a specific header +* Extract specific keys from event +* Automatically add security headers on every execution +* Validate if a specific feature flag is enabled +* Save execution history to a DynamoDB table + +=== "combining_powertools_utilities_function.py" + ```python hl_lines="11 28 29 119 52 61 73" + --8<-- "examples/middleware_factory/src/combining_powertools_utilities_function.py" + ``` + +=== "combining_powertools_utilities_schema.py" + ```python hl_lines="12 14" + --8<-- "examples/middleware_factory/src/combining_powertools_utilities_schema.py" + ``` + +=== "combining_powertools_utilities_event.json" + ```python hl_lines="10" + --8<-- "examples/middleware_factory/src/combining_powertools_utilities_event.json" + ``` -```python hl_lines="6-8" title="Add custom tracing insights before/after in your middlware" -from aws_lambda_powertools.middleware_factory import lambda_handler_decorator -from aws_lambda_powertools import Tracer - -@lambda_handler_decorator(trace_execution=True) -def middleware_name(handler, event, context): - # tracer = Tracer() # Takes a copy of an existing tracer instance - # tracer.add_annotation... - # tracer.add_metadata... - return handler(event, context) -``` +=== "SAM TEMPLATE" + ```python hl_lines="66 83 89 96 103 108-113 119 130" + --8<-- "examples/middleware_factory/sam/combining_powertools_utilities_template.yaml" + ``` ## Tips diff --git a/docs/utilities/parser.md b/docs/utilities/parser.md index 8756725d1e0..97b005a9fb5 100644 --- a/docs/utilities/parser.md +++ b/docs/utilities/parser.md @@ -2,6 +2,7 @@ title: Parser description: Utility --- + This utility provides data parsing and deep validation using [Pydantic](https://pydantic-docs.helpmanual.io/). @@ -166,6 +167,7 @@ Parser comes with the following built-in models: | **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service | | **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway | | **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload | +| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload | ### extending built-in models @@ -305,6 +307,7 @@ Parser comes with the following built-in envelopes, where `Model` in the return | **SnsSqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses SNS records in `body` key using `SnsNotificationModel`.
3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` | | **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`.
2. Parses `body` key using your model and returns it. | `Model` | | **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`.
2. Parses `body` key using your model and returns it. | `Model` | +| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlModel`.
2. Parses `body` key using your model and returns it. | `Model` | ### Bringing your own envelope @@ -524,7 +527,7 @@ Parser is best suited for those looking for a trade-off between defining their m We export most common classes, exceptions, and utilities from Pydantic as part of parser e.g. `from aws_lambda_powertools.utilities.parser import BaseModel`. -If what's your trying to use isn't available as part of the high level import system, use the following escape hatch mechanism: +If what you're trying to use isn't available as part of the high level import system, use the following escape hatch mechanism: ```python title="Pydantic import escape hatch" from aws_lambda_powertools.utilities.parser.pydantic import diff --git a/docs/utilities/typing.md b/docs/utilities/typing.md index a23d014afa6..e94f6b49201 100644 --- a/docs/utilities/typing.md +++ b/docs/utilities/typing.md @@ -7,17 +7,40 @@ description: Utility This typing utility provides static typing classes that can be used to ease the development by providing the IDE type hints. -![Utilities Typing](../media/utilities_typing.png) +## Key features + +* Add static typing classes +* Ease the development by leveraging your IDE's type hints +* Avoid common typing mistakes in Python + +![Utilities Typing](../media/utilities_typing_1.png) + +## Getting started + +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + +We provide static typing for any context methods or properties implemented by [Lambda context object](https://docs.aws.amazon.com/lambda/latest/dg/python-context.html){target="_blank"}. ## LambdaContext The `LambdaContext` typing is typically used in the handler method for the Lambda function. -```python hl_lines="4" title="Annotating Lambda context type" -from typing import Any, Dict -from aws_lambda_powertools.utilities.typing import LambdaContext +=== "getting_started_validator_decorator_function.py" + + ```python hl_lines="1 4" + --8<-- "examples/typing/src/getting_started_typing_function.py" + ``` + +## Working with context methods and properties + +Using `LambdaContext` typing makes it possible to access information and hints of all properties and methods implemented by Lambda context object. + +=== "working_with_context_function.py" + + ```python hl_lines="6 16 25 26" + --8<-- "examples/typing/src/working_with_context_function.py" + ``` -def handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]: - # Insert business logic - return event -``` +![Utilities Typing All](../media/utilities_typing_2.png) +![Utilities Typing Specific](../media/utilities_typing_3.png) diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md index ec795c99bef..c9cd5813086 100644 --- a/docs/utilities/validation.md +++ b/docs/utilities/validation.md @@ -3,6 +3,8 @@ title: Validation description: Utility --- + + This utility provides JSON Schema validation for events and responses, including JMESPath support to unwrap events before validation. ## Key features @@ -13,14 +15,17 @@ This utility provides JSON Schema validation for events and responses, including ## Getting started -???+ tip "Tip: Using JSON Schemas for the first time?" - Check this [step-by-step tour in the official JSON Schema website](https://json-schema.org/learn/getting-started-step-by-step.html){target="_blank"}. +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. You can validate inbound and outbound events using [`validator` decorator](#validator-decorator). You can also use the standalone `validate` function, if you want more control over the validation process such as handling a validation error. -We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/){target="_blank"} library. +???+ tip "Tip: Using JSON Schemas for the first time?" + Check this [step-by-step tour in the official JSON Schema website](https://json-schema.org/learn/getting-started-step-by-step.html){target="_blank"}. + + We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.github.io/python-fastjsonschema/){target="_blank"} library. ???+ warning Both `validator` decorator and `validate` standalone function expects your JSON Schema to be a **dictionary**, not a filename. @@ -31,31 +36,22 @@ We support any JSONSchema draft supported by [fastjsonschema](https://horejsek.g It will fail fast with `SchemaValidationError` exception if event or response doesn't conform with given JSON Schema. -=== "validator_decorator.py" +=== "getting_started_validator_decorator_function.py" - ```python hl_lines="3 5" - from aws_lambda_powertools.utilities.validation import validator + ```python hl_lines="8 27 28 42" + --8<-- "examples/validation/src/getting_started_validator_decorator_function.py" + ``` - import schemas +=== "getting_started_validator_decorator_schema.py" - @validator(inbound_schema=schemas.INPUT, outbound_schema=schemas.OUTPUT) - def handler(event, context): - return event - ``` + ```python hl_lines="10 12 17 19 24 26 28 44 46 51 53" + --8<-- "examples/validation/src/getting_started_validator_decorator_schema.py" + ``` -=== "event.json" +=== "getting_started_validator_decorator_payload.json" ```json - { - "message": "hello world", - "username": "lessa" - } - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + --8<-- "examples/validation/src/getting_started_validator_decorator_payload.json" ``` ???+ note @@ -67,71 +63,48 @@ It will fail fast with `SchemaValidationError` exception if event or response do You can also gracefully handle schema validation errors by catching `SchemaValidationError` exception. -=== "validator_decorator.py" +=== "getting_started_validator_standalone_function.py" - ```python hl_lines="8" - from aws_lambda_powertools.utilities.validation import validate - from aws_lambda_powertools.utilities.validation.exceptions import SchemaValidationError + ```python hl_lines="5 16 17 26" + --8<-- "examples/validation/src/getting_started_validator_standalone_function.py" + ``` - import schemas +=== "getting_started_validator_standalone_schema.py" - def handler(event, context): - try: - validate(event=event, schema=schemas.INPUT) - except SchemaValidationError as e: - # do something before re-raising - raise - - return event - ``` + ```python hl_lines="7 8 10 12 17 19 24 26 28" + --8<-- "examples/validation/src/getting_started_validator_standalone_schema.py" + ``` -=== "event.json" +=== "getting_started_validator_standalone_payload.json" ```json - { - "data": "hello world", - "username": "lessa" - } - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + --8<-- "examples/validation/src/getting_started_validator_standalone_payload.json" ``` ### Unwrapping events prior to validation -You might want to validate only a portion of your event - This is where the `envelope` parameter is for. +You might want to validate only a portion of your event - This is what the `envelope` parameter is for. Envelopes are [JMESPath expressions](https://jmespath.org/tutorial.html) to extract a portion of JSON you want before applying JSON Schema validation. Here is a sample custom EventBridge event, where we only validate what's inside the `detail` key: -=== "unwrapping_events.py" +=== "getting_started_validator_unwrapping_function.py" - We use the `envelope` parameter to extract the payload inside the `detail` key before validating. + ```python hl_lines="2 6 12" + --8<-- "examples/validation/src/getting_started_validator_unwrapping_function.py" + ``` - ```python hl_lines="5" - from aws_lambda_powertools.utilities.validation import validator +=== "getting_started_validator_unwrapping_schema.py" - import schemas + ```python hl_lines="9-14 23 25 28 33 36 41 44 48 51" + --8<-- "examples/validation/src/getting_started_validator_unwrapping_schema.py" + ``` - @validator(inbound_schema=schemas.INPUT, envelope="detail") - def handler(event, context): - return event - ``` - -=== "sample_wrapped_event.json" - - ```python hl_lines="11-14" - --8<-- "docs/shared/validation_basic_eventbridge_event.json" - ``` +=== "getting_started_validator_unwrapping_payload.json" -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json + --8<-- "examples/validation/src/getting_started_validator_unwrapping_payload.json" ``` This is quite powerful because you can use JMESPath Query language to extract records from [arrays](https://jmespath.org/tutorial.html#list-and-slice-projections), combine [pipe](https://jmespath.org/tutorial.html#pipe-expressions) and [function expressions](https://jmespath.org/tutorial.html#functions). @@ -140,30 +113,24 @@ When combined, these features allow you to extract what you need before validati ### Built-in envelopes -This utility comes with built-in envelopes to easily extract the payload from popular event sources. +We provide built-in envelopes to easily extract the payload from popular event sources. -=== "unwrapping_popular_event_sources.py" +=== "unwrapping_popular_event_source_function.py" - ```python hl_lines="5 7" - from aws_lambda_powertools.utilities.validation import envelopes, validator + ```python hl_lines="2 7 12" + --8<-- "examples/validation/src/unwrapping_popular_event_source_function.py" + ``` - import schemas +=== "unwrapping_popular_event_source_schema.py" - @validator(inbound_schema=schemas.INPUT, envelope=envelopes.EVENTBRIDGE) - def handler(event, context): - return event - ``` + ```python hl_lines="7 9 12 17 20" + --8<-- "examples/validation/src/unwrapping_popular_event_source_schema.py" + ``` -=== "sample_wrapped_event.json" +=== "unwrapping_popular_event_source_payload.json" - ```python hl_lines="11-14" - --8<-- "docs/shared/validation_basic_eventbridge_event.json" - ``` - -=== "schemas.py" - - ```python hl_lines="7 14 16 23 39 45 47 52" - --8<-- "docs/shared/validation_basic_jsonschema.py" + ```json hl_lines="12 13" + --8<-- "examples/validation/src/unwrapping_popular_event_source_payload.json" ``` Here is a handy table with built-in envelopes along with their JMESPath expressions in case you want to build your own. @@ -186,243 +153,35 @@ Here is a handy table with built-in envelopes along with their JMESPath expressi ???+ note JSON Schema DRAFT 7 [has many new built-in formats](https://json-schema.org/understanding-json-schema/reference/string.html#format){target="_blank"} such as date, time, and specifically a regex format which might be a better replacement for a custom format, if you do have control over the schema. -JSON Schemas with custom formats like `int64` will fail validation. If you have these, you can pass them using `formats` parameter: +JSON Schemas with custom formats like `awsaccountid` will fail validation. If you have these, you can pass them using `formats` parameter: ```json title="custom_json_schema_type_format.json" { - "lastModifiedTime": { - "format": "int64", - "type": "integer" + "accountid": { + "format": "awsaccountid", + "type": "string" } } ``` For each format defined in a dictionary key, you must use a regex, or a function that returns a boolean to instruct the validator on how to proceed when encountering that type. -=== "validate_custom_format.py" +=== "custom_format_function.py" - ```python hl_lines="5-8 10" - from aws_lambda_powertools.utilities.validation import validate + ```python hl_lines="5 8 10 11 17 27" + --8<-- "examples/validation/src/custom_format_function.py" + ``` - import schema +=== "custom_format_schema.py" - custom_format = { - "int64": True, # simply ignore it, - "positive": lambda x: False if x < 0 else True - } + ```python hl_lines="7 9 12 13 17 20" + --8<-- "examples/validation/src/custom_format_schema.py" + ``` - validate(event=event, schema=schemas.INPUT, formats=custom_format) - ``` +=== "custom_format_payload.json" -=== "schemas.py" - - ```python hl_lines="68" 91 93" - INPUT = { - "$schema": "http://json-schema.org/draft-04/schema#", - "definitions": { - "AWSAPICallViaCloudTrail": { - "properties": { - "additionalEventData": {"$ref": "#/definitions/AdditionalEventData"}, - "awsRegion": {"type": "string"}, - "errorCode": {"type": "string"}, - "errorMessage": {"type": "string"}, - "eventID": {"type": "string"}, - "eventName": {"type": "string"}, - "eventSource": {"type": "string"}, - "eventTime": {"format": "date-time", "type": "string"}, - "eventType": {"type": "string"}, - "eventVersion": {"type": "string"}, - "recipientAccountId": {"type": "string"}, - "requestID": {"type": "string"}, - "requestParameters": {"$ref": "#/definitions/RequestParameters"}, - "resources": {"items": {"type": "object"}, "type": "array"}, - "responseElements": {"type": ["object", "null"]}, - "sourceIPAddress": {"type": "string"}, - "userAgent": {"type": "string"}, - "userIdentity": {"$ref": "#/definitions/UserIdentity"}, - "vpcEndpointId": {"type": "string"}, - "x-amazon-open-api-schema-readOnly": {"type": "boolean"}, - }, - "required": [ - "eventID", - "awsRegion", - "eventVersion", - "responseElements", - "sourceIPAddress", - "eventSource", - "requestParameters", - "resources", - "userAgent", - "readOnly", - "userIdentity", - "eventType", - "additionalEventData", - "vpcEndpointId", - "requestID", - "eventTime", - "eventName", - "recipientAccountId", - ], - "type": "object", - }, - "AdditionalEventData": { - "properties": { - "objectRetentionInfo": {"$ref": "#/definitions/ObjectRetentionInfo"}, - "x-amz-id-2": {"type": "string"}, - }, - "required": ["x-amz-id-2"], - "type": "object", - }, - "Attributes": { - "properties": { - "creationDate": {"format": "date-time", "type": "string"}, - "mfaAuthenticated": {"type": "string"}, - }, - "required": ["mfaAuthenticated", "creationDate"], - "type": "object", - }, - "LegalHoldInfo": { - "properties": { - "isUnderLegalHold": {"type": "boolean"}, - "lastModifiedTime": {"format": "int64", "type": "integer"}, - }, - "type": "object", - }, - "ObjectRetentionInfo": { - "properties": { - "legalHoldInfo": {"$ref": "#/definitions/LegalHoldInfo"}, - "retentionInfo": {"$ref": "#/definitions/RetentionInfo"}, - }, - "type": "object", - }, - "RequestParameters": { - "properties": { - "bucketName": {"type": "string"}, - "key": {"type": "string"}, - "legal-hold": {"type": "string"}, - "retention": {"type": "string"}, - }, - "required": ["bucketName", "key"], - "type": "object", - }, - "RetentionInfo": { - "properties": { - "lastModifiedTime": {"format": "int64", "type": "integer"}, - "retainUntilMode": {"type": "string"}, - "retainUntilTime": {"format": "int64", "type": "integer"}, - }, - "type": "object", - }, - "SessionContext": { - "properties": {"attributes": {"$ref": "#/definitions/Attributes"}}, - "required": ["attributes"], - "type": "object", - }, - "UserIdentity": { - "properties": { - "accessKeyId": {"type": "string"}, - "accountId": {"type": "string"}, - "arn": {"type": "string"}, - "principalId": {"type": "string"}, - "sessionContext": {"$ref": "#/definitions/SessionContext"}, - "type": {"type": "string"}, - }, - "required": ["accessKeyId", "sessionContext", "accountId", "principalId", "type", "arn"], - "type": "object", - }, - }, - "properties": { - "account": {"type": "string"}, - "detail": {"$ref": "#/definitions/AWSAPICallViaCloudTrail"}, - "detail-type": {"type": "string"}, - "id": {"type": "string"}, - "region": {"type": "string"}, - "resources": {"items": {"type": "string"}, "type": "array"}, - "source": {"type": "string"}, - "time": {"format": "date-time", "type": "string"}, - "version": {"type": "string"}, - }, - "required": ["detail-type", "resources", "id", "source", "time", "detail", "region", "version", "account"], - "title": "AWSAPICallViaCloudTrail", - "type": "object", - "x-amazon-events-detail-type": "AWS API Call via CloudTrail", - "x-amazon-events-source": "aws.s3", - } - ``` - -=== "event.json" - - ```json - { - "account": "123456789012", - "detail": { - "additionalEventData": { - "AuthenticationMethod": "AuthHeader", - "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", - "SignatureVersion": "SigV4", - "bytesTransferredIn": 0, - "bytesTransferredOut": 0, - "x-amz-id-2": "ejUr9Nd/4IO1juF/a6GOcu+PKrVX6dOH6jDjQOeCJvtARUqzxrhHGrhEt04cqYtAZVqcSEXYqo0=", - }, - "awsRegion": "us-west-1", - "eventCategory": "Data", - "eventID": "be4fdb30-9508-4984-b071-7692221899ae", - "eventName": "HeadObject", - "eventSource": "s3.amazonaws.com", - "eventTime": "2020-12-22T10:05:29Z", - "eventType": "AwsApiCall", - "eventVersion": "1.07", - "managementEvent": False, - "readOnly": True, - "recipientAccountId": "123456789012", - "requestID": "A123B1C123D1E123", - "requestParameters": { - "Host": "lambda-artifacts-deafc19498e3f2df.s3.us-west-1.amazonaws.com", - "bucketName": "lambda-artifacts-deafc19498e3f2df", - "key": "path1/path2/path3/file.zip", - }, - "resources": [ - { - "ARN": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df/path1/path2/path3/file.zip", - "type": "AWS::S3::Object", - }, - { - "ARN": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df", - "accountId": "123456789012", - "type": "AWS::S3::Bucket", - }, - ], - "responseElements": None, - "sourceIPAddress": "AWS Internal", - "userAgent": "AWS Internal", - "userIdentity": { - "accessKeyId": "ABCDEFGHIJKLMNOPQR12", - "accountId": "123456789012", - "arn": "arn:aws:sts::123456789012:assumed-role/role-name1/1234567890123", - "invokedBy": "AWS Internal", - "principalId": "ABCDEFGHIJKLMN1OPQRST:1234567890123", - "sessionContext": { - "attributes": {"creationDate": "2020-12-09T09:58:24Z", "mfaAuthenticated": "false"}, - "sessionIssuer": { - "accountId": "123456789012", - "arn": "arn:aws:iam::123456789012:role/role-name1", - "principalId": "ABCDEFGHIJKLMN1OPQRST", - "type": "Role", - "userName": "role-name1", - }, - }, - "type": "AssumedRole", - }, - "vpcEndpointId": "vpce-a123cdef", - }, - "detail-type": "AWS API Call via CloudTrail", - "id": "e0bad426-0a70-4424-b53a-eb902ebf5786", - "region": "us-west-1", - "resources": [], - "source": "aws.s3", - "time": "2020-12-22T10:05:29Z", - "version": "0", - } + ```json hl_lines="12 13" + --8<-- "examples/validation/src/custom_format_payload.json" ``` ### Built-in JMESPath functions diff --git a/examples/jmespath_functions/src/extract_data_from_builtin_envelope.json b/examples/jmespath_functions/src/extract_data_from_builtin_envelope.json new file mode 100644 index 00000000000..6fe1d1655ab --- /dev/null +++ b/examples/jmespath_functions/src/extract_data_from_builtin_envelope.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\",\"booking\":{\"id\":\"5b2c4803-330b-42b7-811a-c68689425de1\",\"reference\":\"ySz7oA\",\"outboundFlightId\":\"20c0d2f2-56a3-4068-bf20-ff7703db552d\"},\"payment\":{\"receipt\":\"https:\/\/pay.stripe.com\/receipts\/acct_1Dvn7pF4aIiftV70\/ch_3JTC14F4aIiftV700iFq2CHB\/rcpt_K7QsrFln9FgFnzUuBIiNdkkRYGxUL0X\",\"amount\":100}}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "7b270e59b47ff90a553787216d55d91d", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] +} diff --git a/examples/jmespath_functions/src/extract_data_from_builtin_envelope.py b/examples/jmespath_functions/src/extract_data_from_builtin_envelope.py new file mode 100644 index 00000000000..53c230e1b9b --- /dev/null +++ b/examples/jmespath_functions/src/extract_data_from_builtin_envelope.py @@ -0,0 +1,9 @@ +from aws_lambda_powertools.utilities.jmespath_utils import envelopes, extract_data_from_envelope +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def handler(event: dict, context: LambdaContext) -> dict: + payload = extract_data_from_envelope(data=event, envelope=envelopes.SQS) + customer_id = payload.get("customerId") # now deserialized + + return {"customer_id": customer_id, "message": "success", "statusCode": 200} diff --git a/examples/jmespath_functions/src/extract_data_from_envelope.json b/examples/jmespath_functions/src/extract_data_from_envelope.json new file mode 100644 index 00000000000..0a0f0763279 --- /dev/null +++ b/examples/jmespath_functions/src/extract_data_from_envelope.json @@ -0,0 +1,12 @@ +{ + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}", + "deeply_nested": [ + { + "some_data": [ + 1, + 2, + 3 + ] + } + ] +} \ No newline at end of file diff --git a/examples/jmespath_functions/src/extract_data_from_envelope.py b/examples/jmespath_functions/src/extract_data_from_envelope.py new file mode 100644 index 00000000000..5c35bc4348b --- /dev/null +++ b/examples/jmespath_functions/src/extract_data_from_envelope.py @@ -0,0 +1,12 @@ +from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def handler(event: dict, context: LambdaContext) -> dict: + payload = extract_data_from_envelope(data=event, envelope="powertools_json(body)") + customer_id = payload.get("customerId") # now deserialized + + # also works for fetching and flattening deeply nested data + some_data = extract_data_from_envelope(data=event, envelope="deeply_nested[*].some_data[]") + + return {"customer_id": customer_id, "message": "success", "context": some_data, "statusCode": 200} diff --git a/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py new file mode 100644 index 00000000000..cff3424b487 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py @@ -0,0 +1,41 @@ +import base64 +import binascii +import gzip +import json + +import powertools_base64_gzip_jmespath_schema as schemas +from jmespath.exceptions import JMESPathTypeError + +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + validate(event=event, schema=schemas.INPUT, envelope="powertools_base64_gzip(payload) | powertools_json(@)") + + # Alternatively, extract_data_from_envelope works here too + encoded_payload = base64.b64decode(event["payload"]) + uncompressed_payload = gzip.decompress(encoded_payload).decode() + log: dict = json.loads(uncompressed_payload) + + return { + "message": "Logs processed", + "log_group": log.get("logGroup"), + "owner": log.get("owner"), + "success": True, + } + + except JMESPathTypeError: + return return_error_message("The powertools_base64_gzip() envelope function must match a valid path.") + except binascii.Error: + return return_error_message("Payload must be a valid base64 encoded string") + except json.JSONDecodeError: + return return_error_message("Payload must be valid JSON (base64 encoded).") + except SchemaValidationError as exception: + # SchemaValidationError indicates where a data mismatch is + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"message": message, "success": False} diff --git a/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json new file mode 100644 index 00000000000..13995523099 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json @@ -0,0 +1,3 @@ +{ + "payload": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==" +} diff --git a/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py new file mode 100644 index 00000000000..0ba02934928 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py @@ -0,0 +1,47 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [ + { + "owner": "123456789012", + "logGroup": "/aws/lambda/powertools-example", + "logStream": "2022/08/07/[$LATEST]d3a8dcaffc7f4de2b8db132e3e106660", + "logEvents": {}, + } + ], + "required": ["owner", "logGroup", "logStream", "logEvents"], + "properties": { + "owner": { + "$id": "#/properties/owner", + "type": "string", + "title": "The owner", + "examples": ["123456789012"], + "maxLength": 12, + }, + "logGroup": { + "$id": "#/properties/logGroup", + "type": "string", + "title": "The logGroup", + "examples": ["/aws/lambda/powertools-example"], + "maxLength": 100, + }, + "logStream": { + "$id": "#/properties/logStream", + "type": "string", + "title": "The logGroup", + "examples": ["2022/08/07/[$LATEST]d3a8dcaffc7f4de2b8db132e3e106660"], + "maxLength": 100, + }, + "logEvents": { + "$id": "#/properties/logEvents", + "type": "array", + "title": "The logEvents", + "examples": [ + "{'id': 'eventId1', 'message': {'username': 'lessa', 'message': 'hello world'}, 'timestamp': 1440442987000}" # noqa E501 + ], + }, + }, +} diff --git a/examples/jmespath_functions/src/powertools_base64_jmespath_function.py b/examples/jmespath_functions/src/powertools_base64_jmespath_function.py new file mode 100644 index 00000000000..ba7208298a0 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_jmespath_function.py @@ -0,0 +1,63 @@ +import base64 +import binascii +import json +from dataclasses import asdict, dataclass, field, is_dataclass +from uuid import uuid4 + +import powertools_base64_jmespath_schema as schemas +from jmespath.exceptions import JMESPathTypeError + +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + + +@dataclass +class Order: + user_id: int + product_id: int + quantity: int + price: float + currency: str + order_id: str = field(default_factory=lambda: f"{uuid4()}") + + +class DataclassCustomEncoder(json.JSONEncoder): + """A custom JSON encoder to serialize dataclass obj""" + + def default(self, obj): + # Only called for values that aren't JSON serializable + # where `obj` will be an instance of Todo in this example + return asdict(obj) if is_dataclass(obj) else super().default(obj) + + +def lambda_handler(event, context: LambdaContext) -> dict: + + # Try to validate the schema + try: + validate(event=event, schema=schemas.INPUT, envelope="powertools_json(powertools_base64(payload))") + + # alternatively, extract_data_from_envelope works here too + payload_decoded = base64.b64decode(event["payload"]).decode() + + order_payload: dict = json.loads(payload_decoded) + + return { + "order": json.dumps(Order(**order_payload), cls=DataclassCustomEncoder), + "message": "order created", + "success": True, + } + except JMESPathTypeError: + return return_error_message( + "The powertools_json(powertools_base64()) envelope function must match a valid path." + ) + except binascii.Error: + return return_error_message("Payload must be a valid base64 encoded string") + except json.JSONDecodeError: + return return_error_message("Payload must be valid JSON (base64 encoded).") + except SchemaValidationError as exception: + # SchemaValidationError indicates where a data mismatch is + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"order": None, "message": message, "success": False} diff --git a/examples/jmespath_functions/src/powertools_base64_jmespath_payload.json b/examples/jmespath_functions/src/powertools_base64_jmespath_payload.json new file mode 100644 index 00000000000..b4ea41d1d09 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_jmespath_payload.json @@ -0,0 +1,3 @@ +{ + "payload":"eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0=" +} diff --git a/examples/jmespath_functions/src/powertools_base64_jmespath_schema.py b/examples/jmespath_functions/src/powertools_base64_jmespath_schema.py new file mode 100644 index 00000000000..bd643a11c13 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_base64_jmespath_schema.py @@ -0,0 +1,46 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample order schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": 123, "product_id": 1, "quantity": 2, "price": 10.40, "currency": "USD"}], + "required": ["user_id", "product_id", "quantity", "price", "currency"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "integer", + "title": "The unique identifier of the user", + "examples": [123], + "maxLength": 10, + }, + "product_id": { + "$id": "#/properties/product_id", + "type": "integer", + "title": "The unique identifier of the product", + "examples": [1], + "maxLength": 10, + }, + "quantity": { + "$id": "#/properties/quantity", + "type": "integer", + "title": "The quantity of the product", + "examples": [2], + "maxLength": 10, + }, + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "The individual price of the product", + "examples": [10.40], + "maxLength": 10, + }, + "currency": { + "$id": "#/properties/currency", + "type": "string", + "title": "The currency", + "examples": ["The currency of the order"], + "maxLength": 100, + }, + }, +} diff --git a/examples/jmespath_functions/src/powertools_custom_jmespath_function.json b/examples/jmespath_functions/src/powertools_custom_jmespath_function.json new file mode 100644 index 00000000000..fa0c44a3060 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_custom_jmespath_function.json @@ -0,0 +1,14 @@ +{ + "Records": [ + { + "user": "integration-kafka", + "datetime": "2022-01-01T00:00:00.000Z", + "log": "/QGIMjAyMi8wNi8xNiAxNjoyNTowMCBbY3JpdF0gMzA1MTg5MCMNCPBEOiAqMSBjb25uZWN0KCkg\ndG8gMTI3LjAuMC4xOjUwMDAgZmFpbGVkICgxMzogUGVybWlzc2lvbiBkZW5pZWQpIHdoaWxlEUEI\naW5nAUJAdXBzdHJlYW0sIGNsaWVudDoZVKgsIHNlcnZlcjogXywgcmVxdWVzdDogIk9QVElPTlMg\nLyBIVFRQLzEuMSIsFUckOiAiaHR0cDovLzabABQvIiwgaG8FQDAxMjcuMC4wLjE6ODEi\n" + }, + { + "user": "integration-kafka", + "datetime": "2022-01-01T00:00:01.000Z", + "log": "tQHwnDEyNy4wLjAuMSAtIC0gWzE2L0p1bi8yMDIyOjE2OjMwOjE5ICswMTAwXSAiT1BUSU9OUyAv\nIEhUVFAvMS4xIiAyMDQgMCAiLSIgIk1vemlsbGEvNS4wIChYMTE7IExpbnV4IHg4Nl82NCkgQXBw\nbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEwMi4BmUwwIFNhZmFy\naS81MzcuMzYiICItIg==\n" + } + ] +} diff --git a/examples/jmespath_functions/src/powertools_custom_jmespath_function.py b/examples/jmespath_functions/src/powertools_custom_jmespath_function.py new file mode 100644 index 00000000000..71fdecd0db2 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_custom_jmespath_function.py @@ -0,0 +1,45 @@ +import base64 +import binascii + +import snappy +from jmespath.exceptions import JMESPathTypeError +from jmespath.functions import signature + +from aws_lambda_powertools.utilities.jmespath_utils import PowertoolsFunctions, extract_data_from_envelope + + +class CustomFunctions(PowertoolsFunctions): + # only decode if value is a string + # see supported data types: https://jmespath.org/specification.html#built-in-functions + @signature({"types": ["string"]}) + def _func_decode_snappy_compression(self, payload: str): + decoded: bytes = base64.b64decode(payload) + return snappy.uncompress(decoded) + + +custom_jmespath_options = {"custom_functions": CustomFunctions()} + + +def lambda_handler(event, context) -> dict: + + try: + logs = [] + logs.append( + extract_data_from_envelope( + data=event, + # NOTE: Use the prefix `_func_` before the name of the function + envelope="Records[*].decode_snappy_compression(log)", + jmespath_options=custom_jmespath_options, + ) + ) + return {"logs": logs, "message": "Extracted messages", "success": True} + except JMESPathTypeError: + return return_error_message("The envelope function must match a valid path.") + except snappy.UncompressError: + return return_error_message("Log must be a valid snappy compressed binary") + except binascii.Error: + return return_error_message("Log must be a valid base64 encoded string") + + +def return_error_message(message: str) -> dict: + return {"logs": None, "message": message, "success": False} diff --git a/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json b/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json new file mode 100644 index 00000000000..31d61c31839 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json @@ -0,0 +1,30 @@ +{ + "version":"2.0", + "routeKey":"ANY /createpayment", + "rawPath":"/createpayment", + "rawQueryString":"", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "requestContext":{ + "accountId":"123456789012", + "apiId":"api-id", + "domainName":"id.execute-api.us-east-1.amazonaws.com", + "domainPrefix":"id", + "http":{ + "method":"POST", + "path":"/createpayment", + "protocol":"HTTP/1.1", + "sourceIp":"ip", + "userAgent":"agent" + }, + "requestId":"id", + "routeKey":"ANY /createpayment", + "stage":"$default", + "time":"10/Feb/2021:13:40:43 +0000", + "timeEpoch":1612964443723 + }, + "body":"{\"user\":\"xyz\",\"product_id\":\"123456789\"}", + "isBase64Encoded":false + } diff --git a/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py b/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py new file mode 100644 index 00000000000..aaf5724b54b --- /dev/null +++ b/examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py @@ -0,0 +1,34 @@ +import json +from uuid import uuid4 + +import requests + +from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig, idempotent + +persistence_layer = DynamoDBPersistenceLayer(table_name="IdempotencyTable") + +# Treat everything under the "body" key +# in the event json object as our payload +config = IdempotencyConfig(event_key_jmespath="powertools_json(body)") + + +class PaymentError(Exception): + ... + + +@idempotent(config=config, persistence_store=persistence_layer) +def handler(event, context) -> dict: + body = json.loads(event["body"]) + try: + payment: dict = create_subscription_payment(user=body["user"], product_id=body["product_id"]) + return {"payment_id": payment.get("id"), "message": "success", "statusCode": 200} + except requests.HTTPError as e: + raise PaymentError("Unable to create payment subscription") from e + + +def create_subscription_payment(user: str, product_id: str) -> dict: + payload = {"user": user, "product_id": product_id} + ret: requests.Response = requests.post(url="https://httpbin.org/anything", data=payload) + ret.raise_for_status() + + return {"id": f"{uuid4()}", "message": "paid"} diff --git a/examples/jmespath_functions/src/powertools_json_jmespath_function.py b/examples/jmespath_functions/src/powertools_json_jmespath_function.py new file mode 100644 index 00000000000..5eae585c0c1 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_json_jmespath_function.py @@ -0,0 +1,56 @@ +import json +from dataclasses import asdict, dataclass, field, is_dataclass +from uuid import uuid4 + +import powertools_json_jmespath_schema as schemas +from jmespath.exceptions import JMESPathTypeError + +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + + +@dataclass +class Order: + user_id: int + product_id: int + quantity: int + price: float + currency: str + order_id: str = field(default_factory=lambda: f"{uuid4()}") + + +class DataclassCustomEncoder(json.JSONEncoder): + """A custom JSON encoder to serialize dataclass obj""" + + def default(self, obj): + # Only called for values that aren't JSON serializable + # where `obj` will be an instance of Order in this example + return asdict(obj) if is_dataclass(obj) else super().default(obj) + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + # Validate order against our schema + validate(event=event, schema=schemas.INPUT, envelope="powertools_json(payload)") + + # Deserialize JSON string order as dict + # alternatively, extract_data_from_envelope works here too + order_payload: dict = json.loads(event.get("payload")) + + return { + "order": json.dumps(Order(**order_payload), cls=DataclassCustomEncoder), + "message": "order created", + "success": True, + } + except JMESPathTypeError: + # The powertools_json() envelope function must match a valid path + return return_error_message("Invalid request.") + except SchemaValidationError as exception: + # SchemaValidationError indicates where a data mismatch is + return return_error_message(str(exception)) + except json.JSONDecodeError: + return return_error_message("Payload must be valid JSON (base64 encoded).") + + +def return_error_message(message: str) -> dict: + return {"order": None, "message": message, "success": False} diff --git a/examples/jmespath_functions/src/powertools_json_jmespath_payload.json b/examples/jmespath_functions/src/powertools_json_jmespath_payload.json new file mode 100644 index 00000000000..647583bba82 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_json_jmespath_payload.json @@ -0,0 +1,3 @@ +{ + "payload":"{\"user_id\": 123, \"product_id\": 1, \"quantity\": 2, \"price\": 10.40, \"currency\": \"USD\"}" +} diff --git a/examples/jmespath_functions/src/powertools_json_jmespath_schema.py b/examples/jmespath_functions/src/powertools_json_jmespath_schema.py new file mode 100644 index 00000000000..bd643a11c13 --- /dev/null +++ b/examples/jmespath_functions/src/powertools_json_jmespath_schema.py @@ -0,0 +1,46 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample order schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": 123, "product_id": 1, "quantity": 2, "price": 10.40, "currency": "USD"}], + "required": ["user_id", "product_id", "quantity", "price", "currency"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "integer", + "title": "The unique identifier of the user", + "examples": [123], + "maxLength": 10, + }, + "product_id": { + "$id": "#/properties/product_id", + "type": "integer", + "title": "The unique identifier of the product", + "examples": [1], + "maxLength": 10, + }, + "quantity": { + "$id": "#/properties/quantity", + "type": "integer", + "title": "The quantity of the product", + "examples": [2], + "maxLength": 10, + }, + "price": { + "$id": "#/properties/price", + "type": "number", + "title": "The individual price of the product", + "examples": [10.40], + "maxLength": 10, + }, + "currency": { + "$id": "#/properties/currency", + "type": "string", + "title": "The currency", + "examples": ["The currency of the order"], + "maxLength": 100, + }, + }, +} diff --git a/examples/middleware_factory/sam/combining_powertools_utilities_template.yaml b/examples/middleware_factory/sam/combining_powertools_utilities_template.yaml new file mode 100644 index 00000000000..4ee87e379cd --- /dev/null +++ b/examples/middleware_factory/sam/combining_powertools_utilities_template.yaml @@ -0,0 +1,136 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Middleware-powertools-utilities example + +Globals: + Function: + Timeout: 5 + Runtime: python3.9 + Tracing: Active + Architectures: + - x86_64 + Environment: + Variables: + LOG_LEVEL: DEBUG + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: middleware + +Resources: + MiddlewareFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: middleware/ + Handler: app.lambda_handler + Description: Middleware function + Policies: + - AWSLambdaBasicExecutionRole # Managed Policy + - Version: '2012-10-17' # Policy Document + Statement: + - Effect: Allow + Action: + - dynamodb:PutItem + Resource: !GetAtt HistoryTable.Arn + - Effect: Allow + Action: # https://docs.aws.amazon.com/appconfig/latest/userguide/getting-started-with-appconfig-permissions.html + - ssm:GetDocument + - ssm:ListDocuments + - appconfig:GetLatestConfiguration + - appconfig:StartConfigurationSession + - appconfig:ListApplications + - appconfig:GetApplication + - appconfig:ListEnvironments + - appconfig:GetEnvironment + - appconfig:ListConfigurationProfiles + - appconfig:GetConfigurationProfile + - appconfig:ListDeploymentStrategies + - appconfig:GetDeploymentStrategy + - appconfig:GetConfiguration + - appconfig:ListDeployments + - appconfig:GetDeployment + Resource: "*" + Events: + GetComments: + Type: Api + Properties: + Path: /comments + Method: GET + GetCommentsById: + Type: Api + Properties: + Path: /comments/{comment_id} + Method: GET + + # DynamoDB table to store historical data + HistoryTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: "HistoryTable" + AttributeDefinitions: + - AttributeName: customer_id + AttributeType: S + - AttributeName: request_id + AttributeType: S + KeySchema: + - AttributeName: customer_id + KeyType: HASH + - AttributeName: request_id + KeyType: "RANGE" + BillingMode: PAY_PER_REQUEST + + # Feature flags using AppConfig + FeatureCommentApp: + Type: AWS::AppConfig::Application + Properties: + Description: "Comments Application for feature toggles" + Name: comments + + FeatureCommentDevEnv: + Type: AWS::AppConfig::Environment + Properties: + ApplicationId: !Ref FeatureCommentApp + Description: "Development Environment for the App Config Comments" + Name: dev + + FeatureCommentConfigProfile: + Type: AWS::AppConfig::ConfigurationProfile + Properties: + ApplicationId: !Ref FeatureCommentApp + Name: features + LocationUri: "hosted" + + HostedConfigVersion: + Type: AWS::AppConfig::HostedConfigurationVersion + Properties: + ApplicationId: !Ref FeatureCommentApp + ConfigurationProfileId: !Ref FeatureCommentConfigProfile + Description: 'A sample hosted configuration version' + Content: | + { + "save_history": { + "default": true + } + } + ContentType: 'application/json' + + # this is just an example + # change this values according your deployment strategy + BasicDeploymentStrategy: + Type: AWS::AppConfig::DeploymentStrategy + Properties: + Name: "Deployment" + Description: "Deployment strategy for comments app." + DeploymentDurationInMinutes: 1 + FinalBakeTimeInMinutes: 1 + GrowthFactor: 100 + GrowthType: LINEAR + ReplicateTo: NONE + + ConfigDeployment: + Type: AWS::AppConfig::Deployment + Properties: + ApplicationId: !Ref FeatureCommentApp + ConfigurationProfileId: !Ref FeatureCommentConfigProfile + ConfigurationVersion: !Ref HostedConfigVersion + DeploymentStrategyId: !Ref BasicDeploymentStrategy + EnvironmentId: !Ref FeatureCommentDevEnv diff --git a/examples/middleware_factory/src/advanced_middleware_tracer_function.py b/examples/middleware_factory/src/advanced_middleware_tracer_function.py new file mode 100644 index 00000000000..05aa65d33c4 --- /dev/null +++ b/examples/middleware_factory/src/advanced_middleware_tracer_function.py @@ -0,0 +1,44 @@ +import time +from typing import Callable + +import requests +from requests import Response + +from aws_lambda_powertools import Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +app = APIGatewayRestResolver() + + +@lambda_handler_decorator(trace_execution=True) +def middleware_with_advanced_tracing(handler, event, context) -> Callable: + + tracer.put_metadata(key="resource", value=event.get("resource")) + + start_time = time.time() + response = handler(event, context) + execution_time = time.time() - start_time + + tracer.put_annotation(key="TotalExecutionTime", value=str(execution_time)) + + # adding custom headers in response object after lambda executing + response["headers"]["execution_time"] = execution_time + response["headers"]["aws_request_id"] = context.aws_request_id + + return response + + +@app.get("/products") +def create_product() -> dict: + product: Response = requests.get("https://dummyjson.com/products/1") + product.raise_for_status() + + return {"product": product.json()} + + +@middleware_with_advanced_tracing +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/middleware_factory/src/advanced_middleware_tracer_payload.json b/examples/middleware_factory/src/advanced_middleware_tracer_payload.json new file mode 100644 index 00000000000..d8a89bcfc67 --- /dev/null +++ b/examples/middleware_factory/src/advanced_middleware_tracer_payload.json @@ -0,0 +1,5 @@ +{ + "resource": "/products", + "path": "/products", + "httpMethod": "GET" + } diff --git a/examples/middleware_factory/src/combining_powertools_utilities_event.json b/examples/middleware_factory/src/combining_powertools_utilities_event.json new file mode 100644 index 00000000000..74257f56411 --- /dev/null +++ b/examples/middleware_factory/src/combining_powertools_utilities_event.json @@ -0,0 +1,79 @@ +{ + "body":"None", + "headers":{ + "Accept":"*/*", + "Accept-Encoding":"gzip, deflate, br", + "Connection":"keep-alive", + "Host":"127.0.0.1:3001", + "Postman-Token":"a9d49365-ebe1-4bb0-8627-d5e37cdce86d", + "User-Agent":"PostmanRuntime/7.29.0", + "X-Customer-Id":"1", + "X-Forwarded-Port":"3001", + "X-Forwarded-Proto":"http" + }, + "httpMethod":"GET", + "isBase64Encoded":false, + "multiValueHeaders":{ + "Accept":[ + "*/*" + ], + "Accept-Encoding":[ + "gzip, deflate, br" + ], + "Connection":[ + "keep-alive" + ], + "Host":[ + "127.0.0.1:3001" + ], + "Postman-Token":[ + "a9d49365-ebe1-4bb0-8627-d5e37cdce86d" + ], + "User-Agent":[ + "PostmanRuntime/7.29.0" + ], + "X-Customer-Id":[ + "1" + ], + "X-Forwarded-Port":[ + "3001" + ], + "X-Forwarded-Proto":[ + "http" + ] + }, + "multiValueQueryStringParameters":"None", + "path":"/comments", + "pathParameters":"None", + "queryStringParameters":"None", + "requestContext":{ + "accountId":"123456789012", + "apiId":"1234567890", + "domainName":"127.0.0.1:3001", + "extendedRequestId":"None", + "httpMethod":"GET", + "identity":{ + "accountId":"None", + "apiKey":"None", + "caller":"None", + "cognitoAuthenticationProvider":"None", + "cognitoAuthenticationType":"None", + "cognitoIdentityPoolId":"None", + "sourceIp":"127.0.0.1", + "user":"None", + "userAgent":"Custom User Agent String", + "userArn":"None" + }, + "path":"/comments", + "protocol":"HTTP/1.1", + "requestId":"56d1a102-6d9d-4f13-b4f7-26751c10a131", + "requestTime":"20/Aug/2022:18:18:58 +0000", + "requestTimeEpoch":1661019538, + "resourceId":"123456", + "resourcePath":"/comments", + "stage":"Prod" + }, + "resource":"/comments", + "stageVariables":"None", + "version":"1.0" + } diff --git a/examples/middleware_factory/src/combining_powertools_utilities_function.py b/examples/middleware_factory/src/combining_powertools_utilities_function.py new file mode 100644 index 00000000000..3b546ea240e --- /dev/null +++ b/examples/middleware_factory/src/combining_powertools_utilities_function.py @@ -0,0 +1,121 @@ +import json +from typing import Callable + +import boto3 +import combining_powertools_utilities_schema as schemas +import requests + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.event_handler.exceptions import InternalServerError +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.shared.types import JSONType +from aws_lambda_powertools.utilities.feature_flags import AppConfigStore, FeatureFlags +from aws_lambda_powertools.utilities.jmespath_utils import extract_data_from_envelope +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + +app = APIGatewayRestResolver() +tracer = Tracer() +logger = Logger() + +table_historic = boto3.resource("dynamodb").Table("HistoricTable") + +app_config = AppConfigStore(environment="dev", application="comments", name="features") +feature_flags = FeatureFlags(store=app_config) + + +@lambda_handler_decorator(trace_execution=True) +def middleware_custom(handler: Callable, event: dict, context: LambdaContext): + + # validating the INPUT with the given schema + # X-Customer-Id header must be informed in all requests + try: + validate(event=event, schema=schemas.INPUT) + except SchemaValidationError as e: + return { + "statusCode": 400, + "body": json.dumps(str(e)), + } + + # extracting headers and requestContext from event + headers = extract_data_from_envelope(data=event, envelope="headers") + request_context = extract_data_from_envelope(data=event, envelope="requestContext") + + logger.debug(f"X-Customer-Id => {headers.get('X-Customer-Id')}") + tracer.put_annotation(key="CustomerId", value=headers.get("X-Customer-Id")) + + response = handler(event, context) + + # automatically adding security headers to all responses + # see: https://securityheaders.com/ + logger.info("Injecting security headers") + response["headers"]["Referrer-Policy"] = "no-referrer" + response["headers"]["Strict-Transport-Security"] = "max-age=15552000; includeSubDomains; preload" + response["headers"]["X-DNS-Prefetch-Control"] = "off" + response["headers"]["X-Content-Type-Options"] = "nosniff" + response["headers"]["X-Permitted-Cross-Domain-Policies"] = "none" + response["headers"]["X-Download-Options"] = "noopen" + + logger.info("Saving api call in history table") + save_api_execution_history(str(event.get("path")), headers, request_context) + + # return lambda execution + return response + + +@tracer.capture_method +def save_api_execution_history(path: str, headers: dict, request_context: dict) -> None: + + try: + # using the feature flags utility to check if the new feature "save api call to history" is enabled by default + # see: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/feature_flags/#static-flags + save_history: JSONType = feature_flags.evaluate(name="save_history", default=False) + if save_history: + # saving history in dynamodb table + tracer.put_metadata(key="execution detail", value=request_context) + table_historic.put_item( + Item={ + "customer_id": headers.get("X-Customer-Id"), + "request_id": request_context.get("requestId"), + "path": path, + "request_time": request_context.get("requestTime"), + "source_ip": request_context.get("identity", {}).get("sourceIp"), + "http_method": request_context.get("httpMethod"), + } + ) + + return None + except Exception: + # you can add more logic here to handle exceptions or even save this to a DLQ + # but not to make this example too long, we just return None since the Lambda has been successfully executed + return None + + +@app.get("/comments") +@tracer.capture_method +def get_comments(): + try: + comments: requests.Response = requests.get("https://jsonplaceholder.typicode.com/comments") + comments.raise_for_status() + + return {"comments": comments.json()[:10]} + except Exception as exc: + raise InternalServerError(str(exc)) + + +@app.get("/comments/") +@tracer.capture_method +def get_comments_by_id(comment_id: str): + try: + comments: requests.Response = requests.get(f"https://jsonplaceholder.typicode.com/comments/{comment_id}") + comments.raise_for_status() + + return {"comments": comments.json()} + except Exception as exc: + raise InternalServerError(str(exc)) + + +@middleware_custom +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/middleware_factory/src/combining_powertools_utilities_schema.py b/examples/middleware_factory/src/combining_powertools_utilities_schema.py new file mode 100644 index 00000000000..7a1978a71a3 --- /dev/null +++ b/examples/middleware_factory/src/combining_powertools_utilities_schema.py @@ -0,0 +1,25 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1661012141.json", + "title": "Root", + "type": "object", + "required": ["headers"], + "properties": { + "headers": { + "$id": "#root/headers", + "title": "Headers", + "type": "object", + "required": ["X-Customer-Id"], + "properties": { + "X-Customer-Id": { + "$id": "#root/headers/X-Customer-Id", + "title": "X-customer-id", + "type": "string", + "default": "", + "examples": ["1"], + "pattern": "^.*$", + } + }, + } + }, +} diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py new file mode 100644 index 00000000000..e77328ca8f7 --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_function.py @@ -0,0 +1,39 @@ +import time +from typing import Callable + +import requests +from requests import Response + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver() + + +@lambda_handler_decorator +def middleware_after(handler, event, context) -> Callable: + + start_time = time.time() + response = handler(event, context) + execution_time = time.time() - start_time + + # adding custom headers in response object after lambda executing + response["headers"]["execution_time"] = execution_time + response["headers"]["aws_request_id"] = context.aws_request_id + + return response + + +@app.post("/todos") +def create_todo() -> dict: + todo_data: dict = app.current_event.json_body # deserialize json str to dict + todo: Response = requests.post("https://jsonplaceholder.typicode.com/todos", data=todo_data) + todo.raise_for_status() + + return {"todo": todo.json()} + + +@middleware_after +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/middleware_factory/src/getting_started_middleware_after_logic_payload.json b/examples/middleware_factory/src/getting_started_middleware_after_logic_payload.json new file mode 100644 index 00000000000..e0f775d72df --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_after_logic_payload.json @@ -0,0 +1,6 @@ +{ + "resource": "/todos", + "path": "/todos", + "httpMethod": "POST", + "body": "{\"title\": \"foo\", \"userId\": 1, \"completed\": false}" +} diff --git a/examples/middleware_factory/src/getting_started_middleware_before_logic_function.py b/examples/middleware_factory/src/getting_started_middleware_before_logic_function.py new file mode 100644 index 00000000000..7d5ee035e7b --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_before_logic_function.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass, field +from typing import Callable +from uuid import uuid4 + +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.jmespath_utils import envelopes, extract_data_from_envelope +from aws_lambda_powertools.utilities.typing import LambdaContext + + +@dataclass +class Payment: + user_id: str + order_id: str + amount: float + status_id: str + payment_id: str = field(default_factory=lambda: f"{uuid4()}") + + +class PaymentError(Exception): + ... + + +@lambda_handler_decorator +def middleware_before(handler, event, context) -> Callable: + # extract payload from a EventBridge event + detail: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE) + + # check if status_id exists in payload, otherwise add default state before processing payment + if "status_id" not in detail: + event["detail"]["status_id"] = "pending" + + response = handler(event, context) + + return response + + +@middleware_before +def lambda_handler(event, context: LambdaContext) -> dict: + try: + payment_payload: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE) + return { + "order": Payment(**payment_payload).__dict__, + "message": "payment created", + "success": True, + } + except Exception as e: + raise PaymentError("Unable to create payment") from e diff --git a/examples/middleware_factory/src/getting_started_middleware_before_logic_payload.json b/examples/middleware_factory/src/getting_started_middleware_before_logic_payload.json new file mode 100644 index 00000000000..21fa5d9b6c7 --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_before_logic_payload.json @@ -0,0 +1,14 @@ +{ + "version": "0", + "id": "9c95e8e4-96a4-ef3f-b739-b6aa5b193afb", + "detail-type": "PaymentCreated", + "source": "app.payment", + "account": "0123456789012", + "time": "2022-08-08T20:41:53Z", + "region": "eu-east-1", + "detail": { + "amount": "150.00", + "order_id": "8f1f1710-1b30-48a5-a6bd-153fd23b866b", + "user_id": "f80e3c51-5b8c-49d5-af7d-c7804966235f" + } + } diff --git a/examples/middleware_factory/src/getting_started_middleware_tracer_function.py b/examples/middleware_factory/src/getting_started_middleware_tracer_function.py new file mode 100644 index 00000000000..0c461592254 --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_tracer_function.py @@ -0,0 +1,38 @@ +import time +from typing import Callable + +import requests +from requests import Response + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.typing import LambdaContext + +app = APIGatewayRestResolver() + + +@lambda_handler_decorator(trace_execution=True) +def middleware_with_tracing(handler, event, context) -> Callable: + + start_time = time.time() + response = handler(event, context) + execution_time = time.time() - start_time + + # adding custom headers in response object after lambda executing + response["headers"]["execution_time"] = execution_time + response["headers"]["aws_request_id"] = context.aws_request_id + + return response + + +@app.get("/products") +def create_product() -> dict: + product: Response = requests.get("https://dummyjson.com/products/1") + product.raise_for_status() + + return {"product": product.json()} + + +@middleware_with_tracing +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) diff --git a/examples/middleware_factory/src/getting_started_middleware_tracer_payload.json b/examples/middleware_factory/src/getting_started_middleware_tracer_payload.json new file mode 100644 index 00000000000..d8a89bcfc67 --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_tracer_payload.json @@ -0,0 +1,5 @@ +{ + "resource": "/products", + "path": "/products", + "httpMethod": "GET" + } diff --git a/examples/middleware_factory/src/getting_started_middleware_with_params_function.py b/examples/middleware_factory/src/getting_started_middleware_with_params_function.py new file mode 100644 index 00000000000..ce800e9162f --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_with_params_function.py @@ -0,0 +1,58 @@ +import base64 +from dataclasses import dataclass, field +from typing import Any, Callable, List +from uuid import uuid4 + +from aws_lambda_powertools.middleware_factory import lambda_handler_decorator +from aws_lambda_powertools.utilities.jmespath_utils import envelopes, extract_data_from_envelope +from aws_lambda_powertools.utilities.typing import LambdaContext + + +@dataclass +class Booking: + days: int + date_from: str + date_to: str + hotel_id: int + country: str + city: str + guest: dict + booking_id: str = field(default_factory=lambda: f"{uuid4()}") + + +class BookingError(Exception): + ... + + +@lambda_handler_decorator +def obfuscate_sensitive_data(handler, event, context, fields: List) -> Callable: + # extracting payload from a EventBridge event + detail: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE) + guest_data: Any = detail.get("guest") + + # Obfuscate fields (email, vat, passport) before calling Lambda handler + for guest_field in fields: + if guest_data.get(guest_field): + event["detail"]["guest"][guest_field] = obfuscate_data(str(guest_data.get(guest_field))) + + response = handler(event, context) + + return response + + +def obfuscate_data(value: str) -> bytes: + # base64 is not effective for obfuscation, this is an example + return base64.b64encode(value.encode("ascii")) + + +@obfuscate_sensitive_data(fields=["email", "passport", "vat"]) +def lambda_handler(event, context: LambdaContext) -> dict: + try: + booking_payload: dict = extract_data_from_envelope(data=event, envelope=envelopes.EVENTBRIDGE) + return { + "book": Booking(**booking_payload).__dict__, + "message": "booking created", + "success": True, + } + except Exception as e: + raise BookingError("Unable to create booking") from e diff --git a/examples/middleware_factory/src/getting_started_middleware_with_params_payload.json b/examples/middleware_factory/src/getting_started_middleware_with_params_payload.json new file mode 100644 index 00000000000..de6dbc626d3 --- /dev/null +++ b/examples/middleware_factory/src/getting_started_middleware_with_params_payload.json @@ -0,0 +1,23 @@ +{ + "version": "0", + "id": "9c95e8e4-96a4-ef3f-b739-b6aa5b193afb", + "detail-type": "BookingCreated", + "source": "app.booking", + "account": "0123456789012", + "time": "2022-08-08T20:41:53Z", + "region": "eu-east-1", + "detail": { + "days": 5, + "date_from": "2020-08-08", + "date_to": "2020-08-13", + "hotel_id": "1", + "country": "Portugal", + "city": "Lisbon", + "guest": { + "name": "Lambda", + "email": "lambda@powertool.tools", + "passport": "AA123456", + "vat": "123456789" + } + } +} diff --git a/examples/typing/src/getting_started_typing_function.py b/examples/typing/src/getting_started_typing_function.py new file mode 100644 index 00000000000..493f0c18b2e --- /dev/null +++ b/examples/typing/src/getting_started_typing_function.py @@ -0,0 +1,6 @@ +from aws_lambda_powertools.utilities.typing import LambdaContext + + +def handler(event: dict, context: LambdaContext) -> dict: + # Insert business logic + return event diff --git a/examples/typing/src/working_with_context_function.py b/examples/typing/src/working_with_context_function.py new file mode 100644 index 00000000000..bfe610efa38 --- /dev/null +++ b/examples/typing/src/working_with_context_function.py @@ -0,0 +1,32 @@ +from time import sleep + +import requests + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.utilities.typing import LambdaContext + +logger = Logger() + + +def lambda_handler(event, context: LambdaContext) -> dict: + + limit_execution: int = 1000 # milliseconds + + # scrape website and exit before lambda timeout + while context.get_remaining_time_in_millis() > limit_execution: + + comments: requests.Response = requests.get("https://jsonplaceholder.typicode.com/comments") + # add logic here and save the results of the request to an S3 bucket, for example. + + logger.info( + { + "operation": "scrape_website", + "request_id": context.aws_request_id, + "remaining_time": context.get_remaining_time_in_millis(), + "comments": comments.json()[:2], + } + ) + + sleep(1) + + return {"message": "Success"} diff --git a/examples/validation/src/custom_format_function.py b/examples/validation/src/custom_format_function.py new file mode 100644 index 00000000000..bf589018c5c --- /dev/null +++ b/examples/validation/src/custom_format_function.py @@ -0,0 +1,34 @@ +import json +import re + +import boto3 +import custom_format_schema as schemas + +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + +# awsaccountid must have 12 digits +custom_format = {"awsaccountid": lambda value: re.match(r"^(\d{12})$", value)} + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + # validate input using custom json format + validate(event=event, schema=schemas.INPUT, formats=custom_format) + + client_organization = boto3.client("organizations", region_name=event.get("region")) + account_data = client_organization.describe_account(AccountId=event.get("accountid")) + + return { + "account": json.dumps(account_data.get("Account"), default=str), + "message": "Success", + "statusCode": 200, + } + except SchemaValidationError as exception: + return return_error_message(str(exception)) + except Exception as exception: + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"account": None, "message": message, "statusCode": 400} diff --git a/examples/validation/src/custom_format_payload.json b/examples/validation/src/custom_format_payload.json new file mode 100644 index 00000000000..8f0607f94b0 --- /dev/null +++ b/examples/validation/src/custom_format_payload.json @@ -0,0 +1,4 @@ +{ + "accountid": "200984112386", + "region": "us-east-1" +} diff --git a/examples/validation/src/custom_format_schema.py b/examples/validation/src/custom_format_schema.py new file mode 100644 index 00000000000..e06a4b35e2d --- /dev/null +++ b/examples/validation/src/custom_format_schema.py @@ -0,0 +1,26 @@ +INPUT = { + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660245931.json", + "title": "Root", + "type": "object", + "required": ["accountid", "region"], + "properties": { + "accountid": { + "$id": "#root/accountid", + "title": "The accountid", + "type": "string", + "format": "awsaccountid", + "default": "", + "examples": ["123456789012"], + }, + "region": { + "$id": "#root/region", + "title": "The region", + "type": "string", + "default": "", + "examples": ["us-east-1"], + "pattern": "^.*$", + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_decorator_function.py b/examples/validation/src/getting_started_validator_decorator_function.py new file mode 100644 index 00000000000..bc371742860 --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_function.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass, field +from uuid import uuid4 + +import getting_started_validator_decorator_schema as schemas + +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import validator + +# we can get list of allowed IPs from AWS Parameter Store using Parameters Utility +# See: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parameters/ +ALLOWED_IPS = parameters.get_parameter("/lambda-powertools/allowed_ips") + + +class UserPermissionsError(Exception): + ... + + +@dataclass +class User: + ip: str + permissions: list + user_id: str = field(default_factory=lambda: f"{uuid4()}") + name: str = "Project Lambda Powertools" + + +# using a decorator to validate input and output data +@validator(inbound_schema=schemas.INPUT, outbound_schema=schemas.OUTPUT) +def lambda_handler(event, context: LambdaContext) -> dict: + + try: + user_details: dict = {} + + # get permissions by user_id and project + if ( + event.get("user_id") == "0d44b083-8206-4a3a-aa95-5d392a99be4a" + and event.get("project") == "powertools" + and event.get("ip") in ALLOWED_IPS + ): + user_details = User(ip=event.get("ip"), permissions=["read", "write"]).__dict__ + + # the body must be an object because must match OUTPUT schema, otherwise it fails + return {"body": user_details or None, "statusCode": 200 if user_details else 204} + except Exception as e: + raise UserPermissionsError(str(e)) diff --git a/examples/validation/src/getting_started_validator_decorator_payload.json b/examples/validation/src/getting_started_validator_decorator_payload.json new file mode 100644 index 00000000000..0e8bb8b752b --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_payload.json @@ -0,0 +1,6 @@ + +{ + "user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", + "project": "powertools", + "ip": "192.168.0.1" +} diff --git a/examples/validation/src/getting_started_validator_decorator_schema.py b/examples/validation/src/getting_started_validator_decorator_schema.py new file mode 100644 index 00000000000..1f74a2cc711 --- /dev/null +++ b/examples/validation/src/getting_started_validator_decorator_schema.py @@ -0,0 +1,60 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", "project": "powertools", "ip": "192.168.0.1"}], + "required": ["user_id", "project", "ip"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "string", + "title": "The user_id", + "examples": ["0d44b083-8206-4a3a-aa95-5d392a99be4a"], + "maxLength": 50, + }, + "project": { + "$id": "#/properties/project", + "type": "string", + "title": "The project", + "examples": ["powertools"], + "maxLength": 30, + }, + "ip": { + "$id": "#/properties/ip", + "type": "string", + "title": "The ip", + "format": "ipv4", + "examples": ["192.168.0.1"], + "maxLength": 30, + }, + }, +} + +OUTPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample outgoing schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"statusCode": 200, "body": {}}], + "required": ["statusCode", "body"], + "properties": { + "statusCode": { + "$id": "#/properties/statusCode", + "type": "integer", + "title": "The statusCode", + "examples": [200], + "maxLength": 3, + }, + "body": { + "$id": "#/properties/body", + "type": "object", + "title": "The body", + "examples": [ + '{"ip": "192.168.0.1", "permissions": ["read", "write"], "user_id": "7576b683-295e-4f69-b558-70e789de1b18", "name": "Project Lambda Powertools"}' # noqa E501 + ], + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_standalone_function.py b/examples/validation/src/getting_started_validator_standalone_function.py new file mode 100644 index 00000000000..1680511766b --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_function.py @@ -0,0 +1,30 @@ +import getting_started_validator_standalone_schema as schemas + +from aws_lambda_powertools.utilities import parameters +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import SchemaValidationError, validate + +# we can get list of allowed IPs from AWS Parameter Store using Parameters Utility +# See: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parameters/ +ALLOWED_IPS = parameters.get_parameter("/lambda-powertools/allowed_ips") + + +def lambda_handler(event, context: LambdaContext) -> dict: + try: + user_authenticated: str = "" + + # using standalone function to validate input data only + validate(event=event, schema=schemas.INPUT) + + if ( + event.get("user_id") == "0d44b083-8206-4a3a-aa95-5d392a99be4a" + and event.get("project") == "powertools" + and event.get("ip") in ALLOWED_IPS + ): + user_authenticated = "Allowed" + + # in this example the body can be of any type because we are not validating the OUTPUT + return {"body": user_authenticated, "statusCode": 200 if user_authenticated else 204} + except SchemaValidationError as exception: + # SchemaValidationError indicates where a data mismatch is + return {"body": str(exception), "statusCode": 400} diff --git a/examples/validation/src/getting_started_validator_standalone_payload.json b/examples/validation/src/getting_started_validator_standalone_payload.json new file mode 100644 index 00000000000..0e8bb8b752b --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_payload.json @@ -0,0 +1,6 @@ + +{ + "user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", + "project": "powertools", + "ip": "192.168.0.1" +} diff --git a/examples/validation/src/getting_started_validator_standalone_schema.py b/examples/validation/src/getting_started_validator_standalone_schema.py new file mode 100644 index 00000000000..28711157196 --- /dev/null +++ b/examples/validation/src/getting_started_validator_standalone_schema.py @@ -0,0 +1,33 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://example.com/example.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [{"user_id": "0d44b083-8206-4a3a-aa95-5d392a99be4a", "powertools": "lessa", "ip": "192.168.0.1"}], + "required": ["user_id", "project", "ip"], + "properties": { + "user_id": { + "$id": "#/properties/user_id", + "type": "string", + "title": "The user_id", + "examples": ["0d44b083-8206-4a3a-aa95-5d392a99be4a"], + "maxLength": 50, + }, + "project": { + "$id": "#/properties/project", + "type": "string", + "title": "The project", + "examples": ["powertools"], + "maxLength": 30, + }, + "ip": { + "$id": "#/properties/ip", + "type": "string", + "title": "The ip", + "format": "ipv4", + "examples": ["192.168.0.1"], + "maxLength": 30, + }, + }, +} diff --git a/examples/validation/src/getting_started_validator_unwrapping_function.py b/examples/validation/src/getting_started_validator_unwrapping_function.py new file mode 100644 index 00000000000..96c66a6f2d3 --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_function.py @@ -0,0 +1,36 @@ +import boto3 +import getting_started_validator_unwrapping_schema as schemas + +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import EventBridgeEvent +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import validator + +s3_client = boto3.resource("s3") + + +# we use the 'envelope' parameter to extract the payload inside the 'detail' key before validating +@validator(inbound_schema=schemas.INPUT, envelope="detail") +def lambda_handler(event: dict, context: LambdaContext) -> dict: + my_event = EventBridgeEvent(event) + data = my_event.detail.get("data", {}) + s3_bucket, s3_key = data.get("s3_bucket"), data.get("s3_key") + + try: + s3_object = s3_client.Object(bucket_name=s3_bucket, key=s3_key) + payload = s3_object.get()["Body"] + content = payload.read().decode("utf-8") + + return {"message": process_data_object(content), "success": True} + except s3_client.meta.client.exceptions.NoSuchBucket as exception: + return return_error_message(str(exception)) + except s3_client.meta.client.exceptions.NoSuchKey as exception: + return return_error_message(str(exception)) + + +def return_error_message(message: str) -> dict: + return {"message": message, "success": False} + + +def process_data_object(content: str) -> str: + # insert logic here + return "Data OK" diff --git a/examples/validation/src/getting_started_validator_unwrapping_payload.json b/examples/validation/src/getting_started_validator_unwrapping_payload.json new file mode 100644 index 00000000000..7757085361b --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_payload.json @@ -0,0 +1,17 @@ +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "CustomEvent", + "source": "mycompany.service", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": [], + "detail": { + "data": { + "s3_bucket": "aws-lambda-powertools", + "s3_key": "folder/event.txt", + "file_size": 200, + "file_type": "text/plain" + } + } +} \ No newline at end of file diff --git a/examples/validation/src/getting_started_validator_unwrapping_schema.py b/examples/validation/src/getting_started_validator_unwrapping_schema.py new file mode 100644 index 00000000000..2db9b8a4ab9 --- /dev/null +++ b/examples/validation/src/getting_started_validator_unwrapping_schema.py @@ -0,0 +1,59 @@ +INPUT = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660222326.json", + "type": "object", + "title": "Sample schema", + "description": "The root schema comprises the entire JSON document.", + "examples": [ + { + "data": { + "s3_bucket": "aws-lambda-powertools", + "s3_key": "event.txt", + "file_size": 200, + "file_type": "text/plain", + } + } + ], + "required": ["data"], + "properties": { + "data": { + "$id": "#root/data", + "title": "Root", + "type": "object", + "required": ["s3_bucket", "s3_key", "file_size", "file_type"], + "properties": { + "s3_bucket": { + "$id": "#root/data/s3_bucket", + "title": "The S3 Bucker", + "type": "string", + "default": "", + "examples": ["aws-lambda-powertools"], + "pattern": "^.*$", + }, + "s3_key": { + "$id": "#root/data/s3_key", + "title": "The S3 Key", + "type": "string", + "default": "", + "examples": ["folder/event.txt"], + "pattern": "^.*$", + }, + "file_size": { + "$id": "#root/data/file_size", + "title": "The file size", + "type": "integer", + "examples": [200], + "default": 0, + }, + "file_type": { + "$id": "#root/data/file_type", + "title": "The file type", + "type": "string", + "default": "", + "examples": ["text/plain"], + "pattern": "^.*$", + }, + }, + } + }, +} diff --git a/examples/validation/src/unwrapping_popular_event_source_function.py b/examples/validation/src/unwrapping_popular_event_source_function.py new file mode 100644 index 00000000000..8afbb5c727f --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_function.py @@ -0,0 +1,24 @@ +import boto3 +import unwrapping_popular_event_source_schema as schemas +from botocore.exceptions import ClientError + +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import EventBridgeEvent +from aws_lambda_powertools.utilities.typing import LambdaContext +from aws_lambda_powertools.utilities.validation import envelopes, validator + + +# extracting detail from EventBridge custom event +# see: https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/jmespath_functions/#built-in-envelopes +@validator(inbound_schema=schemas.INPUT, envelope=envelopes.EVENTBRIDGE) +def lambda_handler(event: dict, context: LambdaContext) -> dict: + my_event = EventBridgeEvent(event) + ec2_client = boto3.resource("ec2", region_name=my_event.region) + + try: + instance_id = my_event.detail.get("instance_id") + instance = ec2_client.Instance(instance_id) + instance.stop() + + return {"message": f"Successfully stopped {instance_id}", "success": True} + except ClientError as exception: + return {"message": str(exception), "success": False} diff --git a/examples/validation/src/unwrapping_popular_event_source_payload.json b/examples/validation/src/unwrapping_popular_event_source_payload.json new file mode 100644 index 00000000000..271e0be5b27 --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_payload.json @@ -0,0 +1,16 @@ + +{ + "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "1970-01-01T00:00:00Z", + "region": "us-east-1", + "resources": [ + "arn:aws:events:us-east-1:123456789012:rule/ExampleRule" + ], + "detail": { + "instance_id": "i-042dd005362091826", + "region": "us-east-2" + } +} diff --git a/examples/validation/src/unwrapping_popular_event_source_schema.py b/examples/validation/src/unwrapping_popular_event_source_schema.py new file mode 100644 index 00000000000..0c5cc746250 --- /dev/null +++ b/examples/validation/src/unwrapping_popular_event_source_schema.py @@ -0,0 +1,26 @@ +INPUT = { + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.com/object1660233148.json", + "title": "Root", + "type": "object", + "required": ["instance_id", "region"], + "properties": { + "instance_id": { + "$id": "#root/instance_id", + "title": "Instance_id", + "type": "string", + "default": "", + "examples": ["i-042dd005362091826"], + "pattern": "^.*$", + }, + "region": { + "$id": "#root/region", + "title": "Region", + "type": "string", + "default": "", + "examples": ["us-east-1"], + "pattern": "^.*$", + }, + }, +} diff --git a/layer/app.py b/layer/app.py index 78e99b17654..50f8090482e 100644 --- a/layer/app.py +++ b/layer/app.py @@ -12,12 +12,22 @@ if not POWERTOOLS_VERSION: raise ValueError( - "Please set the version for Powertools by passing the '--context=version:' parameter to the CDK " + "Please set the version for Powertools by passing the '--context version=' parameter to the CDK " "synth step." ) -LayerStack(app, "LayerStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN) - -CanaryStack(app, "CanaryStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN) +LayerStack( + app, + "LayerStack", + powertools_version=POWERTOOLS_VERSION, + ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN, +) + +CanaryStack( + app, + "CanaryStack", + powertools_version=POWERTOOLS_VERSION, + ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN, +) app.synth() diff --git a/layer/layer/canary/app.py b/layer/layer/canary/app.py index 31db94dd92b..1011fc654c2 100644 --- a/layer/layer/canary/app.py +++ b/layer/layer/canary/app.py @@ -42,7 +42,9 @@ def on_create(event): def check_envs(): - logger.info('Checking required envs ["POWERTOOLS_LAYER_ARN", "AWS_REGION", "STAGE"]') + logger.info( + 'Checking required envs ["POWERTOOLS_LAYER_ARN", "AWS_REGION", "STAGE"]' + ) if not layer_arn: raise ValueError("POWERTOOLS_LAYER_ARN is not set. Aborting...") if not powertools_version: @@ -73,6 +75,11 @@ def send_notification(): """ sends an event to version tracking event bridge """ + if stage != "PROD": + logger.info( + "Not sending notification to event bus, because this is not the PROD stage" + ) + return event = { "Time": datetime.datetime.now(), "Source": "powertools.layer.canary", @@ -80,10 +87,8 @@ def send_notification(): "DetailType": "deployment", "Detail": json.dumps( { - "id": "powertools-python", - "stage": stage, - "region": os.environ["AWS_REGION"], "version": powertools_version, + "region": os.environ["AWS_REGION"], "layerArn": layer_arn, } ), diff --git a/layer/poetry.lock b/layer/poetry.lock new file mode 100644 index 00000000000..182094a8b9d --- /dev/null +++ b/layer/poetry.lock @@ -0,0 +1,409 @@ +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "aws-cdk-lib" +version = "2.35.0" +description = "Version 2 of the AWS Cloud Development Kit library" +category = "main" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +constructs = ">=10.0.0,<11.0.0" +jsii = ">=1.63.2,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "boto3" +version = "1.24.46" +description = "The AWS SDK for Python" +category = "dev" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.27.46,<1.28.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.27.46" +description = "Low-level, data-driven core of boto 3." +category = "dev" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.13.8)"] + +[[package]] +name = "cattrs" +version = "22.1.0" +description = "Composable complex class support for attrs and dataclasses." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +attrs = ">=20" +exceptiongroup = {version = "*", markers = "python_version <= \"3.10\""} + +[[package]] +name = "cdk-lambda-powertools-python-layer" +version = "2.0.49" +description = "A lambda layer for AWS Powertools for python" +category = "main" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +aws-cdk-lib = ">=2.2.0,<3.0.0" +constructs = ">=10.0.5,<11.0.0" +jsii = ">=1.61.0,<2.0.0" +publication = ">=0.0.3" + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "constructs" +version = "10.1.67" +description = "A programming model for software-defined state" +category = "main" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +jsii = ">=1.63.2,<2.0.0" +publication = ">=0.0.3" +typeguard = ">=2.13.3,<2.14.0" + +[[package]] +name = "exceptiongroup" +version = "1.0.0rc8" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jsii" +version = "1.63.2" +description = "Python client for jsii runtime" +category = "main" +optional = false +python-versions = "~=3.7" + +[package.dependencies] +attrs = ">=21.2,<22.0" +cattrs = ">=1.8,<22.2" +publication = ">=0.0.3" +python-dateutil = "*" +typeguard = ">=2.13.3,<2.14.0" +typing-extensions = ">=3.7,<5.0" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "publication" +version = "0.0.3" +description = "Publication helps you maintain public-api-friendly modules by preventing unintentional access to private implementation details via introspection." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "dev" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +category = "main" +optional = false +python-versions = ">=3.5.3" + +[package.extras] +doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["pytest", "typing-extensions", "mypy"] + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "urllib3" +version = "1.26.11" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "a68a9649808efb49529ace7d990559e6569be096bf2d86234f3bd056bae0fdc3" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +aws-cdk-lib = [ + {file = "aws-cdk-lib-2.35.0.tar.gz", hash = "sha256:fc9cba4df0b60a9ab7f17ceb3b1c447d27e96cec9eb9e8c5b7ecfd1275878930"}, + {file = "aws_cdk_lib-2.35.0-py3-none-any.whl", hash = "sha256:ee481dca9335c32b5871e58ba697e27e2f1e92d9b81cf9341cfc6cc36127a2b0"}, +] +boto3 = [ + {file = "boto3-1.24.46-py3-none-any.whl", hash = "sha256:44026e44549148dbc5b261ead5f6b339e785680c350ef621bf85f7e2fca05b49"}, + {file = "boto3-1.24.46.tar.gz", hash = "sha256:b2d9d55f123a9a91eea2fd8e379d90abf37634420fbb45c22d67e10b324ec71b"}, +] +botocore = [ + {file = "botocore-1.27.46-py3-none-any.whl", hash = "sha256:747b7e94aef41498f063fc0be79c5af102d940beea713965179e1ead89c7e9ec"}, + {file = "botocore-1.27.46.tar.gz", hash = "sha256:f66d8305d1f59d83334df9b11b6512bb1e14698ec4d5d6d42f833f39f3304ca7"}, +] +cattrs = [ + {file = "cattrs-22.1.0-py3-none-any.whl", hash = "sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364"}, + {file = "cattrs-22.1.0.tar.gz", hash = "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6"}, +] +cdk-lambda-powertools-python-layer = [ + {file = "cdk-lambda-powertools-python-layer-2.0.49.tar.gz", hash = "sha256:8055fc691539f16e22a40e3d3df9c3f59fb28012437b08c47c639aefb001f1b2"}, + {file = "cdk_lambda_powertools_python_layer-2.0.49-py3-none-any.whl", hash = "sha256:9b0a7b7344f9ccb486564af728cefeac743687bfb131631e6d9171a55800dbac"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +constructs = [ + {file = "constructs-10.1.67-py3-none-any.whl", hash = "sha256:d597d8d5387328c1e95fa674d5d64969b1c1a479e63544e53a067a5d95b5c46b"}, + {file = "constructs-10.1.67.tar.gz", hash = "sha256:8b9fdf5040dde63545c08b8cc86fcd019512e0d16ee599c82b1201a5806f0066"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.0.0rc8-py3-none-any.whl", hash = "sha256:ab0a968e1ef769e55d9a596f4a89f7be9ffedbc9fdefdb77cc68cf5c33ce1035"}, + {file = "exceptiongroup-1.0.0rc8.tar.gz", hash = "sha256:6990c24f06b8d33c8065cfe43e5e8a4bfa384e0358be036af9cc60b6321bd11a"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +jmespath = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] +jsii = [ + {file = "jsii-1.63.2-py3-none-any.whl", hash = "sha256:ae8cbc84c633382c317dc367e1441bb2afd8b74ed82b3557b8df15e05316b14d"}, + {file = "jsii-1.63.2.tar.gz", hash = "sha256:6f68dcd82395ccd12606b31383f611adfefd246082750350891a2a277562f34b"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +publication = [ + {file = "publication-0.0.3-py2.py3-none-any.whl", hash = "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6"}, + {file = "publication-0.0.3.tar.gz", hash = "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +s3transfer = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typeguard = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +urllib3 = [] diff --git a/layer/pyproject.toml b/layer/pyproject.toml new file mode 100644 index 00000000000..7f219453a72 --- /dev/null +++ b/layer/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "aws-lambda-powertools-python-layer" +version = "0.1.0" +description = "AWS Lambda Powertools for Python Lambda Layers" +authors = ["DevAx "] +license = "MIT" + +[tool.poetry.dependencies] +python = "^3.9" +cdk-lambda-powertools-python-layer = "^2.0.49" +aws-cdk-lib = "^2.35.0" + +[tool.poetry.dev-dependencies] +pytest = "^7.1.2" +boto3 = "^1.24.46" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/layer/requirements-dev.txt b/layer/requirements-dev.txt deleted file mode 100644 index f3ec7d732b5..00000000000 --- a/layer/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==6.2.5 -boto3==1.24.22 diff --git a/layer/requirements.txt b/layer/requirements.txt deleted file mode 100644 index c165a46a846..00000000000 --- a/layer/requirements.txt +++ /dev/null @@ -1,84 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.9 -# To update, run: -# -# pip-compile --generate-hashes requirements.txt -# -attrs==22.1.0 \ - --hash=sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6 \ - --hash=sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c - # via - # -r requirements.txt - # cattrs - # jsii -aws-cdk-lib==2.31.1 \ - --hash=sha256:a07f6a247be110e874af374fa683d6c7eba86dfc9781cb555428b534c75bd4c0 \ - --hash=sha256:a3868c367cab3cf09e6bb68405e31f4342fc4a4905ccc3e3fdde133d520206c0 - # via - # -r requirements.txt - # cdk-lambda-powertools-python-layer -cattrs==22.1.0 \ - --hash=sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6 \ - --hash=sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364 - # via - # -r requirements.txt - # jsii -cdk-lambda-powertools-python-layer==2.0.49 \ - --hash=sha256:8055fc691539f16e22a40e3d3df9c3f59fb28012437b08c47c639aefb001f1b2 \ - --hash=sha256:9b0a7b7344f9ccb486564af728cefeac743687bfb131631e6d9171a55800dbac - # via -r requirements.txt -constructs==10.1.66 \ - --hash=sha256:0f9a7a34e4e07c11a792214481b41559e0ef17b3f3e6de6e018829c39882064e \ - --hash=sha256:79ecb6a23edafc9939a026d82f63fff6f3a0fe8520d3bc1d47f7731aa229eea4 - # via - # -r requirements.txt - # aws-cdk-lib - # cdk-lambda-powertools-python-layer -exceptiongroup==1.0.0rc8 \ - --hash=sha256:6990c24f06b8d33c8065cfe43e5e8a4bfa384e0358be036af9cc60b6321bd11a \ - --hash=sha256:ab0a968e1ef769e55d9a596f4a89f7be9ffedbc9fdefdb77cc68cf5c33ce1035 - # via - # -r requirements.txt - # cattrs -jsii==1.63.2 \ - --hash=sha256:6f68dcd82395ccd12606b31383f611adfefd246082750350891a2a277562f34b \ - --hash=sha256:ae8cbc84c633382c317dc367e1441bb2afd8b74ed82b3557b8df15e05316b14d - # via - # -r requirements.txt - # aws-cdk-lib - # cdk-lambda-powertools-python-layer - # constructs -publication==0.0.3 \ - --hash=sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6 \ - --hash=sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4 - # via - # -r requirements.txt - # aws-cdk-lib - # cdk-lambda-powertools-python-layer - # constructs - # jsii -python-dateutil==2.8.2 \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 - # via - # -r requirements.txt - # jsii -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # -r requirements.txt - # python-dateutil -typeguard==2.13.3 \ - --hash=sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4 \ - --hash=sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1 - # via - # -r requirements.txt - # constructs - # jsii -typing-extensions==4.3.0 \ - --hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02 \ - --hash=sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6 - # via - # -r requirements.txt - # jsii diff --git a/mkdocs.yml b/mkdocs.yml index 5b08b31a81d..171cf36eb13 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ theme: icon: material/toggle-switch name: Switch to light mode features: + - header.autohide - navigation.sections - navigation.expand - navigation.top diff --git a/mypy.ini b/mypy.ini index 8274442fe4b..4da15d3898a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -42,4 +42,7 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-aiohttp] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True + +[mypy-snappy] +ignore_missing_imports = True diff --git a/parallel_run_e2e.py b/parallel_run_e2e.py new file mode 100755 index 00000000000..b9603701e5e --- /dev/null +++ b/parallel_run_e2e.py @@ -0,0 +1,16 @@ +""" Calculate how many parallel workers are needed to complete E2E infrastructure jobs across available CPU Cores """ +import subprocess +from pathlib import Path + + +def main(): + features = Path("tests/e2e").rglob("infrastructure.py") + workers = len(list(features)) - 1 + + command = f"poetry run pytest -n {workers} --dist loadfile -o log_cli=true tests/e2e" + print(f"Running E2E tests with: {command}") + subprocess.run(command.split(), shell=False) + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 96d9c4e560b..c6696bdff76 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -89,14 +89,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.21.44" +version = "1.23.10" description = "The AWS SDK for Python" category = "main" optional = false python-versions = ">= 3.6" [package.dependencies] -botocore = ">=1.24.44,<1.25.0" +botocore = ">=1.26.10,<1.27.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.5.0,<0.6.0" @@ -105,7 +105,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.24.44" +version = "1.26.10" description = "Low-level, data-driven core of boto 3." category = "main" optional = false @@ -148,15 +148,15 @@ typing_extensions = {version = "*", markers = "python_version >= \"3.7\" and pyt [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.0.8" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -167,7 +167,7 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.0.4" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -179,7 +179,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -229,18 +229,19 @@ python-versions = ">=3.5" [[package]] name = "dnspython" -version = "2.1.0" +version = "2.2.1" description = "DNS toolkit" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.6,<4.0" [package.extras] -trio = ["sniffio (>=1.1)", "trio (>=0.14.0)"] -curio = ["sniffio (>=1.1)", "curio (>=1.2)"] -idna = ["idna (>=2.1)"] -doh = ["requests-toolbelt", "requests"] -dnssec = ["cryptography (>=2.6)"] +dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] [[package]] name = "email-validator" @@ -256,7 +257,7 @@ idna = ">=2.0.0" [[package]] name = "eradicate" -version = "2.0.0" +version = "2.1.0" description = "Removes commented-out code." category = "dev" optional = false @@ -296,35 +297,34 @@ python-versions = "*" devel = ["colorama", "jsonschema", "json-spec", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] [[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" +name = "filelock" +version = "3.8.0" +description = "A platform independent file lock." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +[package.extras] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] -name = "flake8-black" -version = "0.2.3" -description = "flake8 plugin to call black as a code style validator" +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -black = "*" -flake8 = ">=3.0.0" -toml = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-bugbear" -version = "22.7.1" +version = "22.8.23" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -378,7 +378,7 @@ six = "*" [[package]] name = "flake8-eradicate" -version = "1.2.1" +version = "1.3.0" description = "Flake8 plugin to find commented out code" category = "dev" optional = false @@ -387,7 +387,7 @@ python-versions = ">=3.6,<4.0" [package.dependencies] attrs = "*" eradicate = ">=2.0,<3.0" -flake8 = ">=3.5,<5" +flake8 = ">=3.5,<6" [[package]] name = "flake8-fixme" @@ -399,14 +399,14 @@ python-versions = "*" [[package]] name = "flake8-isort" -version = "4.1.2.post0" +version = "4.2.0" description = "flake8 plugin that integrates isort ." category = "dev" optional = false python-versions = "*" [package.dependencies] -flake8 = ">=3.2.1,<5" +flake8 = ">=3.2.1,<6" isort = ">=4.3.5,<6" [package.extras] @@ -430,7 +430,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "ghp-import" -version = "2.0.2" +version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." category = "dev" optional = false @@ -475,19 +475,20 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "4.2.0" +version = "4.12.0" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "importlib-resources" @@ -597,12 +598,15 @@ restructuredText = ["rst2ansi"] [[package]] name = "markdown" -version = "3.3.5" +version = "3.3.7" description = "Python implementation of Markdown." category = "dev" optional = false python-versions = ">=3.6" +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + [package.extras] testing = ["coverage", "pyyaml"] @@ -650,7 +654,7 @@ dev = ["pypandoc (>=1.4)", "flake8 (>=3.0)", "coverage"] [[package]] name = "mkdocs" -version = "1.2.3" +version = "1.3.1" description = "Project documentation with Markdown." category = "dev" optional = false @@ -659,9 +663,9 @@ python-versions = ">=3.6" [package.dependencies] click = ">=3.3" ghp-import = ">=1.0" -importlib-metadata = ">=3.10" -Jinja2 = ">=2.10.1" -Markdown = ">=3.2.1" +importlib-metadata = ">=4.3" +Jinja2 = ">=2.10.2" +Markdown = ">=3.2.1,<3.4" mergedeep = ">=1.3.4" packaging = ">=20.5" PyYAML = ">=3.10" @@ -686,19 +690,19 @@ mkdocs = ">=0.17" [[package]] name = "mkdocs-material" -version = "8.2.7" -description = "A Material Design theme for MkDocs" +version = "8.4.1" +description = "Documentation that simply works" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -jinja2 = ">=2.11.1,<3.1" +jinja2 = ">=3.0.2" markdown = ">=3.2" -mkdocs = ">=1.2.3" -mkdocs-material-extensions = ">=1.0" -pygments = ">=2.10" -pymdown-extensions = ">=9.0" +mkdocs = ">=1.3.0" +mkdocs-material-extensions = ">=1.0.3" +pygments = ">=2.12" +pymdown-extensions = ">=9.4" [[package]] name = "mkdocs-material-extensions" @@ -729,77 +733,110 @@ reports = ["lxml"] [[package]] name = "mypy-boto3-appconfig" -version = "1.24.29" -description = "Type annotations for boto3.AppConfig 1.24.29 service generated with mypy-boto3-builder 7.7.3" +version = "1.24.36.post1" +description = "Type annotations for boto3.AppConfig 1.24.36 service generated with mypy-boto3-builder 7.10.0" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-cloudformation" +version = "1.24.36.post1" +description = "Type annotations for boto3.CloudFormation 1.24.36 service generated with mypy-boto3-builder 7.10.0" +category = "dev" +optional = false +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-cloudwatch" -version = "1.24.35" -description = "Type annotations for boto3.CloudWatch 1.24.35 service generated with mypy-boto3-builder 7.9.2" +version = "1.24.55" +description = "Type annotations for boto3.CloudWatch 1.24.55 service generated with mypy-boto3-builder 7.11.6" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-dynamodb" -version = "1.24.27" -description = "Type annotations for boto3.DynamoDB 1.24.27 service generated with mypy-boto3-builder 7.6.0" +version = "1.24.55.post1" +description = "Type annotations for boto3.DynamoDB 1.24.55 service generated with mypy-boto3-builder 7.11.7" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-lambda" -version = "1.24.0" -description = "Type annotations for boto3.Lambda 1.24.0 service generated with mypy-boto3-builder 7.6.1" +version = "1.24.54" +description = "Type annotations for boto3.Lambda 1.24.54 service generated with mypy-boto3-builder 7.11.6" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-logs" +version = "1.24.36.post1" +description = "Type annotations for boto3.CloudWatchLogs 1.24.36 service generated with mypy-boto3-builder 7.10.0" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = ">=4.1.0" + +[[package]] +name = "mypy-boto3-s3" +version = "1.24.36.post1" +description = "Type annotations for boto3.S3 1.24.36 service generated with mypy-boto3-builder 7.10.0" +category = "dev" +optional = false +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-secretsmanager" -version = "1.24.11.post3" -description = "Type annotations for boto3.SecretsManager 1.24.11 service generated with mypy-boto3-builder 7.7.1" +version = "1.24.54" +description = "Type annotations for boto3.SecretsManager 1.24.54 service generated with mypy-boto3-builder 7.11.6" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-ssm" -version = "1.24.0" -description = "Type annotations for boto3.SSM 1.24.0 service generated with mypy-boto3-builder 7.6.1" +version = "1.24.39.post2" +description = "Type annotations for boto3.SSM 1.24.39 service generated with mypy-boto3-builder 7.10.1" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" [[package]] name = "mypy-boto3-xray" -version = "1.24.0" -description = "Type annotations for boto3.XRay 1.24.0 service generated with mypy-boto3-builder 7.6.1" +version = "1.24.36.post1" +description = "Type annotations for boto3.XRay 1.24.36 service generated with mypy-boto3-builder 7.10.0" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = ">=4.1.0" @@ -833,7 +870,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pbr" -version = "5.8.0" +version = "5.10.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -860,8 +897,8 @@ optional = false python-versions = ">=3.6" [package.extras] -test = ["pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "pytest (>=6)", "appdirs (==1.4.4)"] -docs = ["sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)", "Sphinx (>=4)"] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -904,15 +941,15 @@ python-versions = "*" [[package]] name = "pycodestyle" -version = "2.8.0" +version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.9.1" +version = "1.9.2" description = "Data validation and settings management using python type hints" category = "main" optional = true @@ -928,7 +965,7 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" -version = "2.4.0" +version = "2.3.1" description = "passive checker of Python programs" category = "dev" optional = false @@ -936,26 +973,29 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.11.2" +version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "9.1" +version = "9.5" description = "Extension pack for Python Markdown." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -Markdown = ">=3.2" +markdown = ">=3.2" [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false @@ -1087,13 +1127,21 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "python-snappy" +version = "0.6.1" +description = "Python library for the snappy compression library from Google" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" [[package]] name = "pyyaml-env-tag" @@ -1121,7 +1169,7 @@ mando = ">=0.6,<0.7" [[package]] name = "requests" -version = "2.26.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "dev" optional = false @@ -1151,14 +1199,14 @@ py = ">=1.4.26,<2.0.0" [[package]] name = "ruamel.yaml" -version = "0.17.17" +version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" category = "dev" optional = false python-versions = ">=3" [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} [package.extras] docs = ["ryd"] @@ -1174,7 +1222,7 @@ python-versions = ">=3.5" [[package]] name = "s3transfer" -version = "0.5.0" +version = "0.5.2" description = "An Amazon S3 Transfer Manager" category = "main" optional = false @@ -1214,17 +1262,9 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" -version = "1.2.2" +version = "1.2.3" description = "A lil' TOML parser" category = "dev" optional = false @@ -1232,15 +1272,15 @@ python-versions = ">=3.6" [[package]] name = "typed-ast" -version = "1.4.3" +version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "types-requests" -version = "2.28.7" +version = "2.28.9" description = "Typing stubs for requests" category = "dev" optional = false @@ -1251,7 +1291,7 @@ types-urllib3 = "<1.27" [[package]] name = "types-urllib3" -version = "1.26.17" +version = "1.26.23" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -1267,20 +1307,20 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "watchdog" -version = "2.1.6" +version = "2.1.9" description = "Filesystem events monitoring" category = "dev" optional = false @@ -1291,7 +1331,7 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wrapt" -version = "1.13.3" +version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = false @@ -1312,15 +1352,15 @@ requests = ">=2.0,<3.0" [[package]] name = "zipp" -version = "3.6.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] pydantic = ["pydantic", "email-validator"] @@ -1328,12 +1368,11 @@ pydantic = ["pydantic", "email-validator"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "575cd7f9ff3a989898ec6f9944aab56b4e08964a37173d49b34e1e1bbc6a3d39" +content-hash = "a5ff8f9945c42eeee596b973e484efae6bcfde17a0e37cc708cac05b635cec1f" [metadata.files] atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, @@ -1356,12 +1395,12 @@ black = [ {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, ] boto3 = [ - {file = "boto3-1.21.44-py3-none-any.whl", hash = "sha256:0789842ca7d722723d7e9fae2158aea6f304c14df08929f9c62b6a277705ff39"}, - {file = "boto3-1.21.44.tar.gz", hash = "sha256:1300661bd4defa42d7e019d515fbfd2984170cf3f5c0bf6bc275fbd9498faf5f"}, + {file = "boto3-1.23.10-py3-none-any.whl", hash = "sha256:40d08614f17a69075e175c02c5d5aab69a6153fd50e40fa7057b913ac7bf40e7"}, + {file = "boto3-1.23.10.tar.gz", hash = "sha256:2a4395e3241c20eef441d7443a5e6eaa0ee3f7114653fb9d9cef41587526f7bd"}, ] botocore = [ - {file = "botocore-1.24.44-py3-none-any.whl", hash = "sha256:ed07772c924984e5b3c1005f7ba4600cebd4169c23307cf6e92cccadf0b5d2e7"}, - {file = "botocore-1.24.44.tar.gz", hash = "sha256:0030a11eac972be46859263820885ba650503622c5acfe58966f482d42cc538d"}, + {file = "botocore-1.26.10-py3-none-any.whl", hash = "sha256:8a4a984bf901ccefe40037da11ba2abd1ddbcb3b490a492b7f218509c99fc12f"}, + {file = "botocore-1.26.10.tar.gz", hash = "sha256:5df2cf7ebe34377470172bd0bbc582cf98c5cbd02da0909a14e9e2885ab3ae9c"}, ] cattrs = [ {file = "cattrs-1.0.0-py2.py3-none-any.whl", hash = "sha256:616972ae3dfa6e623a40ad3cb845420e64942989152774ab055e5c2b2f89f997"}, @@ -1370,20 +1409,20 @@ cattrs = [ {file = "cattrs-22.1.0.tar.gz", hash = "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.8.tar.gz", hash = "sha256:735e240d9a8506778cd7a453d97e817e536bb1fc29f4f6961ce297b9c7a917b0"}, - {file = "charset_normalizer-2.0.8-py3-none-any.whl", hash = "sha256:83fcdeb225499d6344c8f7f34684c2981270beacc32ede2e669e94f7fa544405"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] constructs = [ {file = "constructs-10.1.1-py3-none-any.whl", hash = "sha256:c1f3deb196f54e070ded3c92c4339f73ef2b6022d35fb34908c0ebfa7ef8a640"}, @@ -1447,15 +1486,16 @@ decorator = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] dnspython = [ - {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, - {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] email-validator = [ {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, ] eradicate = [ - {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, + {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"}, + {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"}, ] exceptiongroup = [ {file = "exceptiongroup-1.0.0rc8-py3-none-any.whl", hash = "sha256:ab0a968e1ef769e55d9a596f4a89f7be9ffedbc9fdefdb77cc68cf5c33ce1035"}, @@ -1469,17 +1509,17 @@ fastjsonschema = [ {file = "fastjsonschema-2.16.1-py3-none-any.whl", hash = "sha256:2f7158c4de792555753d6c2277d6a2af2d406dfd97aeca21d17173561ede4fe6"}, {file = "fastjsonschema-2.16.1.tar.gz", hash = "sha256:d6fa3ffbe719768d70e298b9fb847484e2bdfdb7241ed052b8d57a9294a8c334"}, ] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +filelock = [ + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] -flake8-black = [ - {file = "flake8-black-0.2.3.tar.gz", hash = "sha256:c199844bc1b559d91195ebe8620216f21ed67f2cc1ff6884294c91a0d2492684"}, - {file = "flake8_black-0.2.3-py3-none-any.whl", hash = "sha256:cc080ba5b3773b69ba102b6617a00cc4ecbad8914109690cfda4d565ea435d96"}, +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-22.7.1.tar.gz", hash = "sha256:e450976a07e4f9d6c043d4f72b17ec1baf717fe37f7997009c8ae58064f88305"}, - {file = "flake8_bugbear-22.7.1-py3-none-any.whl", hash = "sha256:db5d7a831ef4412a224b26c708967ff816818cabae415e76b8c58df156c4b8e5"}, + {file = "flake8-bugbear-22.8.23.tar.gz", hash = "sha256:de0717d11124a082118dd08387b34fd86b2721642ec2d8e92be66cfa5ea7c445"}, + {file = "flake8_bugbear-22.8.23-py3-none-any.whl", hash = "sha256:1b0ebe0873d1cd55bf9f1588bfcb930db339018ef44a3981a26532daa9fd14a8"}, ] flake8-builtins = [ {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, @@ -1494,16 +1534,16 @@ flake8-debugger = [ {file = "flake8_debugger-4.0.0-py3-none-any.whl", hash = "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032"}, ] flake8-eradicate = [ - {file = "flake8-eradicate-1.2.1.tar.gz", hash = "sha256:e486f8ab7e2dba3667223688e9239158fbf4ecaa88125e2283bcda81171412b7"}, - {file = "flake8_eradicate-1.2.1-py3-none-any.whl", hash = "sha256:00d77faefb64cef18b3c1b48a004c3a2ad663aa3cf85650f422437d25ece6441"}, + {file = "flake8-eradicate-1.3.0.tar.gz", hash = "sha256:e4c98f00d17dc8653e3388cac2624cd81e9735de2fd4a8dcf99029633ebd7a63"}, + {file = "flake8_eradicate-1.3.0-py3-none-any.whl", hash = "sha256:85a71e0c5f4e07f7c6c5fec520483561fd6bd295417d622855bdeade99242e3d"}, ] flake8-fixme = [ {file = "flake8-fixme-1.1.1.tar.gz", hash = "sha256:50cade07d27a4c30d4f12351478df87339e67640c83041b664724bda6d16f33a"}, {file = "flake8_fixme-1.1.1-py2.py3-none-any.whl", hash = "sha256:226a6f2ef916730899f29ac140bed5d4a17e5aba79f00a0e3ae1eff1997cb1ac"}, ] flake8-isort = [ - {file = "flake8-isort-4.1.2.post0.tar.gz", hash = "sha256:dee69bc3c09f0832df88acf795845db8a6673b79237371a05fa927ce095248e5"}, - {file = "flake8_isort-4.1.2.post0-py3-none-any.whl", hash = "sha256:4f95b40706dbb507cff872b34683283662e945d6028d3c8257e69de5fc6b7446"}, + {file = "flake8-isort-4.2.0.tar.gz", hash = "sha256:26571500cd54976bbc0cf1006ffbcd1a68dd102f816b7a1051b219616ba9fee0"}, + {file = "flake8_isort-4.2.0-py3-none-any.whl", hash = "sha256:5b87630fb3719bf4c1833fd11e0d9534f43efdeba524863e15d8f14a7ef6adbf"}, ] flake8-variables-names = [ {file = "flake8_variables_names-0.0.4.tar.gz", hash = "sha256:d6fa0571a807c72940b5773827c5760421ea6f8206595ff0a8ecfa01e42bf2cf"}, @@ -1512,8 +1552,8 @@ future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, ] ghp-import = [ - {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, - {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, @@ -1528,8 +1568,8 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, ] importlib-resources = [ {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, @@ -1564,8 +1604,8 @@ mando = [ {file = "mando-0.6.4.tar.gz", hash = "sha256:79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"}, ] markdown = [ - {file = "Markdown-3.3.5-py3-none-any.whl", hash = "sha256:0d2d09f75cb8d1ffc6770c65c61770b23a61708101f47bda416a002a0edbc480"}, - {file = "Markdown-3.3.5.tar.gz", hash = "sha256:26e9546bfbcde5fcd072bd8f612c9c1b6e2677cb8aadbdf65206674f46dde069"}, + {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, + {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, ] markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, @@ -1651,15 +1691,15 @@ mike = [ {file = "mike-0.6.0.tar.gz", hash = "sha256:6d6239de2a60d733da2f34617e9b9a14c4b5437423b47e524f14dc96d6ce5f2f"}, ] mkdocs = [ - {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, - {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, + {file = "mkdocs-1.3.1-py3-none-any.whl", hash = "sha256:fda92466393127d2da830bc6edc3a625a14b436316d1caf347690648e774c4f0"}, + {file = "mkdocs-1.3.1.tar.gz", hash = "sha256:a41a2ff25ce3bbacc953f9844ba07d106233cd76c88bac1f59cb1564ac0d87ed"}, ] mkdocs-git-revision-date-plugin = [ {file = "mkdocs_git_revision_date_plugin-0.3.2-py3-none-any.whl", hash = "sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef"}, ] mkdocs-material = [ - {file = "mkdocs-material-8.2.7.tar.gz", hash = "sha256:3314d94ccc11481b1a3aa4f7babb4fb2bc47daa2fa8ace2463665952116f409b"}, - {file = "mkdocs_material-8.2.7-py2.py3-none-any.whl", hash = "sha256:20c13aa0a54841e1f1c080edb0e3573407884e4abea51ee25573061189bec83e"}, + {file = "mkdocs-material-8.4.1.tar.gz", hash = "sha256:92c70f94b2e1f8a05d9e05eec1c7af9dffc516802d69222329db89503c97b4f3"}, + {file = "mkdocs_material-8.4.1-py2.py3-none-any.whl", hash = "sha256:319a6254819ce9d864ff79de48c43842fccfdebb43e4e6820eef75216f8cfb0a"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, @@ -1691,32 +1731,44 @@ mypy = [ {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, ] mypy-boto3-appconfig = [ - {file = "mypy-boto3-appconfig-1.24.29.tar.gz", hash = "sha256:10583d309a9db99babfbe85d3b6467b49b3509a57e4f8771da239f6d5cb3731b"}, - {file = "mypy_boto3_appconfig-1.24.29-py3-none-any.whl", hash = "sha256:e9d9e2e25fdd82bffc6262dc184edf5d0d3d9fbb0ab35e597a1ea57ba13d4d80"}, + {file = "mypy-boto3-appconfig-1.24.36.post1.tar.gz", hash = "sha256:e1916b3754915cb411ef977083500e1f30f81f7b3aea6ff5eed1cec91944dea6"}, + {file = "mypy_boto3_appconfig-1.24.36.post1-py3-none-any.whl", hash = "sha256:a5dbe549dbebf4bc7a6cfcbfa9dff89ceb4983c042b785763ee656504bdb49f6"}, +] +mypy-boto3-cloudformation = [ + {file = "mypy-boto3-cloudformation-1.24.36.post1.tar.gz", hash = "sha256:ed7df9ae3a8390a145229122a1489d0a58bbf9986cb54f0d7a65ed54f12c8e63"}, + {file = "mypy_boto3_cloudformation-1.24.36.post1-py3-none-any.whl", hash = "sha256:b39020c13a876bb18908aad22326478d0ac3faec0bdac0d2c11dc318c9dcf149"}, ] mypy-boto3-cloudwatch = [ - {file = "mypy-boto3-cloudwatch-1.24.35.tar.gz", hash = "sha256:92a818e2ea330f9afb5f8f9c15df47934736041e3ccfd696ffc0774bad14e0aa"}, - {file = "mypy_boto3_cloudwatch-1.24.35-py3-none-any.whl", hash = "sha256:28947763d70cdac24aca25779cd5b00cd995636f5815fac3d95009430ce02b72"}, + {file = "mypy-boto3-cloudwatch-1.24.55.tar.gz", hash = "sha256:f8950de7a93b3db890cd8524514a2245d9b5fd83ce2dd60a37047a2cd42d5dd6"}, + {file = "mypy_boto3_cloudwatch-1.24.55-py3-none-any.whl", hash = "sha256:23faf8fdfe928f9dcce453a60b03bda69177554eb88c2d7e5240ff91b5b14388"}, ] mypy-boto3-dynamodb = [ - {file = "mypy-boto3-dynamodb-1.24.27.tar.gz", hash = "sha256:c982d24f9b2525a70f408ad40eff69660d56928217597d88860b60436b25efbf"}, - {file = "mypy_boto3_dynamodb-1.24.27-py3-none-any.whl", hash = "sha256:63f7d9755fc5cf2e637edf8d33024050152a53013d1a102716ae0d534563ef07"}, + {file = "mypy-boto3-dynamodb-1.24.55.post1.tar.gz", hash = "sha256:c469223c15556d93d247d38c0c31ce3c08d8073ca4597158a27abc70b8d7fbee"}, + {file = "mypy_boto3_dynamodb-1.24.55.post1-py3-none-any.whl", hash = "sha256:c762975d023b356c573d58105c7bfc1b9e7ee62c1299f09784e9dede533179e1"}, ] mypy-boto3-lambda = [ - {file = "mypy-boto3-lambda-1.24.0.tar.gz", hash = "sha256:ab425f941d0d50a2b8a20cc13cebe03c3097b122259bf00e7b295d284814bd6f"}, - {file = "mypy_boto3_lambda-1.24.0-py3-none-any.whl", hash = "sha256:a286a464513adf50847bda8573f2dc7adc348234827d1ac0200e610ee9a09b80"}, + {file = "mypy-boto3-lambda-1.24.54.tar.gz", hash = "sha256:c76d28d84bdf94c8980acd85bc07f2747559ca11a990fd6785c9c2389e13aff1"}, + {file = "mypy_boto3_lambda-1.24.54-py3-none-any.whl", hash = "sha256:231b6aac22b107ebb7afa2ec6dc1311b769dbdd5bfae957cf60db3e8bc3133d7"}, +] +mypy-boto3-logs = [ + {file = "mypy-boto3-logs-1.24.36.post1.tar.gz", hash = "sha256:8b00c2d5328e72023b1d1acd65e7cea7854f07827d23ce21c78391ca74271290"}, + {file = "mypy_boto3_logs-1.24.36.post1-py3-none-any.whl", hash = "sha256:f96257ec06099bfda1ce5f35b410e7fb93fb601bc312e8d7a09b13adaefd23f0"}, +] +mypy-boto3-s3 = [ + {file = "mypy-boto3-s3-1.24.36.post1.tar.gz", hash = "sha256:3bd7e06f9ade5059eae2181d7a9f1a41e7fa807ad3e94c01c9901838e87e0abe"}, + {file = "mypy_boto3_s3-1.24.36.post1-py3-none-any.whl", hash = "sha256:30ae59b33c55f8b7b693170f9519ea5b91a2fbf31a73de79cdef57a27d784e5a"}, ] mypy-boto3-secretsmanager = [ - {file = "mypy-boto3-secretsmanager-1.24.11.post3.tar.gz", hash = "sha256:f153b3f5ff2c65664a906fb2c97a6598a57da9f1da77679dbaf541051dcff36e"}, - {file = "mypy_boto3_secretsmanager-1.24.11.post3-py3-none-any.whl", hash = "sha256:d9655d568f7fd8fe05265613b85fba55ab6e4dcd078989af1ef9f0ffe4b45019"}, + {file = "mypy-boto3-secretsmanager-1.24.54.tar.gz", hash = "sha256:a846b79f86e218a794dbc858c08290bb6aebffa180c80cf0a463c32a04621ff1"}, + {file = "mypy_boto3_secretsmanager-1.24.54-py3-none-any.whl", hash = "sha256:b89c9a0ff65a8ab2c4e4d3f6e721a0477b7d0fec246ffc08e4378420eb50b4d0"}, ] mypy-boto3-ssm = [ - {file = "mypy-boto3-ssm-1.24.0.tar.gz", hash = "sha256:bab58398947c3627a4e7610cd0f57b525c12fd1d0a6bb862400b6af0a4e684fc"}, - {file = "mypy_boto3_ssm-1.24.0-py3-none-any.whl", hash = "sha256:1f17055abb8d70f25e6ece2ef4c0dc74d585744c25a3a833c2985d74165ac0c6"}, + {file = "mypy-boto3-ssm-1.24.39.post2.tar.gz", hash = "sha256:2859bdcef110d9cc53007a7adba9c765e804b886f98d742a496bb8f7dac07308"}, + {file = "mypy_boto3_ssm-1.24.39.post2-py3-none-any.whl", hash = "sha256:bfdb434c513fbb1f3bc4b5c158ed4e7a46cb578e5eb01e818d45f4f38296ef2c"}, ] mypy-boto3-xray = [ - {file = "mypy-boto3-xray-1.24.0.tar.gz", hash = "sha256:fbe211b7601684a2d4defa2f959286f1441027c15044c0c0013257e22307778a"}, - {file = "mypy_boto3_xray-1.24.0-py3-none-any.whl", hash = "sha256:6b9bc96e7924215fe833fe0d732d5e3ce98f7739b373432b9735a9905f867171"}, + {file = "mypy-boto3-xray-1.24.36.post1.tar.gz", hash = "sha256:104f1ecf7f1f6278c582201e71a7ab64843d3a3fdc8f23295cf68788cc77e9bb"}, + {file = "mypy_boto3_xray-1.24.36.post1-py3-none-any.whl", hash = "sha256:97b9f0686c717c8be99ac06cb52febaf71712b4e4cd0b61ed2eb5ed012a9b5fd"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1731,8 +1783,8 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pbr = [ - {file = "pbr-5.8.0-py2.py3-none-any.whl", hash = "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a"}, - {file = "pbr-5.8.0.tar.gz", hash = "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf"}, + {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, + {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, ] pdoc3 = [ {file = "pdoc3-0.10.0-py3-none-any.whl", hash = "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b"}, @@ -1758,61 +1810,61 @@ py-cpuinfo = [ {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, ] pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pydantic = [ - {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, - {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, - {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"}, - {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"}, - {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"}, - {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"}, - {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"}, - {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"}, - {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"}, - {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"}, - {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"}, - {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"}, - {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"}, - {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"}, - {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"}, - {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"}, - {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"}, - {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"}, - {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"}, - {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"}, - {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"}, - {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"}, - {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"}, - {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"}, - {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"}, - {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"}, - {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"}, - {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"}, - {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"}, - {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"}, - {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"}, - {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"}, - {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"}, - {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"}, - {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"}, + {file = "pydantic-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e"}, + {file = "pydantic-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08"}, + {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c"}, + {file = "pydantic-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131"}, + {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76"}, + {file = "pydantic-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567"}, + {file = "pydantic-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044"}, + {file = "pydantic-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555"}, + {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84"}, + {file = "pydantic-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f"}, + {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb"}, + {file = "pydantic-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b"}, + {file = "pydantic-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001"}, + {file = "pydantic-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56"}, + {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4"}, + {file = "pydantic-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f"}, + {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979"}, + {file = "pydantic-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d"}, + {file = "pydantic-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7"}, + {file = "pydantic-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3"}, + {file = "pydantic-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa"}, + {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3"}, + {file = "pydantic-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0"}, + {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8"}, + {file = "pydantic-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8"}, + {file = "pydantic-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326"}, + {file = "pydantic-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801"}, + {file = "pydantic-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44"}, + {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747"}, + {file = "pydantic-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453"}, + {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb"}, + {file = "pydantic-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15"}, + {file = "pydantic-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55"}, + {file = "pydantic-1.9.2-py3-none-any.whl", hash = "sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e"}, + {file = "pydantic-1.9.2.tar.gz", hash = "sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d"}, ] pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pygments = [ - {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, - {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-9.1.tar.gz", hash = "sha256:74247f2c80f1d9e3c7242abe1c16317da36c6f26c7ad4b8a7f457f0ec20f0365"}, - {file = "pymdown_extensions-9.1-py3-none-any.whl", hash = "sha256:b03e66f91f33af4a6e7a0e20c740313522995f69a03d86316b1449766c473d0e"}, + {file = "pymdown_extensions-9.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"}, + {file = "pymdown_extensions-9.5.tar.gz", hash = "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, @@ -1846,36 +1898,90 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +python-snappy = [ + {file = "python-snappy-0.6.1.tar.gz", hash = "sha256:b6a107ab06206acc5359d4c5632bd9b22d448702a79b3169b0c62e0fb808bb2a"}, + {file = "python_snappy-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b7f920eaf46ebf41bd26f9df51c160d40f9e00b7b48471c3438cb8d027f7fb9b"}, + {file = "python_snappy-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4ec533a8c1f8df797bded662ec3e494d225b37855bb63eb0d75464a07947477c"}, + {file = "python_snappy-0.6.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6f8bf4708a11b47517baf962f9a02196478bbb10fdb9582add4aa1459fa82380"}, + {file = "python_snappy-0.6.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d0c019ee7dcf2c60e240877107cddbd95a5b1081787579bf179938392d66480"}, + {file = "python_snappy-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb18d9cd7b3f35a2f5af47bb8ed6a5bdbf4f3ddee37f3daade4ab7864c292f5b"}, + {file = "python_snappy-0.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b265cde49774752aec9ca7f5d272e3f98718164afc85521622a8a5394158a2b5"}, + {file = "python_snappy-0.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d017775851a778ec9cc32651c4464079d06d927303c2dde9ae9830ccf6fe94e1"}, + {file = "python_snappy-0.6.1-cp310-cp310-win32.whl", hash = "sha256:8277d1f6282463c40761f802b742f833f9f2449fcdbb20a96579aa05c8feb614"}, + {file = "python_snappy-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:2aaaf618c68d8c9daebc23a20436bd01b09ee70d7fbf7072b7f38b06d2fab539"}, + {file = "python_snappy-0.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:277757d5dad4e239dc1417438a0871b65b1b155beb108888e7438c27ffc6a8cc"}, + {file = "python_snappy-0.6.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e066a0586833d610c4bbddba0be5ba0e3e4f8e0bc5bb6d82103d8f8fc47bb59a"}, + {file = "python_snappy-0.6.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0d489b50f49433494160c45048fe806de6b3aeab0586e497ebd22a0bab56e427"}, + {file = "python_snappy-0.6.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:463fd340a499d47b26ca42d2f36a639188738f6e2098c6dbf80aef0e60f461e1"}, + {file = "python_snappy-0.6.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9837ac1650cc68d22a3cf5f15fb62c6964747d16cecc8b22431f113d6e39555d"}, + {file = "python_snappy-0.6.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e973e637112391f05581f427659c05b30b6843bc522a65be35ac7b18ce3dedd"}, + {file = "python_snappy-0.6.1-cp36-cp36m-win32.whl", hash = "sha256:c20498bd712b6e31a4402e1d027a1cd64f6a4a0066a3fe3c7344475886d07fdf"}, + {file = "python_snappy-0.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59e975be4206cc54d0a112ef72fa3970a57c2b1bcc2c97ed41d6df0ebe518228"}, + {file = "python_snappy-0.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2a7e528ab6e09c0d67dcb61a1730a292683e5ff9bb088950638d3170cf2a0a54"}, + {file = "python_snappy-0.6.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39692bedbe0b717001a99915ac0eb2d9d0bad546440d392a2042b96d813eede1"}, + {file = "python_snappy-0.6.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6a7620404da966f637b9ce8d4d3d543d363223f7a12452a575189c5355fc2d25"}, + {file = "python_snappy-0.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7778c224efc38a40d274da4eb82a04cac27aae20012372a7db3c4bbd8926c4d4"}, + {file = "python_snappy-0.6.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d029f7051ec1bbeaa3e03030b6d8ed47ceb69cae9016f493c802a08af54e026"}, + {file = "python_snappy-0.6.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ad38bc98d0b0497a0b0dbc29409bcabfcecff4511ed7063403c86de16927bc"}, + {file = "python_snappy-0.6.1-cp37-cp37m-win32.whl", hash = "sha256:5a453c45178d7864c1bdd6bfe0ee3ed2883f63b9ba2c9bb967c6b586bf763f96"}, + {file = "python_snappy-0.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9f0c0d88b84259f93c3aa46398680646f2c23e43394779758d9f739c34e15295"}, + {file = "python_snappy-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb05c28298803a74add08ba496879242ef159c75bc86a5406fac0ffc7dd021b"}, + {file = "python_snappy-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9eac51307c6a1a38d5f86ebabc26a889fddf20cbba7a116ccb54ba1446601d5b"}, + {file = "python_snappy-0.6.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:88b6ea78b83d2796f330b0af1b70cdd3965dbdab02d8ac293260ec2c8fe340ee"}, + {file = "python_snappy-0.6.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c07220408d3268e8268c9351c5c08041bc6f8c6172e59d398b71020df108541"}, + {file = "python_snappy-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4038019b1bcaadde726a57430718394076c5a21545ebc5badad2c045a09546cf"}, + {file = "python_snappy-0.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc96668d9c7cc656609764275c5f8da58ef56d89bdd6810f6923d36497468ff7"}, + {file = "python_snappy-0.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf5bb9254e1c38aacf253d510d3d9be631bba21f3d068b17672b38b5cbf2fff5"}, + {file = "python_snappy-0.6.1-cp38-cp38-win32.whl", hash = "sha256:eaf905a580f2747c4a474040a5063cd5e0cc3d1d2d6edb65f28196186493ad4a"}, + {file = "python_snappy-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:546c1a7470ecbf6239101e9aff0f709b68ca0f0268b34d9023019a55baa1f7c6"}, + {file = "python_snappy-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e3a013895c64352b49d0d8e107a84f99631b16dbab156ded33ebf0becf56c8b2"}, + {file = "python_snappy-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb9a88a4dd6336488f3de67ce75816d0d796dce53c2c6e4d70e0b565633c7fd"}, + {file = "python_snappy-0.6.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:735cd4528c55dbe4516d6d2b403331a99fc304f8feded8ae887cf97b67d589bb"}, + {file = "python_snappy-0.6.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:90b0186516b7a101c14764b0c25931b741fb0102f21253eff67847b4742dfc72"}, + {file = "python_snappy-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a993dc8aadd901915a510fe6af5f20ae4256f527040066c22a154db8946751f"}, + {file = "python_snappy-0.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:530bfb9efebcc1aab8bb4ebcbd92b54477eed11f6cf499355e882970a6d3aa7d"}, + {file = "python_snappy-0.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5843feb914796b1f0405ccf31ea0fb51034ceb65a7588edfd5a8250cb369e3b2"}, + {file = "python_snappy-0.6.1-cp39-cp39-win32.whl", hash = "sha256:66c80e9b366012dbee262bb1869e4fc5ba8786cda85928481528bc4a72ec2ee8"}, + {file = "python_snappy-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:4d3cafdf454354a621c8ab7408e45aa4e9d5c0b943b61ff4815f71ca6bdf0130"}, + {file = "python_snappy-0.6.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:586724a0276d7a6083a17259d0b51622e492289a9998848a1b01b6441ca12b2f"}, + {file = "python_snappy-0.6.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2be4f4550acd484912441f5f1209ba611ac399aac9355fee73611b9a0d4f949c"}, + {file = "python_snappy-0.6.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdb6942180660bda7f7d01f4c0def3cfc72b1c6d99aad964801775a3e379aba"}, + {file = "python_snappy-0.6.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:03bb511380fca2a13325b6f16fe8234c8e12da9660f0258cd45d9a02ffc916af"}, +] pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] pyyaml-env-tag = [ {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, @@ -1886,19 +1992,20 @@ radon = [ {file = "radon-5.1.0.tar.gz", hash = "sha256:cb1d8752e5f862fb9e20d82b5f758cbc4fb1237c92c9a66450ea0ea7bf29aeee"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] retry = [ {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, ] "ruamel.yaml" = [ - {file = "ruamel.yaml-0.17.17-py3-none-any.whl", hash = "sha256:9af3ec5d7f8065582f3aa841305465025d0afd26c5fb54e15b964e11838fc74f"}, - {file = "ruamel.yaml-0.17.17.tar.gz", hash = "sha256:9751de4cbb57d4bfbf8fc394e125ed4a2f170fbff3dc3d78abf50be85924f8be"}, + {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, + {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, ] "ruamel.yaml.clib" = [ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:066f886bc90cc2ce44df8b5f7acfc6a7e2b2e672713f027136464492b0c34d7c"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, @@ -1908,25 +2015,29 @@ retry = [ {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d3c620a54748a3d4cf0bcfe623e388407c8e85a4b06b8188e126302bcab93ea8"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:210c8fcfeff90514b7133010bf14e3bad652c8efde6b20e00c43854bf94fa5a6"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:61bc5e5ca632d95925907c569daa559ea194a4d16084ba86084be98ab1cec1c6"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1b4139a6ffbca8ef60fdaf9b33dec05143ba746a6f0ae0f9d11d38239211d335"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] s3transfer = [ - {file = "s3transfer-0.5.0-py3-none-any.whl", hash = "sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803"}, - {file = "s3transfer-0.5.0.tar.gz", hash = "sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c"}, + {file = "s3transfer-0.5.2-py3-none-any.whl", hash = "sha256:7a6f4c4d1fdb9a2b640244008e142cbc2cd3ae34b386584ef044dd0f27101971"}, + {file = "s3transfer-0.5.2.tar.gz", hash = "sha256:95c58c194ce657a5f4fb0b9e60a84968c808888aed628cd98ab8771fe1db98ed"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1940,145 +2051,150 @@ stevedore = [ {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, ] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] tomli = [ - {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, - {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] types-requests = [ - {file = "types-requests-2.28.7.tar.gz", hash = "sha256:36385618d4bd2ee3211d4d2e78b44f067ceb5984865c0f253f3c9ecb964526cf"}, - {file = "types_requests-2.28.7-py3-none-any.whl", hash = "sha256:38015d310d13cf7d4d712d2507178349e13fd5dab85259dab7d9a9884c2c9c2a"}, + {file = "types-requests-2.28.9.tar.gz", hash = "sha256:feaf581bd580497a47fe845d506fa3b91b484cf706ff27774e87659837de9962"}, + {file = "types_requests-2.28.9-py3-none-any.whl", hash = "sha256:86cb66d3de2f53eac5c09adc42cf6547eefbd0c7e1210beca1ee751c35d96083"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.17.tar.gz", hash = "sha256:73fd274524c3fc7cd8cd9ceb0cb67ed99b45f9cb2831013e46d50c1451044800"}, - {file = "types_urllib3-1.26.17-py3-none-any.whl", hash = "sha256:0d027fcd27dbb3cb532453b4d977e05bc1e13aefd70519866af211b3003d895d"}, + {file = "types-urllib3-1.26.23.tar.gz", hash = "sha256:b78e819f0e350221d0689a5666162e467ba3910737bafda14b5c2c85e9bb1e56"}, + {file = "types_urllib3-1.26.23-py3-none-any.whl", hash = "sha256:333e675b188a1c1fd980b4b352f9e40572413a4c1ac689c23cd546e96310070a"}, ] typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] watchdog = [ - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, - {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, - {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, - {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, - {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, - {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, - {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, - {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, - {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, - {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, - {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, - {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, + {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, + {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, + {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"}, + {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"}, + {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"}, + {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"}, + {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"}, + {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"}, + {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"}, + {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"}, + {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"}, + {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"}, + {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"}, + {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"}, + {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"}, + {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"}, + {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"}, + {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, + {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, ] wrapt = [ - {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, - {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, - {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, - {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, - {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, - {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, - {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, - {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, - {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, - {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, - {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, - {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, - {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, - {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, - {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, - {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, - {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, - {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, - {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, - {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, - {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, - {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, - {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, - {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, - {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, - {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, - {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, - {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, - {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, - {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, - {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, - {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] xenon = [ {file = "xenon-0.9.0-py2.py3-none-any.whl", hash = "sha256:994c80c7f1c6d40596b600b93734d85a5739208f31895ef99f1e4d362caf9e35"}, {file = "xenon-0.9.0.tar.gz", hash = "sha256:d2b9cb6c6260f771a432c1e588e51fddb17858f88f73ef641e7532f7a5f58fb8"}, ] zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, ] diff --git a/pyproject.toml b/pyproject.toml index 1b04b223f00..aa6bfec1c1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,8 +33,6 @@ email-validator = {version = "*", optional = true } coverage = {extras = ["toml"], version = "^6.2"} pytest = "^7.0.1" black = "^21.12b0" -flake8 = "^4.0.1" -flake8-black = "^0.2.3" flake8-builtins = "^1.5.3" flake8-comprehensions = "^3.7.0" flake8-debugger = "^4.0.0" @@ -50,24 +48,29 @@ bandit = "^1.7.1" radon = "^5.1.0" xenon = "^0.9.0" flake8-eradicate = "^1.2.1" -flake8-bugbear = "^22.7.1" +flake8-bugbear = "^22.8.23" mkdocs-git-revision-date-plugin = "^0.3.2" mike = "^0.6.0" mypy = "^0.971" -mkdocs-material = "^8.2.7" -mypy-boto3-secretsmanager = "^1.24.11" -mypy-boto3-ssm = "^1.24.0" -mypy-boto3-appconfig = "^1.24.29" -mypy-boto3-dynamodb = "^1.24.27" retry = "^0.9.2" pytest-xdist = "^2.5.0" aws-cdk-lib = "^2.23.0" pytest-benchmark = "^3.4.1" -mypy-boto3-cloudwatch = "^1.24.35" -mypy-boto3-lambda = "^1.24.0" -mypy-boto3-xray = "^1.24.0" -types-requests = "^2.28.7" +mypy-boto3-appconfig = { version = "^1.24.29", python = ">=3.7" } +mypy-boto3-cloudformation = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-cloudwatch = { version = "^1.24.35", python = ">=3.7" } +mypy-boto3-dynamodb = { version = "^1.24.27", python = ">=3.7" } +mypy-boto3-lambda = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-logs = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-secretsmanager = { version = "^1.24.11", python = ">=3.7" } +mypy-boto3-ssm = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-s3 = { version = "^1.24.0", python = ">=3.7" } +mypy-boto3-xray = { version = "^1.24.0", python = ">=3.7" } +types-requests = "^2.28.8" typing-extensions = { version = "^4.3.0", python = ">=3.7" } +python-snappy = "^0.6.1" +mkdocs-material = { version = "^8.3.9", python = ">=3.7" } +filelock = { version = "^3.8.0", python = ">=3.7" } [tool.poetry.extras] pydantic = ["pydantic", "email-validator"] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 4be6a26c6a6..ac55d373e63 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,65 +1,35 @@ -import datetime -import sys -import uuid -from dataclasses import dataclass - -import boto3 - -# We only need typing_extensions for python versions <3.8 -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - -from typing import Dict, Generator, Optional - import pytest -from e2e.utils import helpers, infrastructure - - -class LambdaConfig(TypedDict): - parameters: dict - environment_variables: Dict[str, str] - - -@dataclass -class InfrastructureOutput: - arns: Dict[str, str] - execution_time: datetime.datetime - - def get_lambda_arns(self) -> Dict[str, str]: - return self.arns - - def get_lambda_function_arn(self, cf_output_name: str) -> Optional[str]: - return self.arns.get(cf_output_name) - - def get_lambda_function_name(self, cf_output_name: str) -> Optional[str]: - lambda_arn = self.get_lambda_function_arn(cf_output_name=cf_output_name) - return lambda_arn.split(":")[-1] if lambda_arn else None - - def get_lambda_execution_time(self) -> datetime.datetime: - return self.execution_time - - def get_lambda_execution_time_timestamp(self) -> int: - return int(self.execution_time.timestamp() * 1000) - - -@pytest.fixture(scope="module") -def create_infrastructure(config, request) -> Generator[Dict[str, str], None, None]: - stack_name = f"test-lambda-{uuid.uuid4()}" - test_dir = request.fspath.dirname - handlers_dir = f"{test_dir}/handlers/" - - infra = infrastructure.Infrastructure(stack_name=stack_name, handlers_dir=handlers_dir, config=config) - yield infra.deploy(Stack=infrastructure.InfrastructureStack) - infra.delete() - -@pytest.fixture(scope="module") -def execute_lambda(create_infrastructure) -> InfrastructureOutput: - execution_time = datetime.datetime.utcnow() - session = boto3.Session() - client = session.client("lambda") - for _, arn in create_infrastructure.items(): - helpers.trigger_lambda(lambda_arn=arn, client=client) - return InfrastructureOutput(arns=create_infrastructure, execution_time=execution_time) +from tests.e2e.utils.infrastructure import LambdaLayerStack, deploy_once + + +@pytest.fixture(scope="session") +def lambda_layer_arn(lambda_layer_deployment): + yield lambda_layer_deployment.get("LayerArn") + + +@pytest.fixture(scope="session") +def lambda_layer_deployment(request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory, worker_id: str): + """Setup and teardown logic for E2E test infrastructure + + Parameters + ---------- + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + tmp_path_factory : pytest.TempPathFactory + pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up + worker_id : str + pytest-xdist worker identification to detect whether parallelization is enabled + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + yield from deploy_once( + stack=LambdaLayerStack, + request=request, + tmp_path_factory=tmp_path_factory, + worker_id=worker_id, + layer_arn="", + ) diff --git a/tests/e2e/logger/conftest.py b/tests/e2e/logger/conftest.py new file mode 100644 index 00000000000..82a89314258 --- /dev/null +++ b/tests/e2e/logger/conftest.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import pytest + +from tests.e2e.logger.infrastructure import LoggerStack + + +@pytest.fixture(autouse=True, scope="module") +def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str): + """Setup and teardown logic for E2E test infrastructure + + Parameters + ---------- + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + lambda_layer_arn : str + Lambda Layer ARN + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + stack = LoggerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn) + try: + yield stack.deploy() + finally: + stack.delete() diff --git a/tests/e2e/logger/handlers/basic_handler.py b/tests/e2e/logger/handlers/basic_handler.py index 34d7fb4678a..0f0dd46b4aa 100644 --- a/tests/e2e/logger/handlers/basic_handler.py +++ b/tests/e2e/logger/handlers/basic_handler.py @@ -1,17 +1,11 @@ -import os - from aws_lambda_powertools import Logger logger = Logger() -MESSAGE = os.environ["MESSAGE"] -ADDITIONAL_KEY = os.environ["ADDITIONAL_KEY"] - -@logger.inject_lambda_context(log_event=True) +@logger.inject_lambda_context def lambda_handler(event, context): - logger.debug(MESSAGE) - logger.info(MESSAGE) - logger.append_keys(**{ADDITIONAL_KEY: "test"}) - logger.info(MESSAGE) + message, append_keys = event.get("message", ""), event.get("append_keys", {}) + logger.append_keys(**append_keys) + logger.info(message) return "success" diff --git a/tests/e2e/logger/handlers/no_context_handler.py b/tests/e2e/logger/handlers/no_context_handler.py deleted file mode 100644 index 1347ba98d81..00000000000 --- a/tests/e2e/logger/handlers/no_context_handler.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -from aws_lambda_powertools import Logger - -logger = Logger() - -MESSAGE = os.environ["MESSAGE"] -ADDITIONAL_KEY = os.environ["ADDITIONAL_KEY"] - - -def lambda_handler(event, context): - logger.info(MESSAGE) - logger.append_keys(**{ADDITIONAL_KEY: "test"}) - return "success" diff --git a/tests/e2e/logger/infrastructure.py b/tests/e2e/logger/infrastructure.py new file mode 100644 index 00000000000..68aaa8eb38a --- /dev/null +++ b/tests/e2e/logger/infrastructure.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from tests.e2e.utils.infrastructure import BaseInfrastructure + + +class LoggerStack(BaseInfrastructure): + FEATURE_NAME = "logger" + + def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None: + super().__init__(feature_name, handlers_dir, layer_arn) + + def create_resources(self): + self.create_lambda_functions() diff --git a/tests/e2e/logger/test_logger.py b/tests/e2e/logger/test_logger.py index ea27b93740b..e5c27dd0a8f 100644 --- a/tests/e2e/logger/test_logger.py +++ b/tests/e2e/logger/test_logger.py @@ -1,142 +1,37 @@ -import boto3 -import pytest -from e2e import conftest -from e2e.utils import helpers - - -@pytest.fixture(scope="module") -def config() -> conftest.LambdaConfig: - return { - "parameters": {}, - "environment_variables": { - "MESSAGE": "logger message test", - "LOG_LEVEL": "INFO", - "ADDITIONAL_KEY": "extra_info", - }, - } - - -def test_basic_lambda_logs_visible(execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig): - # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) - - # THEN - assert any( - log.message == config["environment_variables"]["MESSAGE"] - and log.level == config["environment_variables"]["LOG_LEVEL"] - for log in filtered_logs - ) - - -def test_basic_lambda_no_debug_logs_visible( - execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig -): - # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) - - # THEN - assert not any( - log.message == config["environment_variables"]["MESSAGE"] and log.level == "DEBUG" for log in filtered_logs - ) - - -def test_basic_lambda_contextual_data_logged(execute_lambda: conftest.InfrastructureOutput): - # GIVEN - required_keys = ( - "xray_trace_id", - "function_request_id", - "function_arn", - "function_memory_size", - "function_name", - "cold_start", - ) - - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) - - # THEN - assert all(keys in logs.dict(exclude_unset=True) for logs in filtered_logs for keys in required_keys) - - -def test_basic_lambda_additional_key_persistence_basic_lambda( - execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig -): - # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) - - # THEN - assert any( - log.extra_info - and log.message == config["environment_variables"]["MESSAGE"] - and log.level == config["environment_variables"]["LOG_LEVEL"] - for log in filtered_logs - ) +import json +from uuid import uuid4 +import pytest -def test_basic_lambda_empty_event_logged(execute_lambda: conftest.InfrastructureOutput): +from aws_lambda_powertools.shared.constants import LOGGER_LAMBDA_CONTEXT_KEYS +from tests.e2e.utils import data_fetcher - # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) +@pytest.fixture +def basic_handler_fn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandler", "") - # THEN - assert any(log.message == {} for log in filtered_logs) +@pytest.fixture +def basic_handler_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandlerArn", "") -def test_no_context_lambda_contextual_data_not_logged(execute_lambda: conftest.InfrastructureOutput): +def test_basic_lambda_logs_visible(basic_handler_fn, basic_handler_fn_arn): # GIVEN - required_missing_keys = ( - "function_request_id", - "function_arn", - "function_memory_size", - "function_name", - "cold_start", - ) - - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="nocontexthandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") + message = "logs should be visible with default settings" + custom_key = "order_id" + additional_keys = {custom_key: f"{uuid4()}"} + payload = json.dumps({"message": message, "append_keys": additional_keys}) # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) + _, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload) + data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload) # THEN - assert not any(keys in logs.dict(exclude_unset=True) for logs in filtered_logs for keys in required_missing_keys) - - -def test_no_context_lambda_event_not_logged(execute_lambda: conftest.InfrastructureOutput): + logs = data_fetcher.get_logs(function_name=basic_handler_fn, start_time=execution_time) - # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="nocontexthandlerarn") - timestamp = execute_lambda.get_lambda_execution_time_timestamp() - cw_client = boto3.client("logs") - - # WHEN - filtered_logs = helpers.get_logs(lambda_function_name=lambda_name, start_time=timestamp, log_client=cw_client) - - # THEN - assert not any(log.message == {} for log in filtered_logs) + assert len(logs) == 2 + assert len(logs.get_cold_start_log()) == 1 + assert len(logs.get_log(key=custom_key)) == 2 + assert logs.have_keys(*LOGGER_LAMBDA_CONTEXT_KEYS) is True diff --git a/tests/e2e/metrics/conftest.py b/tests/e2e/metrics/conftest.py new file mode 100644 index 00000000000..663c8845be4 --- /dev/null +++ b/tests/e2e/metrics/conftest.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import pytest + +from tests.e2e.metrics.infrastructure import MetricsStack + + +@pytest.fixture(autouse=True, scope="module") +def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str): + """Setup and teardown logic for E2E test infrastructure + + Parameters + ---------- + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + lambda_layer_arn : str + Lambda Layer ARN + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + stack = MetricsStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn) + try: + yield stack.deploy() + finally: + stack.delete() diff --git a/tests/e2e/metrics/handlers/basic_handler.py b/tests/e2e/metrics/handlers/basic_handler.py index dd2f486d980..ef5e079e604 100644 --- a/tests/e2e/metrics/handlers/basic_handler.py +++ b/tests/e2e/metrics/handlers/basic_handler.py @@ -1,14 +1,17 @@ -import os - from aws_lambda_powertools import Metrics -from aws_lambda_powertools.metrics import MetricUnit - -METRIC_NAME = os.environ["METRIC_NAME"] -metrics = Metrics() +my_metrics = Metrics() -@metrics.log_metrics +@my_metrics.log_metrics def lambda_handler(event, context): - metrics.add_metric(name=METRIC_NAME, unit=MetricUnit.Count, value=1) + metrics, namespace, service = event.get("metrics"), event.get("namespace"), event.get("service") + + # Maintenance: create a public method to set these explicitly + my_metrics.namespace = namespace + my_metrics.service = service + + for metric in metrics: + my_metrics.add_metric(**metric) + return "success" diff --git a/tests/e2e/metrics/handlers/cold_start.py b/tests/e2e/metrics/handlers/cold_start.py new file mode 100644 index 00000000000..20f2ad16f85 --- /dev/null +++ b/tests/e2e/metrics/handlers/cold_start.py @@ -0,0 +1,12 @@ +from aws_lambda_powertools import Metrics + +my_metrics = Metrics() + + +@my_metrics.log_metrics(capture_cold_start_metric=True) +def lambda_handler(event, context): + # Maintenance: create a public method to set these explicitly + my_metrics.namespace = event.get("namespace") + my_metrics.service = event.get("service") + + return "success" diff --git a/tests/e2e/metrics/infrastructure.py b/tests/e2e/metrics/infrastructure.py new file mode 100644 index 00000000000..9afa59bb5cd --- /dev/null +++ b/tests/e2e/metrics/infrastructure.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from tests.e2e.utils.infrastructure import BaseInfrastructure + + +class MetricsStack(BaseInfrastructure): + FEATURE_NAME = "metrics" + + def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None: + super().__init__(feature_name, handlers_dir, layer_arn) + + def create_resources(self): + self.create_lambda_functions() diff --git a/tests/e2e/metrics/test_metrics.py b/tests/e2e/metrics/test_metrics.py index 7d3aa7efa61..516f93ac1f0 100644 --- a/tests/e2e/metrics/test_metrics.py +++ b/tests/e2e/metrics/test_metrics.py @@ -1,40 +1,67 @@ -import datetime -import uuid +import json -import boto3 import pytest -from e2e import conftest -from e2e.utils import helpers +from tests.e2e.utils import data_builder, data_fetcher -@pytest.fixture(scope="module") -def config() -> conftest.LambdaConfig: - return { - "parameters": {}, - "environment_variables": { - "POWERTOOLS_METRICS_NAMESPACE": "powertools-e2e-metric", - "POWERTOOLS_SERVICE_NAME": "test-powertools-service", - "METRIC_NAME": f"business-metric-{str(uuid.uuid4()).replace('-','_')}", - }, - } +@pytest.fixture +def basic_handler_fn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandler", "") -def test_basic_lambda_metric_visible(execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig): + +@pytest.fixture +def basic_handler_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandlerArn", "") + + +@pytest.fixture +def cold_start_fn(infrastructure: dict) -> str: + return infrastructure.get("ColdStart", "") + + +@pytest.fixture +def cold_start_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("ColdStartArn", "") + + +METRIC_NAMESPACE = "powertools-e2e-metric" + + +def test_basic_lambda_metric_is_visible(basic_handler_fn: str, basic_handler_fn_arn: str): # GIVEN - start_date = execute_lambda.get_lambda_execution_time() - end_date = start_date + datetime.timedelta(minutes=5) + metric_name = data_builder.build_metric_name() + service = data_builder.build_service_name() + dimensions = data_builder.build_add_dimensions_input(service=service) + metrics = data_builder.build_multiple_add_metric_input(metric_name=metric_name, value=1, quantity=3) # WHEN - metrics = helpers.get_metrics( - start_date=start_date, - end_date=end_date, - namespace=config["environment_variables"]["POWERTOOLS_METRICS_NAMESPACE"], - metric_name=config["environment_variables"]["METRIC_NAME"], - service_name=config["environment_variables"]["POWERTOOLS_SERVICE_NAME"], - cw_client=boto3.client(service_name="cloudwatch"), + event = json.dumps({"metrics": metrics, "service": service, "namespace": METRIC_NAMESPACE}) + _, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=event) + + metric_values = data_fetcher.get_metrics( + namespace=METRIC_NAMESPACE, start_date=execution_time, metric_name=metric_name, dimensions=dimensions + ) + + # THEN + assert metric_values == [3.0] + + +def test_cold_start_metric(cold_start_fn_arn: str, cold_start_fn: str): + # GIVEN + metric_name = "ColdStart" + service = data_builder.build_service_name() + dimensions = data_builder.build_add_dimensions_input(function_name=cold_start_fn, service=service) + + # WHEN we invoke twice + event = json.dumps({"service": service, "namespace": METRIC_NAMESPACE}) + + _, execution_time = data_fetcher.get_lambda_response(lambda_arn=cold_start_fn_arn, payload=event) + data_fetcher.get_lambda_response(lambda_arn=cold_start_fn_arn, payload=event) + + metric_values = data_fetcher.get_metrics( + namespace=METRIC_NAMESPACE, start_date=execution_time, metric_name=metric_name, dimensions=dimensions ) # THEN - assert metrics.get("Timestamps") and len(metrics.get("Timestamps")) == 1 - assert metrics.get("Values") and len(metrics.get("Values")) == 1 - assert metrics.get("Values") and metrics.get("Values")[0] == 1 + assert metric_values == [1.0] diff --git a/tests/e2e/tracer/conftest.py b/tests/e2e/tracer/conftest.py new file mode 100644 index 00000000000..3b724bf1247 --- /dev/null +++ b/tests/e2e/tracer/conftest.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import pytest + +from tests.e2e.tracer.infrastructure import TracerStack + + +@pytest.fixture(autouse=True, scope="module") +def infrastructure(request: pytest.FixtureRequest, lambda_layer_arn: str): + """Setup and teardown logic for E2E test infrastructure + + Parameters + ---------- + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + lambda_layer_arn : str + Lambda Layer ARN + + Yields + ------ + Dict[str, str] + CloudFormation Outputs from deployed infrastructure + """ + stack = TracerStack(handlers_dir=Path(f"{request.path.parent}/handlers"), layer_arn=lambda_layer_arn) + try: + yield stack.deploy() + finally: + stack.delete() diff --git a/tests/e2e/tracer/handlers/async_capture.py b/tests/e2e/tracer/handlers/async_capture.py new file mode 100644 index 00000000000..b19840a6f69 --- /dev/null +++ b/tests/e2e/tracer/handlers/async_capture.py @@ -0,0 +1,16 @@ +import asyncio +from uuid import uuid4 + +from aws_lambda_powertools import Tracer +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() + + +@tracer.capture_method +async def async_get_users(): + return [{"id": f"{uuid4()}"} for _ in range(5)] + + +def lambda_handler(event: dict, context: LambdaContext): + return asyncio.run(async_get_users()) diff --git a/tests/e2e/tracer/handlers/basic_handler.py b/tests/e2e/tracer/handlers/basic_handler.py index d074b30796f..ba94c845ace 100644 --- a/tests/e2e/tracer/handlers/basic_handler.py +++ b/tests/e2e/tracer/handlers/basic_handler.py @@ -1,25 +1,16 @@ -import asyncio -import os +from uuid import uuid4 from aws_lambda_powertools import Tracer from aws_lambda_powertools.utilities.typing import LambdaContext -tracer = Tracer(service="e2e-tests-app") +tracer = Tracer() -ANNOTATION_KEY = os.environ["ANNOTATION_KEY"] -ANNOTATION_VALUE = os.environ["ANNOTATION_VALUE"] -ANNOTATION_ASYNC_VALUE = os.environ["ANNOTATION_ASYNC_VALUE"] + +@tracer.capture_method +def get_todos(): + return [{"id": f"{uuid4()}", "completed": False} for _ in range(5)] @tracer.capture_lambda_handler def lambda_handler(event: dict, context: LambdaContext): - tracer.put_annotation(key=ANNOTATION_KEY, value=ANNOTATION_VALUE) - tracer.put_metadata(key=ANNOTATION_KEY, value=ANNOTATION_VALUE) - return asyncio.run(collect_payment()) - - -@tracer.capture_method -async def collect_payment() -> str: - tracer.put_annotation(key=ANNOTATION_KEY, value=ANNOTATION_ASYNC_VALUE) - tracer.put_metadata(key=ANNOTATION_KEY, value=ANNOTATION_ASYNC_VALUE) - return "success" + return get_todos() diff --git a/tests/e2e/tracer/infrastructure.py b/tests/e2e/tracer/infrastructure.py new file mode 100644 index 00000000000..9b388558c0b --- /dev/null +++ b/tests/e2e/tracer/infrastructure.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from tests.e2e.utils.data_builder import build_service_name +from tests.e2e.utils.infrastructure import BaseInfrastructure + + +class TracerStack(BaseInfrastructure): + # Maintenance: Tracer doesn't support dynamic service injection (tracer.py L310) + # we could move after handler response or adopt env vars usage in e2e tests + SERVICE_NAME: str = build_service_name() + FEATURE_NAME = "tracer" + + def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None: + super().__init__(feature_name, handlers_dir, layer_arn) + + def create_resources(self) -> None: + env_vars = {"POWERTOOLS_SERVICE_NAME": self.SERVICE_NAME} + self.create_lambda_functions(function_props={"environment": env_vars}) diff --git a/tests/e2e/tracer/test_tracer.py b/tests/e2e/tracer/test_tracer.py index c2af4386749..06dde811ef1 100644 --- a/tests/e2e/tracer/test_tracer.py +++ b/tests/e2e/tracer/test_tracer.py @@ -1,51 +1,69 @@ -import datetime -import uuid - -import boto3 import pytest -from e2e import conftest -from e2e.utils import helpers + +from tests.e2e.tracer.handlers import async_capture, basic_handler +from tests.e2e.tracer.infrastructure import TracerStack +from tests.e2e.utils import data_builder, data_fetcher + + +@pytest.fixture +def basic_handler_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandlerArn", "") + + +@pytest.fixture +def basic_handler_fn(infrastructure: dict) -> str: + return infrastructure.get("BasicHandler", "") + + +@pytest.fixture +def async_fn_arn(infrastructure: dict) -> str: + return infrastructure.get("AsyncCaptureArn", "") + + +@pytest.fixture +def async_fn(infrastructure: dict) -> str: + return infrastructure.get("AsyncCapture", "") -@pytest.fixture(scope="module") -def config() -> conftest.LambdaConfig: - return { - "parameters": {"tracing": "ACTIVE"}, - "environment_variables": { - "ANNOTATION_KEY": f"e2e-tracer-{str(uuid.uuid4()).replace('-','_')}", - "ANNOTATION_VALUE": "stored", - "ANNOTATION_ASYNC_VALUE": "payments", - }, - } +def test_lambda_handler_trace_is_visible(basic_handler_fn_arn: str, basic_handler_fn: str): + # GIVEN + handler_name = basic_handler.lambda_handler.__name__ + handler_subsegment = f"## {handler_name}" + handler_metadata_key = f"{handler_name} response" + + method_name = basic_handler.get_todos.__name__ + method_subsegment = f"## {method_name}" + handler_metadata_key = f"{method_name} response" + + trace_query = data_builder.build_trace_default_query(function_name=basic_handler_fn) + + # WHEN + _, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn) + data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn) + + # THEN + trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query, minimum_traces=2) + + assert len(trace.get_annotation(key="ColdStart", value=True)) == 1 + assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2 + assert len(trace.get_metadata(key=handler_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 2 + assert len(trace.get_subsegment(name=handler_subsegment)) == 2 + assert len(trace.get_subsegment(name=method_subsegment)) == 2 -def test_basic_lambda_async_trace_visible(execute_lambda: conftest.InfrastructureOutput, config: conftest.LambdaConfig): +def test_async_trace_is_visible(async_fn_arn: str, async_fn: str): # GIVEN - lambda_name = execute_lambda.get_lambda_function_name(cf_output_name="basichandlerarn") - start_date = execute_lambda.get_lambda_execution_time() - end_date = start_date + datetime.timedelta(minutes=5) - trace_filter_exporession = f'service("{lambda_name}")' + async_fn_name = async_capture.async_get_users.__name__ + async_fn_name_subsegment = f"## {async_fn_name}" + async_fn_name_metadata_key = f"{async_fn_name} response" + + trace_query = data_builder.build_trace_default_query(function_name=async_fn) # WHEN - trace = helpers.get_traces( - start_date=start_date, - end_date=end_date, - filter_expression=trace_filter_exporession, - xray_client=boto3.client("xray"), - ) + _, execution_time = data_fetcher.get_lambda_response(lambda_arn=async_fn_arn) # THEN - info = helpers.find_trace_additional_info(trace=trace) - print(info) - handler_trace_segment = [trace_segment for trace_segment in info if trace_segment.name == "## lambda_handler"][0] - collect_payment_trace_segment = [ - trace_segment for trace_segment in info if trace_segment.name == "## collect_payment" - ][0] - - annotation_key = config["environment_variables"]["ANNOTATION_KEY"] - expected_value = config["environment_variables"]["ANNOTATION_VALUE"] - expected_async_value = config["environment_variables"]["ANNOTATION_ASYNC_VALUE"] - - assert handler_trace_segment.annotations["Service"] == "e2e-tests-app" - assert handler_trace_segment.metadata["e2e-tests-app"][annotation_key] == expected_value - assert collect_payment_trace_segment.metadata["e2e-tests-app"][annotation_key] == expected_async_value + trace = data_fetcher.get_traces(start_date=execution_time, filter_expression=trace_query) + + assert len(trace.get_subsegment(name=async_fn_name_subsegment)) == 1 + assert len(trace.get_metadata(key=async_fn_name_metadata_key, namespace=TracerStack.SERVICE_NAME)) == 1 diff --git a/tests/e2e/utils/Dockerfile b/tests/e2e/utils/Dockerfile index eccfe2c6dfd..586847bb3fa 100644 --- a/tests/e2e/utils/Dockerfile +++ b/tests/e2e/utils/Dockerfile @@ -1,5 +1,5 @@ -# Image used by CDK's LayerVersion construct to create Lambda Layer with Powertools -# library code. +# Image used by CDK's LayerVersion construct to create Lambda Layer with Powertools +# library code. # The correct AWS SAM build image based on the runtime of the function will be # passed as build arg. The default allows to do `docker build .` when testing. ARG IMAGE=public.ecr.aws/sam/build-python3.7 @@ -9,8 +9,6 @@ ARG PIP_INDEX_URL ARG PIP_EXTRA_INDEX_URL ARG HTTPS_PROXY -# Upgrade pip (required by cryptography v3.4 and above, which is a dependency of poetry) RUN pip install --upgrade pip -RUN pip install pipenv poetry CMD [ "python" ] diff --git a/tests/e2e/utils/asset.py b/tests/e2e/utils/asset.py new file mode 100644 index 00000000000..db9e7299d1a --- /dev/null +++ b/tests/e2e/utils/asset.py @@ -0,0 +1,147 @@ +import io +import json +import logging +import zipfile +from pathlib import Path +from typing import Dict, List, Optional + +import boto3 +import botocore.exceptions +from mypy_boto3_s3 import S3Client +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class AssetManifest(BaseModel): + path: str + packaging: str + + +class AssetTemplateConfigDestinationsAccount(BaseModel): + bucket_name: str = Field(str, alias="bucketName") + object_key: str = Field(str, alias="objectKey") + assume_role_arn: str = Field(str, alias="assumeRoleArn") + + +class AssetTemplateConfigDestinations(BaseModel): + current_account_current_region: AssetTemplateConfigDestinationsAccount = Field( + AssetTemplateConfigDestinationsAccount, alias="current_account-current_region" + ) + + +class AssetTemplateConfig(BaseModel): + source: AssetManifest + destinations: AssetTemplateConfigDestinations + + +class TemplateAssembly(BaseModel): + version: str + files: Dict[str, AssetTemplateConfig] + + +class Asset: + def __init__( + self, config: AssetTemplateConfig, account_id: str, region: str, boto3_client: Optional[S3Client] = None + ) -> None: + """CDK Asset logic to verify existence and resolve deeply nested configuration + + Parameters + ---------- + config : AssetTemplateConfig + CDK Asset configuration found in synthesized template + account_id : str + AWS Account ID + region : str + AWS Region + boto3_client : Optional["S3Client"], optional + S3 client instance for asset operations, by default None + """ + self.config = config + self.s3 = boto3_client or boto3.client("s3") + self.account_id = account_id + self.region = region + self.asset_path = config.source.path + self.asset_packaging = config.source.packaging + self.object_key = config.destinations.current_account_current_region.object_key + self._bucket = config.destinations.current_account_current_region.bucket_name + self.bucket_name = self._resolve_bucket_name() + + @property + def is_zip(self): + return self.asset_packaging == "zip" + + def exists_in_s3(self, key: str) -> bool: + try: + return self.s3.head_object(Bucket=self.bucket_name, Key=key) is not None + except botocore.exceptions.ClientError: + return False + + def _resolve_bucket_name(self) -> str: + return self._bucket.replace("${AWS::AccountId}", self.account_id).replace("${AWS::Region}", self.region) + + +class Assets: + def __init__( + self, asset_manifest: Path, account_id: str, region: str, boto3_client: Optional[S3Client] = None + ) -> None: + """CDK Assets logic to find each asset, compress, and upload + + Parameters + ---------- + asset_manifest : Path + Asset manifest JSON file (self.__synthesize) + account_id : str + AWS Account ID + region : str + AWS Region + boto3_client : Optional[S3Client], optional + S3 client instance for asset operations, by default None + """ + self.asset_manifest = asset_manifest + self.account_id = account_id + self.region = region + self.s3 = boto3_client or boto3.client("s3") + self.assets = self._find_assets_from_template() + self.assets_location = str(self.asset_manifest.parent) + + def upload(self): + """Drop-in replacement for cdk-assets package s3 upload part. + https://www.npmjs.com/package/cdk-assets. + We use custom solution to avoid dependencies from nodejs ecosystem. + We follow the same design cdk-assets: + https://github.com/aws/aws-cdk-rfcs/blob/master/text/0092-asset-publishing.md. + """ + logger.debug(f"Upload {len(self.assets)} assets") + for asset in self.assets: + if not asset.is_zip: + logger.debug(f"Asset '{asset.object_key}' is not zip. Skipping upload.") + continue + + if asset.exists_in_s3(key=asset.object_key): + logger.debug(f"Asset '{asset.object_key}' already exists in S3. Skipping upload.") + continue + + archive = self._compress_assets(asset) + logger.debug("Uploading archive to S3") + self.s3.upload_fileobj(Fileobj=archive, Bucket=asset.bucket_name, Key=asset.object_key) + logger.debug("Successfully uploaded") + + def _find_assets_from_template(self) -> List[Asset]: + data = json.loads(self.asset_manifest.read_text()) + template = TemplateAssembly(**data) + return [ + Asset(config=asset_config, account_id=self.account_id, region=self.region) + for asset_config in template.files.values() + ] + + def _compress_assets(self, asset: Asset) -> io.BytesIO: + buf = io.BytesIO() + asset_dir = f"{self.assets_location}/{asset.asset_path}" + asset_files = list(Path(asset_dir).rglob("*")) + with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for asset_file in asset_files: + logger.debug(f"Adding file '{asset_file}' to the archive.") + archive.write(asset_file, arcname=asset_file.relative_to(asset_dir)) + buf.seek(0) + return buf diff --git a/tests/e2e/utils/data_builder/__init__.py b/tests/e2e/utils/data_builder/__init__.py new file mode 100644 index 00000000000..72c216faa76 --- /dev/null +++ b/tests/e2e/utils/data_builder/__init__.py @@ -0,0 +1,13 @@ +from tests.e2e.utils.data_builder.common import build_random_value, build_service_name +from tests.e2e.utils.data_builder.metrics import ( + build_add_dimensions_input, + build_add_metric_input, + build_metric_name, + build_metric_query_data, + build_multiple_add_metric_input, +) +from tests.e2e.utils.data_builder.traces import ( + build_put_annotations_input, + build_put_metadata_input, + build_trace_default_query, +) diff --git a/tests/e2e/utils/data_builder/common.py b/tests/e2e/utils/data_builder/common.py new file mode 100644 index 00000000000..f28778ffed3 --- /dev/null +++ b/tests/e2e/utils/data_builder/common.py @@ -0,0 +1,9 @@ +import secrets + + +def build_service_name() -> str: + return f"test_service{build_random_value()}" + + +def build_random_value(nbytes: int = 10) -> str: + return secrets.token_urlsafe(nbytes).replace("-", "") diff --git a/tests/e2e/utils/data_builder/metrics.py b/tests/e2e/utils/data_builder/metrics.py new file mode 100644 index 00000000000..d14f4ae3567 --- /dev/null +++ b/tests/e2e/utils/data_builder/metrics.py @@ -0,0 +1,116 @@ +from typing import Dict, List, Optional + +from mypy_boto3_cloudwatch.type_defs import DimensionTypeDef, MetricDataQueryTypeDef + +from aws_lambda_powertools.metrics import MetricUnit +from tests.e2e.utils.data_builder.common import build_random_value + + +def build_metric_query_data( + namespace: str, + metric_name: str, + period: int = 60, + stat: str = "Sum", + dimensions: Optional[List[DimensionTypeDef]] = None, +) -> List[MetricDataQueryTypeDef]: + """Create input for CloudWatch GetMetricData API call + + Parameters + ---------- + namespace : str + Metric namespace to search for + metric_name : str + Metric name to search for + period : int, optional + Time period in seconds to search metrics, by default 60 + stat : str, optional + Aggregate function to use for results, by default "Sum" + dimensions : Optional[List[DimensionTypeDef]], optional + Metric dimensions to search for, by default None + + Returns + ------- + List[MetricDataQueryTypeDef] + _description_ + """ + dimensions = dimensions or [] + data_query: List[MetricDataQueryTypeDef] = [ + { + "Id": metric_name.lower(), + "MetricStat": { + "Metric": {"Namespace": namespace, "MetricName": metric_name}, + "Period": period, + "Stat": stat, + }, + "ReturnData": True, + } + ] + + if dimensions: + data_query[0]["MetricStat"]["Metric"]["Dimensions"] = dimensions + + return data_query + + +def build_add_metric_input(metric_name: str, value: float, unit: str = MetricUnit.Count.value) -> Dict: + """Create a metric input to be used with Metrics.add_metric() + + Parameters + ---------- + metric_name : str + metric name + value : float + metric value + unit : str, optional + metric unit, by default Count + + Returns + ------- + Dict + Metric input + """ + return {"name": metric_name, "unit": unit, "value": value} + + +def build_multiple_add_metric_input( + metric_name: str, value: float, unit: str = MetricUnit.Count.value, quantity: int = 1 +) -> List[Dict]: + """Create list of metrics input to be used with Metrics.add_metric() + + Parameters + ---------- + metric_name : str + metric name + value : float + metric value + unit : str, optional + metric unit, by default Count + quantity : int, optional + number of metrics to be created, by default 1 + + Returns + ------- + List[Dict] + List of metrics + """ + return [{"name": metric_name, "unit": unit, "value": value} for _ in range(quantity)] + + +def build_add_dimensions_input(**dimensions) -> List[DimensionTypeDef]: + """Create dimensions input to be used with either get_metrics or Metrics.add_dimension() + + Parameters + ---------- + dimensions : str + key=value pair as dimension + + Returns + ------- + List[DimensionTypeDef] + Metric dimension input + """ + return [{"Name": name, "Value": value} for name, value in dimensions.items()] + + +def build_metric_name() -> str: + return f"test_metric{build_random_value()}" diff --git a/tests/e2e/utils/data_builder/traces.py b/tests/e2e/utils/data_builder/traces.py new file mode 100644 index 00000000000..59350c8ff68 --- /dev/null +++ b/tests/e2e/utils/data_builder/traces.py @@ -0,0 +1,39 @@ +from typing import Any, Dict, List, Optional + + +def build_trace_default_query(function_name: str) -> str: + return f'service("{function_name}")' + + +def build_put_annotations_input(**annotations: str) -> List[Dict]: + """Create trace annotations input to be used with Tracer.put_annotation() + + Parameters + ---------- + annotations : str + annotations in key=value form + + Returns + ------- + List[Dict] + List of put annotations input + """ + return [{"key": key, "value": value} for key, value in annotations.items()] + + +def build_put_metadata_input(namespace: Optional[str] = None, **metadata: Any) -> List[Dict]: + """Create trace metadata input to be used with Tracer.put_metadata() + + All metadata will be under `test` namespace + + Parameters + ---------- + metadata : Any + metadata in key=value form + + Returns + ------- + List[Dict] + List of put metadata input + """ + return [{"key": key, "value": value, "namespace": namespace} for key, value in metadata.items()] diff --git a/tests/e2e/utils/data_fetcher/__init__.py b/tests/e2e/utils/data_fetcher/__init__.py new file mode 100644 index 00000000000..43024f9946f --- /dev/null +++ b/tests/e2e/utils/data_fetcher/__init__.py @@ -0,0 +1,4 @@ +from tests.e2e.utils.data_fetcher.common import get_lambda_response +from tests.e2e.utils.data_fetcher.logs import get_logs +from tests.e2e.utils.data_fetcher.metrics import get_metrics +from tests.e2e.utils.data_fetcher.traces import get_traces diff --git a/tests/e2e/utils/data_fetcher/common.py b/tests/e2e/utils/data_fetcher/common.py new file mode 100644 index 00000000000..2de8838dc74 --- /dev/null +++ b/tests/e2e/utils/data_fetcher/common.py @@ -0,0 +1,15 @@ +from datetime import datetime +from typing import Optional, Tuple + +import boto3 +from mypy_boto3_lambda import LambdaClient +from mypy_boto3_lambda.type_defs import InvocationResponseTypeDef + + +def get_lambda_response( + lambda_arn: str, payload: Optional[str] = None, client: Optional[LambdaClient] = None +) -> Tuple[InvocationResponseTypeDef, datetime]: + client = client or boto3.client("lambda") + payload = payload or "" + execution_time = datetime.utcnow() + return client.invoke(FunctionName=lambda_arn, InvocationType="RequestResponse", Payload=payload), execution_time diff --git a/tests/e2e/utils/data_fetcher/logs.py b/tests/e2e/utils/data_fetcher/logs.py new file mode 100644 index 00000000000..a005009f5f5 --- /dev/null +++ b/tests/e2e/utils/data_fetcher/logs.py @@ -0,0 +1,148 @@ +import json +from datetime import datetime +from typing import List, Optional, Union + +import boto3 +from mypy_boto3_logs import CloudWatchLogsClient +from pydantic import BaseModel, Extra +from retry import retry + + +class Log(BaseModel, extra=Extra.allow): + level: str + location: str + message: Union[dict, str] + timestamp: str + service: str + cold_start: Optional[bool] + function_name: Optional[str] + function_memory_size: Optional[str] + function_arn: Optional[str] + function_request_id: Optional[str] + xray_trace_id: Optional[str] + + +class LogFetcher: + def __init__( + self, + function_name: str, + start_time: datetime, + log_client: Optional[CloudWatchLogsClient] = None, + filter_expression: Optional[str] = None, + ): + """Fetch and expose Powertools Logger logs from CloudWatch Logs + + Parameters + ---------- + function_name : str + Name of Lambda function to fetch logs for + start_time : datetime + Start date range to filter traces + log_client : Optional[CloudWatchLogsClient], optional + Amazon CloudWatch Logs Client, by default boto3.client('logs) + filter_expression : Optional[str], optional + CloudWatch Logs Filter Pattern expression, by default "message" + """ + self.function_name = function_name + self.start_time = int(start_time.timestamp()) + self.log_client = log_client or boto3.client("logs") + self.filter_expression = filter_expression or "message" # Logger message key + self.log_group = f"/aws/lambda/{self.function_name}" + self.logs: List[Log] = self._get_logs() + + def get_log(self, key: str, value: Optional[any] = None) -> List[Log]: + """Get logs based on key or key and value + + Parameters + ---------- + key : str + Log key name + value : Optional[any], optional + Log value, by default None + + Returns + ------- + List[Log] + List of Log instances + """ + logs = [] + for log in self.logs: + log_value = getattr(log, key, None) + if value is not None and log_value == value: + logs.append(log) + elif value is None and hasattr(log, key): + logs.append(log) + return logs + + def get_cold_start_log(self) -> List[Log]: + """Get logs where cold start was true + + Returns + ------- + List[Log] + List of Log instances + """ + return [log for log in self.logs if log.cold_start] + + def have_keys(self, *keys) -> bool: + """Whether an arbitrary number of key names exist in each log event + + Returns + ------- + bool + Whether keys are present + """ + return all(hasattr(log, key) for log in self.logs for key in keys) + + def _get_logs(self) -> List[Log]: + ret = self.log_client.filter_log_events( + logGroupName=self.log_group, + startTime=self.start_time, + filterPattern=self.filter_expression, + ) + + if not ret["events"]: + raise ValueError("Empty response from Cloudwatch Logs. Repeating...") + + filtered_logs = [] + for event in ret["events"]: + try: + message = Log(**json.loads(event["message"])) + except json.decoder.JSONDecodeError: + continue + filtered_logs.append(message) + + return filtered_logs + + def __len__(self) -> int: + return len(self.logs) + + +@retry(ValueError, delay=2, jitter=1.5, tries=10) +def get_logs( + function_name: str, + start_time: datetime, + filter_expression: Optional[str] = None, + log_client: Optional[CloudWatchLogsClient] = None, +) -> LogFetcher: + """_summary_ + + Parameters + ---------- + function_name : str + Name of Lambda function to fetch logs for + start_time : datetime + Start date range to filter traces + log_client : Optional[CloudWatchLogsClient], optional + Amazon CloudWatch Logs Client, by default boto3.client('logs) + filter_expression : Optional[str], optional + CloudWatch Logs Filter Pattern expression, by default "message" + + Returns + ------- + LogFetcher + LogFetcher instance with logs available as properties and methods + """ + return LogFetcher( + function_name=function_name, start_time=start_time, filter_expression=filter_expression, log_client=log_client + ) diff --git a/tests/e2e/utils/data_fetcher/metrics.py b/tests/e2e/utils/data_fetcher/metrics.py new file mode 100644 index 00000000000..18023b18336 --- /dev/null +++ b/tests/e2e/utils/data_fetcher/metrics.py @@ -0,0 +1,71 @@ +from datetime import datetime, timedelta +from typing import List, Optional + +import boto3 +from mypy_boto3_cloudwatch import CloudWatchClient +from mypy_boto3_cloudwatch.type_defs import DimensionTypeDef +from retry import retry + +from tests.e2e.utils.data_builder import build_metric_query_data + + +@retry(ValueError, delay=2, jitter=1.5, tries=10) +def get_metrics( + namespace: str, + start_date: datetime, + metric_name: str, + dimensions: Optional[List[DimensionTypeDef]] = None, + cw_client: Optional[CloudWatchClient] = None, + end_date: Optional[datetime] = None, + period: int = 60, + stat: str = "Sum", +) -> List[float]: + """Fetch CloudWatch Metrics + + It takes into account eventual consistency with up to 10 retries and 1.5s jitter. + + Parameters + ---------- + namespace : str + Metric Namespace + start_date : datetime + Start window to fetch metrics + metric_name : str + Metric name + dimensions : Optional[List[DimensionTypeDef]], optional + List of Metric Dimension, by default None + cw_client : Optional[CloudWatchClient], optional + Boto3 CloudWatch low-level client (boto3.client("cloudwatch"), by default None + end_date : Optional[datetime], optional + End window to fetch metrics, by default start_date + 2 minutes window + period : int, optional + Time period to fetch metrics for, by default 60 + stat : str, optional + Aggregation function to use when fetching metrics, by default "Sum" + + Returns + ------- + List[float] + List with metric values found + + Raises + ------ + ValueError + When no metric is found within retry window + """ + cw_client = cw_client or boto3.client("cloudwatch") + end_date = end_date or start_date + timedelta(minutes=2) + + metric_query = build_metric_query_data( + namespace=namespace, metric_name=metric_name, period=period, stat=stat, dimensions=dimensions + ) + + response = cw_client.get_metric_data( + MetricDataQueries=metric_query, + StartTime=start_date, + EndTime=end_date or datetime.utcnow(), + ) + result = response["MetricDataResults"][0]["Values"] + if not result: + raise ValueError("Empty response from Cloudwatch. Repeating...") + return result diff --git a/tests/e2e/utils/data_fetcher/traces.py b/tests/e2e/utils/data_fetcher/traces.py new file mode 100644 index 00000000000..827109112df --- /dev/null +++ b/tests/e2e/utils/data_fetcher/traces.py @@ -0,0 +1,267 @@ +import json +from datetime import datetime, timedelta +from typing import Any, Dict, Generator, List, Optional + +import boto3 +from botocore.paginate import PageIterator +from mypy_boto3_xray.client import XRayClient +from mypy_boto3_xray.type_defs import TraceSummaryTypeDef +from pydantic import BaseModel +from retry import retry + + +class TraceSubsegment(BaseModel): + id: str # noqa: A003 VNE003 # id is a field we can't change + name: str + start_time: float + end_time: float + aws: Optional[dict] + subsegments: Optional[List["TraceSubsegment"]] + annotations: Optional[Dict[str, Any]] + metadata: Optional[Dict[str, Dict[str, Any]]] + + +class TraceDocument(BaseModel): + id: str # noqa: A003 VNE003 # id is a field we can't change + name: str + start_time: float + end_time: float + trace_id: str + parent_id: Optional[str] + aws: Dict + origin: str + subsegments: Optional[List[TraceSubsegment]] + + +class TraceFetcher: + default_exclude_seg_name: List = ["Initialization", "Invocation", "Overhead"] + + def __init__( + self, + filter_expression: str, + start_date: datetime, + end_date: Optional[datetime] = None, + xray_client: Optional[XRayClient] = None, + exclude_segment_name: Optional[List[str]] = None, + resource_name: Optional[List[str]] = None, + origin: Optional[List[str]] = None, + minimum_traces: int = 1, + ): + """Fetch and expose traces from X-Ray based on parameters + + Data is recursively fetched in the following order: + + * Trace summaries + * Trace IDs + * Traces + * Segments + * Subsegments + * Nested Subsegments + + Parameters + ---------- + filter_expression : str + AWS X-Ray Filter Expressions + see: https://docs.aws.amazon.com/xray/latest/devguide/xray-console-filters.html + start_date : datetime + Start date range to filter traces + end_date : Optional[datetime], optional + End date range to filter traces, by default 5 minutes past start_date + xray_client : Optional[XRayClient], optional + AWS X-Ray SDK Client, by default boto3.client('xray') + exclude_segment_name : Optional[List[str]], optional + Name of segments to exclude, by default ["Initialization", "Invocation", "Overhead"] + resource_name : Optional[List[str]], optional + Name of resource to filter traces (e.g., function name), by default None + origin : Optional[List[str]], optional + Trace origin name to filter traces, by default ["AWS::Lambda::Function"] + minimum_traces : int + Minimum number of traces to be retrieved before exhausting retry attempts + """ + self.filter_expression = filter_expression + self.start_date = start_date + self.end_date = end_date or self.start_date + timedelta(minutes=5) + self.xray_client: XRayClient = xray_client or boto3.client("xray") + self.trace_documents: Dict[str, TraceDocument] = {} + self.subsegments: List[TraceSubsegment] = [] + self.exclude_segment_name = exclude_segment_name or self.default_exclude_seg_name + self.resource_name = resource_name + self.origin = origin or ["AWS::Lambda::Function"] + self.annotations: List[Dict[str, Any]] = [] + self.metadata: List[Dict[str, Dict[str, Any]]] = [] + self.minimum_traces = minimum_traces + + paginator = self.xray_client.get_paginator("get_trace_summaries") + pages = paginator.paginate( + StartTime=self.start_date, + EndTime=self.end_date, + TimeRangeType="Event", + Sampling=False, + FilterExpression=self.filter_expression, + ) + + trace_ids = self._get_trace_ids(pages) + self.trace_documents = self._get_trace_documents(trace_ids) + self.subsegments = self._get_subsegments() + + def get_annotation(self, key: str, value: Optional[any] = None) -> List: + return [ + annotation + for annotation in self.annotations + if (value is not None and annotation.get(key) == value) or (value is None and key in annotation) + ] + + def get_metadata(self, key: str, namespace: str = "") -> List[Dict[str, Any]]: + seen = [] + for meta in self.metadata: + metadata = meta.get(namespace, {}) + if key in metadata: + seen.append(metadata) + return seen + + def get_subsegment(self, name: str) -> List: + return [seg for seg in self.subsegments if seg.name == name] + + def _find_nested_subsegments(self, subsegments: List[TraceSubsegment]) -> Generator[TraceSubsegment, None, None]: + """Recursively yield any subsegment that we might be interested. + + It excludes any subsegments contained in exclude_segment_name. + Since these are nested, subsegment name might be '## lambda_handler'. + + It also populates annotations and metadata nested in subsegments. + + Parameters + ---------- + subsegment : TraceSubsegment + subsegment to traverse + seen : List + list of subsegments to be updated + """ + for seg in subsegments: + if seg.name not in self.exclude_segment_name: + if seg.annotations: + self.annotations.append(seg.annotations) + + if seg.metadata: + self.metadata.append(seg.metadata) + + yield seg + + if seg.subsegments: + # recursively iterate over any arbitrary number of subsegments + yield from self._find_nested_subsegments(seg.subsegments) + + def _get_subsegments(self) -> List[TraceSubsegment]: + """Find subsegments and potentially any nested subsegments + + It excludes any subsegments contained in exclude_segment_name. + Since these are top-level, subsegment name might be 'Overhead/Invocation, etc.'. + + Returns + ------- + List[TraceSubsegment] + List of subsegments + """ + seen = [] + for document in self.trace_documents.values(): + if document.subsegments: + seen.extend(self._find_nested_subsegments(document.subsegments)) + + return seen + + def _get_trace_ids(self, pages: PageIterator) -> List[str]: + """Get list of trace IDs found + + Parameters + ---------- + pages : PageIterator + Paginated streaming response from AWS X-Ray + + Returns + ------- + List[str] + Trace IDs + + Raises + ------ + ValueError + When no traces are available within time range and filter expression + """ + summaries: List[TraceSummaryTypeDef] = [trace["TraceSummaries"] for trace in pages if trace["TraceSummaries"]] + if not summaries: + raise ValueError("Empty response from X-Ray. Repeating...") + + trace_ids = [trace["Id"] for trace in summaries[0]] # type: ignore[index] # TypedDict not being recognized + if len(trace_ids) < self.minimum_traces: + raise ValueError( + f"Number of traces found doesn't meet minimum required ({self.minimum_traces}). Repeating..." + ) + + return trace_ids + + def _get_trace_documents(self, trace_ids: List[str]) -> Dict[str, TraceDocument]: + """Find trace documents available in each trace segment + + Returns + ------- + Dict[str, TraceDocument] + Trace documents grouped by their ID + """ + traces = self.xray_client.batch_get_traces(TraceIds=trace_ids) + documents: Dict = {} + segments = [seg for trace in traces["Traces"] for seg in trace["Segments"]] + for seg in segments: + trace_document = TraceDocument(**json.loads(seg["Document"])) + if trace_document.origin in self.origin or trace_document.name == self.resource_name: + documents[trace_document.id] = trace_document + return documents + + +@retry(ValueError, delay=5, jitter=1.5, tries=10) +def get_traces( + filter_expression: str, + start_date: datetime, + end_date: Optional[datetime] = None, + xray_client: Optional[XRayClient] = None, + exclude_segment_name: Optional[List[str]] = None, + resource_name: Optional[List[str]] = None, + origin: Optional[List[str]] = None, + minimum_traces: int = 1, +) -> TraceFetcher: + """Fetch traces from AWS X-Ray + + Parameters + ---------- + filter_expression : str + AWS X-Ray Filter Expressions + see: https://docs.aws.amazon.com/xray/latest/devguide/xray-console-filters.html + start_date : datetime + Start date range to filter traces + end_date : Optional[datetime], optional + End date range to filter traces, by default 5 minutes past start_date + xray_client : Optional[XRayClient], optional + AWS X-Ray SDK Client, by default boto3.client('xray') + exclude_segment_name : Optional[List[str]], optional + Name of segments to exclude, by default ["Initialization", "Invocation", "Overhead"] + resource_name : Optional[List[str]], optional + Name of resource to filter traces (e.g., function name), by default None + origin : Optional[List[str]], optional + Trace origin name to filter traces, by default ["AWS::Lambda::Function"] + minimum_traces : int + Minimum number of traces to be retrieved before exhausting retry attempts + + Returns + ------- + TraceFetcher + TraceFetcher instance with trace data available as properties and methods + """ + return TraceFetcher( + filter_expression=filter_expression, + start_date=start_date, + end_date=end_date, + xray_client=xray_client, + exclude_segment_name=exclude_segment_name, + resource_name=resource_name, + origin=origin, + minimum_traces=minimum_traces, + ) diff --git a/tests/e2e/utils/helpers.py b/tests/e2e/utils/helpers.py deleted file mode 100644 index 3f88f44f933..00000000000 --- a/tests/e2e/utils/helpers.py +++ /dev/null @@ -1,131 +0,0 @@ -import json -from datetime import datetime -from functools import lru_cache -from typing import Dict, List, Optional, Union - -from mypy_boto3_cloudwatch import type_defs -from mypy_boto3_cloudwatch.client import CloudWatchClient -from mypy_boto3_lambda.client import LambdaClient -from mypy_boto3_xray.client import XRayClient -from pydantic import BaseModel -from retry import retry - - -# Helper methods && Class -class Log(BaseModel): - level: str - location: str - message: Union[dict, str] - timestamp: str - service: str - cold_start: Optional[bool] - function_name: Optional[str] - function_memory_size: Optional[str] - function_arn: Optional[str] - function_request_id: Optional[str] - xray_trace_id: Optional[str] - extra_info: Optional[str] - - -class TraceSegment(BaseModel): - name: str - metadata: Dict = {} - annotations: Dict = {} - - -def trigger_lambda(lambda_arn: str, client: LambdaClient): - response = client.invoke(FunctionName=lambda_arn, InvocationType="RequestResponse") - return response - - -@lru_cache(maxsize=10, typed=False) -@retry(ValueError, delay=1, jitter=1, tries=20) -def get_logs(lambda_function_name: str, log_client: CloudWatchClient, start_time: int, **kwargs: dict) -> List[Log]: - response = log_client.filter_log_events(logGroupName=f"/aws/lambda/{lambda_function_name}", startTime=start_time) - if not response["events"]: - raise ValueError("Empty response from Cloudwatch Logs. Repeating...") - filtered_logs = [] - for event in response["events"]: - try: - message = Log(**json.loads(event["message"])) - except json.decoder.JSONDecodeError: - continue - filtered_logs.append(message) - - return filtered_logs - - -@lru_cache(maxsize=10, typed=False) -@retry(ValueError, delay=1, jitter=1, tries=20) -def get_metrics( - namespace: str, - cw_client: CloudWatchClient, - start_date: datetime, - metric_name: str, - service_name: str, - end_date: Optional[datetime] = None, -) -> type_defs.MetricDataResultTypeDef: - response = cw_client.get_metric_data( - MetricDataQueries=[ - { - "Id": "m1", - "Expression": f'SELECT MAX("{metric_name}") from SCHEMA("{namespace}",service) \ - where service=\'{service_name}\'', - "ReturnData": True, - "Period": 600, - }, - ], - StartTime=start_date, - EndTime=end_date if end_date else datetime.utcnow(), - ) - result = response["MetricDataResults"][0] - if not result["Values"]: - raise ValueError("Empty response from Cloudwatch. Repeating...") - return result - - -@retry(ValueError, delay=1, jitter=1, tries=10) -def get_traces(filter_expression: str, xray_client: XRayClient, start_date: datetime, end_date: datetime) -> Dict: - paginator = xray_client.get_paginator("get_trace_summaries") - response_iterator = paginator.paginate( - StartTime=start_date, - EndTime=end_date, - TimeRangeType="Event", - Sampling=False, - FilterExpression=filter_expression, - ) - - traces = [trace["TraceSummaries"][0]["Id"] for trace in response_iterator if trace["TraceSummaries"]] - if not traces: - raise ValueError("Empty response from X-RAY. Repeating...") - - trace_details = xray_client.batch_get_traces( - TraceIds=traces, - ) - - return trace_details - - -def find_trace_additional_info(trace: Dict) -> List[TraceSegment]: - """Find all trace annotations and metadata and return them to the caller""" - info = [] - for segment in trace["Traces"][0]["Segments"]: - document = json.loads(segment["Document"]) - if document["origin"] == "AWS::Lambda::Function": - for subsegment in document["subsegments"]: - if subsegment["name"] == "Invocation": - find_meta(segment=subsegment, result=info) - return info - - -def find_meta(segment: dict, result: List): - for x_subsegment in segment["subsegments"]: - result.append( - TraceSegment( - name=x_subsegment["name"], - metadata=x_subsegment.get("metadata", {}), - annotations=x_subsegment.get("annotations", {}), - ) - ) - if x_subsegment.get("subsegments"): - find_meta(segment=x_subsegment, result=result) diff --git a/tests/e2e/utils/infrastructure.py b/tests/e2e/utils/infrastructure.py index 001ae0e6346..cddd6844504 100644 --- a/tests/e2e/utils/infrastructure.py +++ b/tests/e2e/utils/infrastructure.py @@ -1,212 +1,300 @@ -import io import json -import os +import logging import sys -import zipfile from abc import ABC, abstractmethod from enum import Enum from pathlib import Path -from typing import Dict, List, Tuple, Type +from typing import Dict, Generator, Optional, Tuple, Type +from uuid import uuid4 import boto3 +import pytest import yaml from aws_cdk import App, AssetStaging, BundlingOptions, CfnOutput, DockerImage, RemovalPolicy, Stack, aws_logs from aws_cdk.aws_lambda import Code, Function, LayerVersion, Runtime, Tracing +from filelock import FileLock +from mypy_boto3_cloudformation import CloudFormationClient + +from tests.e2e.utils.asset import Assets PYTHON_RUNTIME_VERSION = f"V{''.join(map(str, sys.version_info[:2]))}" - -class PythonVersion(Enum): - V37 = {"runtime": Runtime.PYTHON_3_7, "image": Runtime.PYTHON_3_7.bundling_image.image} - V38 = {"runtime": Runtime.PYTHON_3_8, "image": Runtime.PYTHON_3_8.bundling_image.image} - V39 = {"runtime": Runtime.PYTHON_3_9, "image": Runtime.PYTHON_3_9.bundling_image.image} +logger = logging.getLogger(__name__) class BaseInfrastructureStack(ABC): @abstractmethod - def synthesize() -> Tuple[dict, str]: + def synthesize(self) -> Tuple[dict, str]: ... @abstractmethod - def __call__() -> Tuple[dict, str]: + def __call__(self) -> Tuple[dict, str]: ... -class InfrastructureStack(BaseInfrastructureStack): - def __init__(self, handlers_dir: str, stack_name: str, config: dict) -> None: - self.stack_name = stack_name +class PythonVersion(Enum): + V37 = {"runtime": Runtime.PYTHON_3_7, "image": Runtime.PYTHON_3_7.bundling_image.image} + V38 = {"runtime": Runtime.PYTHON_3_8, "image": Runtime.PYTHON_3_8.bundling_image.image} + V39 = {"runtime": Runtime.PYTHON_3_9, "image": Runtime.PYTHON_3_9.bundling_image.image} + + +class BaseInfrastructure(ABC): + def __init__(self, feature_name: str, handlers_dir: Path, layer_arn: str = "") -> None: + self.feature_name = feature_name + self.stack_name = f"test{PYTHON_RUNTIME_VERSION}-{feature_name}-{uuid4()}" self.handlers_dir = handlers_dir - self.config = config + self.layer_arn = layer_arn + self.stack_outputs: Dict[str, str] = {} - def _create_layer(self, stack: Stack): - output_dir = Path(str(AssetStaging.BUNDLING_OUTPUT_DIR), "python") - input_dir = Path(str(AssetStaging.BUNDLING_INPUT_DIR), "aws_lambda_powertools") - powertools_layer = LayerVersion( - stack, - "aws-lambda-powertools", - layer_version_name="aws-lambda-powertools", - compatible_runtimes=[PythonVersion[PYTHON_RUNTIME_VERSION].value["runtime"]], - code=Code.from_asset( - path=".", - bundling=BundlingOptions( - image=DockerImage.from_build( - str(Path(__file__).parent), - build_args={"IMAGE": PythonVersion[PYTHON_RUNTIME_VERSION].value["image"]}, - ), - command=[ - "bash", - "-c", - rf"poetry export --with-credentials --format requirements.txt --output /tmp/requirements.txt &&\ - pip install -r /tmp/requirements.txt -t {output_dir} &&\ - cp -R {input_dir} {output_dir}", - ], - ), - ), - ) - return powertools_layer - - def _find_handlers(self, directory: str) -> List: - for root, _, files in os.walk(directory): - return [os.path.join(root, filename) for filename in files if filename.endswith(".py")] - - def synthesize(self, handlers: List[str]) -> Tuple[dict, str, str]: - integration_test_app = App() - stack = Stack(integration_test_app, self.stack_name) - powertools_layer = self._create_layer(stack) - code = Code.from_asset(self.handlers_dir) - - for filename_path in handlers: - filename = Path(filename_path).stem - function_python = Function( - stack, - f"{filename}-lambda", - runtime=PythonVersion[PYTHON_RUNTIME_VERSION].value["runtime"], - code=code, - handler=f"{filename}.lambda_handler", - layers=[powertools_layer], - environment=self.config.get("environment_variables"), - tracing=Tracing.ACTIVE - if self.config.get("parameters", {}).get("tracing") == "ACTIVE" - else Tracing.DISABLED, + # NOTE: Investigate why cdk.Environment in Stack + # changes synthesized asset (no object_key in asset manifest) + self.app = App() + self.stack = Stack(self.app, self.stack_name) + self.session = boto3.Session() + self.cfn: CloudFormationClient = self.session.client("cloudformation") + + # NOTE: CDK stack account and region are tokens, we need to resolve earlier + self.account_id = self.session.client("sts").get_caller_identity()["Account"] + self.region = self.session.region_name + + def create_lambda_functions(self, function_props: Optional[Dict] = None): + """Create Lambda functions available under handlers_dir + + It creates CloudFormation Outputs for every function found in PascalCase. For example, + {handlers_dir}/basic_handler.py creates `BasicHandler` and `BasicHandlerArn` outputs. + + + Parameters + ---------- + function_props: Optional[Dict] + Dictionary representing CDK Lambda FunctionProps to override defaults + + Examples + -------- + + Creating Lambda functions available in the handlers directory + + ```python + self.create_lambda_functions() + ``` + + Creating Lambda functions and override runtime to Python 3.7 + + ```python + from aws_cdk.aws_lambda import Runtime + + self.create_lambda_functions(function_props={"runtime": Runtime.PYTHON_3_7) + ``` + """ + handlers = list(self.handlers_dir.rglob("*.py")) + source = Code.from_asset(f"{self.handlers_dir}") + logger.debug(f"Creating functions for handlers: {handlers}") + if not self.layer_arn: + raise ValueError( + """Lambda Layer ARN cannot be empty when creating Lambda functions. + Make sure to inject `lambda_layer_arn` fixture and pass at the constructor level""" ) + layer = LayerVersion.from_layer_version_arn(self.stack, "layer-arn", layer_version_arn=self.layer_arn) + function_settings_override = function_props or {} + for fn in handlers: + fn_name = fn.stem + fn_name_pascal_case = fn_name.title().replace("_", "") # basic_handler -> BasicHandler + logger.debug(f"Creating function: {fn_name_pascal_case}") + function_settings = { + "id": f"{fn_name}-lambda", + "code": source, + "handler": f"{fn_name}.lambda_handler", + "tracing": Tracing.ACTIVE, + "runtime": Runtime.PYTHON_3_9, + "layers": [layer], + **function_settings_override, + } + + function = Function(self.stack, **function_settings) + aws_logs.LogGroup( - stack, - f"{filename}-lg", - log_group_name=f"/aws/lambda/{function_python.function_name}", + self.stack, + id=f"{fn_name}-lg", + log_group_name=f"/aws/lambda/{function.function_name}", retention=aws_logs.RetentionDays.ONE_DAY, removal_policy=RemovalPolicy.DESTROY, ) - CfnOutput(stack, f"{filename}_arn", value=function_python.function_arn) - cloud_assembly = integration_test_app.synth() - cf_template = cloud_assembly.get_stack_by_name(self.stack_name).template - cloud_assembly_directory = cloud_assembly.directory - cloud_assembly_assets_manifest_path = cloud_assembly.get_stack_by_name(self.stack_name).dependencies[0].file - return (cf_template, cloud_assembly_directory, cloud_assembly_assets_manifest_path) + # CFN Outputs only support hyphen hence pascal case + self.add_cfn_output(name=fn_name_pascal_case, value=function.function_name, arn=function.function_arn) - def __call__(self) -> Tuple[dict, str]: - handlers = self._find_handlers(directory=self.handlers_dir) - return self.synthesize(handlers=handlers) - - -class Infrastructure: - def __init__(self, stack_name: str, handlers_dir: str, config: dict) -> None: - session = boto3.Session() - self.s3_client = session.client("s3") - self.lambda_client = session.client("lambda") - self.cf_client = session.client("cloudformation") - self.s3_resource = session.resource("s3") - self.account_id = session.client("sts").get_caller_identity()["Account"] - self.region = session.region_name - self.stack_name = stack_name - self.handlers_dir = handlers_dir - self.config = config + def deploy(self) -> Dict[str, str]: + """Creates CloudFormation Stack and return stack outputs as dict - def deploy(self, Stack: Type[BaseInfrastructureStack]) -> Dict[str, str]: + Returns + ------- + Dict[str, str] + CloudFormation Stack Outputs with output key and value + """ + template, asset_manifest_file = self._synthesize() + assets = Assets(asset_manifest=asset_manifest_file, account_id=self.account_id, region=self.region) + assets.upload() + self.stack_outputs = self._deploy_stack(self.stack_name, template) + return self.stack_outputs + + def delete(self) -> None: + """Delete CloudFormation Stack""" + logger.debug(f"Deleting stack: {self.stack_name}") + self.cfn.delete_stack(StackName=self.stack_name) - stack = Stack(handlers_dir=self.handlers_dir, stack_name=self.stack_name, config=self.config) - template, asset_root_dir, asset_manifest_file = stack() - self._upload_assets(asset_root_dir, asset_manifest_file) + @abstractmethod + def create_resources(self) -> None: + """Create any necessary CDK resources. It'll be called before deploy - response = self._deploy_stack(self.stack_name, template) + Examples + ------- - return self._transform_output(response["Stacks"][0]["Outputs"]) + Creating a S3 bucket and export name and ARN - def delete(self): - self.cf_client.delete_stack(StackName=self.stack_name) + ```python + def created_resources(self): + s3 = s3.Bucket(self.stack, "MyBucket") - def _upload_assets(self, asset_root_dir: str, asset_manifest_file: str): - """ - This method is drop-in replacement for cdk-assets package s3 upload part. - https://www.npmjs.com/package/cdk-assets. - We use custom solution to avoid dependencies from nodejs ecosystem. - We follow the same design cdk-assets: - https://github.com/aws/aws-cdk-rfcs/blob/master/text/0092-asset-publishing.md. + # This will create MyBucket and MyBucketArn CloudFormation Output + self.add_cfn_output(name="MyBucket", value=s3.bucket_name, arn_value=bucket.bucket_arn) + ``` + + Creating Lambda functions available in the handlers directory + + ```python + def created_resources(self): + self.create_lambda_functions() + ``` """ + ... + + def _synthesize(self) -> Tuple[Dict, Path]: + logger.debug("Creating CDK Stack resources") + self.create_resources() + logger.debug("Synthesizing CDK Stack into raw CloudFormation template") + cloud_assembly = self.app.synth() + cf_template: Dict = cloud_assembly.get_stack_by_name(self.stack_name).template + cloud_assembly_assets_manifest_path: str = ( + cloud_assembly.get_stack_by_name(self.stack_name).dependencies[0].file # type: ignore[attr-defined] + ) + return cf_template, Path(cloud_assembly_assets_manifest_path) - assets = self._find_assets(asset_manifest_file, self.account_id, self.region) - - for s3_key, config in assets.items(): - print(config) - s3_bucket = self.s3_resource.Bucket(config["bucket_name"]) - - if config["asset_packaging"] != "zip": - print("Asset is not a zip file. Skipping upload") - continue - - if bool(list(s3_bucket.objects.filter(Prefix=s3_key))): - print("object exists, skipping") - continue - - buf = io.BytesIO() - asset_dir = f"{asset_root_dir}/{config['asset_path']}" - os.chdir(asset_dir) - asset_files = self._find_files(directory=".") - with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf: - for asset_file in asset_files: - zf.write(os.path.join(asset_file)) - buf.seek(0) - self.s3_client.upload_fileobj(Fileobj=buf, Bucket=config["bucket_name"], Key=s3_key) - - def _find_files(self, directory: str) -> List: - file_paths = [] - for root, _, files in os.walk(directory): - for filename in files: - file_paths.append(os.path.join(root, filename)) - return file_paths - - def _deploy_stack(self, stack_name: str, template: dict): - response = self.cf_client.create_stack( + def _deploy_stack(self, stack_name: str, template: Dict) -> Dict[str, str]: + logger.debug(f"Creating CloudFormation Stack: {stack_name}") + self.cfn.create_stack( StackName=stack_name, TemplateBody=yaml.dump(template), TimeoutInMinutes=10, OnFailure="ROLLBACK", Capabilities=["CAPABILITY_IAM"], ) - waiter = self.cf_client.get_waiter("stack_create_complete") + waiter = self.cfn.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name, WaiterConfig={"Delay": 10, "MaxAttempts": 50}) - response = self.cf_client.describe_stacks(StackName=stack_name) - return response - - def _find_assets(self, asset_template: str, account_id: str, region: str): - assets = {} - with open(asset_template, mode="r") as template: - for _, config in json.loads(template.read())["files"].items(): - asset_path = config["source"]["path"] - asset_packaging = config["source"]["packaging"] - bucket_name = config["destinations"]["current_account-current_region"]["bucketName"] - object_key = config["destinations"]["current_account-current_region"]["objectKey"] - - assets[object_key] = { - "bucket_name": bucket_name.replace("${AWS::AccountId}", account_id).replace( - "${AWS::Region}", region - ), - "asset_path": asset_path, - "asset_packaging": asset_packaging, - } - return assets + stack_details = self.cfn.describe_stacks(StackName=stack_name) + stack_outputs = stack_details["Stacks"][0]["Outputs"] + return {output["OutputKey"]: output["OutputValue"] for output in stack_outputs if output["OutputKey"]} + + def add_cfn_output(self, name: str, value: str, arn: str = ""): + """Create {Name} and optionally {Name}Arn CloudFormation Outputs. + + Parameters + ---------- + name : str + CloudFormation Output Key + value : str + CloudFormation Output Value + arn : str + CloudFormation Output Value for ARN + """ + CfnOutput(self.stack, f"{name}", value=value) + if arn: + CfnOutput(self.stack, f"{name}Arn", value=arn) + + +def deploy_once( + stack: Type[BaseInfrastructure], + request: pytest.FixtureRequest, + tmp_path_factory: pytest.TempPathFactory, + worker_id: str, + layer_arn: str, +) -> Generator[Dict[str, str], None, None]: + """Deploys provided stack once whether CPU parallelization is enabled or not + + Parameters + ---------- + stack : Type[BaseInfrastructure] + stack class to instantiate and deploy, for example MetricStack. + Not to be confused with class instance (MetricStack()). + request : pytest.FixtureRequest + pytest request fixture to introspect absolute path to test being executed + tmp_path_factory : pytest.TempPathFactory + pytest temporary path factory to discover shared tmp when multiple CPU processes are spun up + worker_id : str + pytest-xdist worker identification to detect whether parallelization is enabled + + Yields + ------ + Generator[Dict[str, str], None, None] + stack CloudFormation outputs + """ + handlers_dir = f"{request.node.path.parent}/handlers" + stack = stack(handlers_dir=Path(handlers_dir), layer_arn=layer_arn) + + try: + if worker_id == "master": + # no parallelization, deploy stack and let fixture be cached + yield stack.deploy() + else: + # tmp dir shared by all workers + root_tmp_dir = tmp_path_factory.getbasetemp().parent + cache = root_tmp_dir / f"{PYTHON_RUNTIME_VERSION}_cache.json" + + with FileLock(f"{cache}.lock"): + # If cache exists, return stack outputs back + # otherwise it's the first run by the main worker + # deploy and return stack outputs so subsequent workers can reuse + if cache.is_file(): + stack_outputs = json.loads(cache.read_text()) + else: + stack_outputs: Dict = stack.deploy() + cache.write_text(json.dumps(stack_outputs)) + yield stack_outputs + finally: + stack.delete() + + +class LambdaLayerStack(BaseInfrastructure): + FEATURE_NAME = "lambda-layer" + + def __init__(self, handlers_dir: Path, feature_name: str = FEATURE_NAME, layer_arn: str = "") -> None: + super().__init__(feature_name, handlers_dir, layer_arn) + + def create_resources(self): + layer = self._create_layer() + CfnOutput(self.stack, "LayerArn", value=layer) + + def _create_layer(self) -> str: + logger.debug("Creating Lambda Layer with latest source code available") + output_dir = Path(str(AssetStaging.BUNDLING_OUTPUT_DIR), "python") + input_dir = Path(str(AssetStaging.BUNDLING_INPUT_DIR), "aws_lambda_powertools") - def _transform_output(self, outputs: dict): - return {output["OutputKey"]: output["OutputValue"] for output in outputs if output["OutputKey"]} + build_commands = [f"pip install .[pydantic] -t {output_dir}", f"cp -R {input_dir} {output_dir}"] + layer = LayerVersion( + self.stack, + "aws-lambda-powertools-e2e-test", + layer_version_name="aws-lambda-powertools-e2e-test", + compatible_runtimes=[PythonVersion[PYTHON_RUNTIME_VERSION].value["runtime"]], + code=Code.from_asset( + path=".", + bundling=BundlingOptions( + image=DockerImage.from_build( + str(Path(__file__).parent), + build_args={"IMAGE": PythonVersion[PYTHON_RUNTIME_VERSION].value["image"]}, + ), + command=["bash", "-c", " && ".join(build_commands)], + ), + ), + ) + return layer.layer_version_arn diff --git a/tests/functional/parser/schemas.py b/tests/functional/parser/schemas.py index 8ff56f703a7..79a74f8eb53 100644 --- a/tests/functional/parser/schemas.py +++ b/tests/functional/parser/schemas.py @@ -86,3 +86,8 @@ class MyCloudWatchBusiness(BaseModel): class MyApiGatewayBusiness(BaseModel): message: str username: str + + +class MyALambdaFuncUrlBusiness(BaseModel): + message: str + username: str diff --git a/tests/functional/parser/test_lambda_function_url.py b/tests/functional/parser/test_lambda_function_url.py new file mode 100644 index 00000000000..a63a4e25884 --- /dev/null +++ b/tests/functional/parser/test_lambda_function_url.py @@ -0,0 +1,128 @@ +from aws_lambda_powertools.utilities.parser import envelopes, event_parser +from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel +from aws_lambda_powertools.utilities.typing import LambdaContext +from tests.functional.parser.schemas import MyALambdaFuncUrlBusiness +from tests.functional.utils import load_event + + +@event_parser(model=MyALambdaFuncUrlBusiness, envelope=envelopes.LambdaFunctionUrlEnvelope) +def handle_lambda_func_url_with_envelope(event: MyALambdaFuncUrlBusiness, _: LambdaContext): + assert event.message == "Hello" + assert event.username == "Ran" + + +@event_parser(model=LambdaFunctionUrlModel) +def handle_lambda_func_url_event(event: LambdaFunctionUrlModel, _: LambdaContext): + return event + + +def test_lambda_func_url_event_with_envelope(): + event = load_event("lambdaFunctionUrlEvent.json") + event["body"] = '{"message": "Hello", "username": "Ran"}' + handle_lambda_func_url_with_envelope(event, LambdaContext()) + + +def test_lambda_function_url_event(): + json_event = load_event("lambdaFunctionUrlEvent.json") + event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext()) + + assert event.version == "2.0" + assert event.routeKey == "$default" + + assert event.rawQueryString == "" + + assert event.cookies is None + + headers = event.headers + assert len(headers) == 20 + + assert event.queryStringParameters is None + + assert event.isBase64Encoded is False + assert event.body is None + assert event.pathParameters is None + assert event.stageVariables is None + + request_context = event.requestContext + + assert request_context.accountId == "anonymous" + assert request_context.apiId is not None + assert request_context.domainName == ".lambda-url.us-east-1.on.aws" + assert request_context.domainPrefix == "" + assert request_context.requestId == "id" + assert request_context.routeKey == "$default" + assert request_context.stage == "$default" + assert request_context.time is not None + convert_time = int(round(request_context.timeEpoch.timestamp() * 1000)) + assert convert_time == 1659687279885 + assert request_context.authorizer is None + + http = request_context.http + assert http.method == "GET" + assert http.path == "/" + assert http.protocol == "HTTP/1.1" + assert str(http.sourceIp) == "123.123.123.123/32" + assert http.userAgent == "agent" + + assert request_context.authorizer is None + + +def test_lambda_function_url_event_iam(): + json_event = load_event("lambdaFunctionUrlIAMEvent.json") + event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext()) + + assert event.version == "2.0" + assert event.routeKey == "$default" + + assert event.rawQueryString == "parameter1=value1¶meter1=value2¶meter2=value" + + cookies = event.cookies + assert len(cookies) == 2 + assert cookies[0] == "cookie1" + + headers = event.headers + assert len(headers) == 2 + + query_string_parameters = event.queryStringParameters + assert len(query_string_parameters) == 2 + assert query_string_parameters.get("parameter2") == "value" + + assert event.isBase64Encoded is False + assert event.body == "Hello from client!" + assert event.pathParameters is None + assert event.stageVariables is None + + request_context = event.requestContext + + assert request_context.accountId == "123456789012" + assert request_context.apiId is not None + assert request_context.domainName == ".lambda-url.us-west-2.on.aws" + assert request_context.domainPrefix == "" + assert request_context.requestId == "id" + assert request_context.routeKey == "$default" + assert request_context.stage == "$default" + assert request_context.time is not None + convert_time = int(round(request_context.timeEpoch.timestamp() * 1000)) + assert convert_time == 1583348638390 + + http = request_context.http + assert http.method == "POST" + assert http.path == "/my/path" + assert http.protocol == "HTTP/1.1" + assert str(http.sourceIp) == "123.123.123.123/32" + assert http.userAgent == "agent" + + authorizer = request_context.authorizer + assert authorizer is not None + assert authorizer.jwt is None + assert authorizer.lambda_value is None + + iam = authorizer.iam + assert iam is not None + assert iam.accessKey == "AKIA..." + assert iam.accountId == "111122223333" + assert iam.callerId == "AIDA..." + assert iam.cognitoIdentity is None + assert iam.principalOrgId is None + assert iam.userId == "AIDA..." + assert iam.userArn == "arn:aws:iam::111122223333:user/example-user" diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py index 2482b0177d3..d9c5b91214a 100644 --- a/tests/unit/test_tracing.py +++ b/tests/unit/test_tracing.py @@ -8,6 +8,8 @@ from aws_lambda_powertools import Tracer +# Maintenance: This should move to Functional tests and use Fake over mocks. + @pytest.fixture def dummy_response(): diff --git a/tests/unit/test_utilities_batch.py b/tests/unit/test_utilities_batch.py index 57de0223404..8cc4f0b0225 100644 --- a/tests/unit/test_utilities_batch.py +++ b/tests/unit/test_utilities_batch.py @@ -4,6 +4,8 @@ from aws_lambda_powertools.utilities.batch import PartialSQSProcessor from aws_lambda_powertools.utilities.batch.exceptions import SQSBatchProcessingError +# Maintenance: This will be deleted as part of legacy Batch deprecation + @pytest.fixture(scope="function") def sqs_event(): diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000000..286b1c10ab0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = py37,py38,py39 + +[testenv] +deps = + filelock + pytest-xdist + pydantic + email-validator + +commands = python parallel_run_e2e.py + +; If you ever encounter another parallel lock across interpreters +; pip install tox tox-poetry +; tox -p --parallel-live \ No newline at end of file