├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── linting.yml │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── LICENSE ├── NOTICE ├── README.md ├── appconfig_helper ├── __init__.py ├── appconfig_helper.py ├── py.typed └── version.py ├── example ├── appconfig-resources-stack.yml ├── main.py └── requirements.txt ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── tests └── test_main.py └── tox.ini /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | name: Python 3.8 10 | 11 | steps: 12 | - uses: actions/checkout@v4.1.1 13 | - name: Setup python 14 | uses: actions/setup-python@v4.7.1 15 | with: 16 | python-version: 3.12 17 | architecture: x64 18 | - name: Update pip 19 | run: pip install --upgrade pip 20 | - name: Install linters 21 | run: pip install black flake8 22 | - name: check code style with black 23 | run: black --check **/*.py 24 | - name: flake8 tests 25 | run: flake8 **/*.py 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tox Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.8, 3.9, "3.10", 3.11, 3.12] 11 | 12 | steps: 13 | - uses: actions/checkout@v4.1.1 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4.7.1 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install tox tox-gh-actions 22 | - name: Test with tox 23 | run: tox 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tox 2 | dist 3 | *.egg-info 4 | .python-version 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/psf/black 12 | rev: 24.4.1 13 | hooks: 14 | - id: black 15 | - repo: https://github.com/pre-commit/mirrors-isort 16 | rev: v5.10.1 # pick the isort version you'd like to use from https://github.com/pre-commit/mirrors-isort/releases 17 | hooks: 18 | - id: isort 19 | - repo: https://github.com/astral-sh/ruff-pre-commit 20 | # Ruff version. 21 | rev: v0.1.3 22 | hooks: 23 | - id: ruff 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 2.2.1 - 2025-01-08 11 | 12 | - Handle `VersionLabel` not being present in API response gracefully 13 | 14 | ## 2.2.0 - 2024-11-26 15 | 16 | ### Added 17 | 18 | - Support the `VersionLabel` response element (as the `version_label` property) 19 | - Bump boto3 and related libraries 20 | 21 | ## 2.1.2 - 2024-06-18 22 | 23 | ### Changed 24 | 25 | - Bump urllib3 dependency due to security issue. 26 | - Add missing explicit export for mypy (#15) 27 | - No functionality changes. 28 | 29 | ## 2.1.1 - 2024-04-26 30 | 31 | ### Changed 32 | 33 | - Include py.typed file to indicate the library is typed per PEP-561. 34 | - No code changes. 35 | 36 | ## 2.1.0 - 2023-11-03 37 | 38 | ### Changed 39 | 40 | - Removed Python 3.6 from list of supported versions. Minimum version is now 3.7. 41 | - Updated dependencies. 42 | - No code changes. 43 | 44 | ## 2.0.3 - 2022-07-19 45 | 46 | ### Fixed 47 | 48 | - Fixed an issue where the library would get stuck if it needed to create a new 49 | session in order to continue (reported and contributed by @BiochemLouis in #5 50 | and #6) 51 | 52 | ## 2.0.2 - 2022-04-04 53 | 54 | This is a release to bump the pip package version, to correct the previous PyPI release not containing the correct revision of code. No other changes. 55 | 56 | ## 2.0.1 - 2022-01-11 57 | 58 | ### Fixed 59 | 60 | - Fixed an issue with empty (unchanged) updates (#2) 61 | 62 | ## 2.0.0 - 2021-11-19 63 | 64 | ### Changed 65 | 66 | - Updated library to use new AppConfig Data API 67 | 68 | ### Removed 69 | 70 | - Removed the `client_id` parameter for the AppConfigHelper class as it is no 71 | longer used by the API 72 | - Removed the `config_version` property, as it is no longer returned by the API 73 | 74 | ## 1.1.0 - 2020-11-04 75 | 76 | ### Added 77 | 78 | - `raw_content` and `content_type` properties 79 | 80 | ## 1.0.0 - 2020-10-20 81 | 82 | - Initial release 83 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | 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. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Louis Cochen (2022) 2 | pete-volantautonomy (@pete-volantautonomy on GitHub) 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc., its affiliates and contributors. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample AWS AppConfig Helper 2 | 3 | A sample helper Python library for AWS AppConfig which makes rolling configuration updates out easier. 4 | 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sample-helper-aws-appconfig) ![PyPI version](https://badge.fury.io/py/sample-helper-aws-appconfig.svg) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 6 | 7 | ## Features 8 | 9 | * Configurable update interval: you can ask the library to update your configuration as often as needed, but it will only call the AWS AppConfig API at the configured interval (in seconds). 10 | * Handles correct API usage: the library uses the new AppConfig Data API and handles tracking the next configuration token and poll interval for you. 11 | * Flexible: Can automatically fetch the current configuration on initialisation, every time the configuration is read by your code, or on demand. You can override the caching interval if needed. 12 | * Handles YAML, JSON and plain text configurations, stored in any supported AppConfig store. Any other content type is returned unprocessed as the Python `bytes` type. 13 | * Supports AWS Lambda, Amazon EC2 instances and on-premises use. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | pip install sample-helper-aws-appconfig 19 | ``` 20 | 21 | ## Example 22 | 23 | ```python 24 | from appconfig_helper import AppConfigHelper 25 | from fastapi import FastAPI 26 | 27 | appconfig = AppConfigHelper( 28 | "MyAppConfigApp", 29 | "MyAppConfigEnvironment", 30 | "MyAppConfigProfile", 31 | 45 # minimum interval between update checks 32 | ) 33 | 34 | app = FastAPI() 35 | 36 | @app.get("/some-url") 37 | def index(): 38 | if appconfig.update_config(): 39 | print("New configuration received") 40 | # your configuration is available in the "config" attribute 41 | return { 42 | "config_info": appconfig.config 43 | } 44 | ``` 45 | 46 | ## Usage 47 | 48 | Please see the [AWS AppConfig documentation](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html) for details on configuring the service. 49 | 50 | ### Initialising 51 | 52 | Start by creating an `AppConfigHelper` object. You must specify the application name, environment name, and profile (configuration) name. You must also specify the refresh interval, in seconds. AppConfigHelper will not attempt to fetch a new configuration version from the AWS AppConfig service more frequently than this interval. You should set it low enough that your code will receive new configuration promptly, but not so low that it takes too long. The library enforces a minimum interval of 15 seconds. 53 | 54 | The configuration is not automatically fetched unless you set `fetch_on_init`. To have the library fetch the configuration when it is accessed, if it has been more than `max_config_age` seconds since the last fetch, set `fetch_on_read`. 55 | 56 | If you need to customise the AWS credentials or region, set `session` to a configured `boto3.Session` object. Otherwise, the [standard boto3 logic](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html) for credential/configuration discovery is used. 57 | 58 | ### Reading the configuration 59 | 60 | The configuration from AWS AppConfig is available as the `config` property. Before accessing it, you should call `update_config()`, unless you specified fetch_on_init or fetch_on_read during initialisation. If you want to force a config fetch, even if the number of seconds specified have not yet passed, call `update_config(True)`. 61 | 62 | `update_config()` returns `True` if a new version of the configuration was received. If no attempt was made to fetch it, or the configuration received was the same as current one, it returns `False`. It will raise `ValueError` if the received configuration data could not be processed (e.g. invalid JSON). If needed, the inner exception for JSON or YAML parsing is available as `__context__` on the raised exception. 63 | 64 | To read the values in your configuration, access the `config` property. For JSON and YAML configurations, this will contain the structure of your data. For plain text configurations, this will be a simple string. 65 | 66 | The original data received from AppConfig is available in the `raw_config` property. Accessing this property will not trigger an automatic update even if `fetch_on_read` is True. The content type field received from AppConfig is available in the `content_type` property. 67 | 68 | For example, with the following JSON in your AppConfig configuration profile: 69 | 70 | ```json 71 | { 72 | "hello": "world", 73 | "data": { 74 | "is_sample": true 75 | } 76 | } 77 | ``` 78 | 79 | you would see the following when using the library: 80 | 81 | ```python 82 | # appconfig is the instance of the library 83 | >>> appconfig.config["hello"] 84 | "world" 85 | >>> appconfig.config["data"] 86 | {'is_sample': True} 87 | ``` 88 | 89 | ### Use in AWS Lambda 90 | 91 | AWS AppConfig is best used in Lambda by taking advantage of [Lambda Extensions](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html) 92 | 93 | ## Security 94 | 95 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 96 | 97 | ## Licence 98 | 99 | This library is licensed under Apache-2.0. See the LICENSE file. 100 | -------------------------------------------------------------------------------- /appconfig_helper/__init__.py: -------------------------------------------------------------------------------- 1 | from .appconfig_helper import AppConfigHelper # noqa: F401 2 | 3 | __all__ = ["AppConfigHelper"] 4 | -------------------------------------------------------------------------------- /appconfig_helper/appconfig_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | AppConfig Helper class 3 | """ 4 | 5 | import json 6 | import time 7 | from typing import Any, Dict, Optional, Union, cast 8 | 9 | import boto3 10 | import botocore 11 | 12 | try: 13 | import yaml 14 | 15 | yaml_available = True 16 | except ImportError: 17 | yaml_available = False 18 | 19 | 20 | class AppConfigHelper: 21 | """ 22 | AWS AppConfig Helper class. 23 | 24 | Helps you fetch configuration from AWS AppConfig easily. Parses JSON and 25 | YAML configurations into native Python dicts, and keeps plain text as 26 | str. 27 | 28 | `appconfig_application`, `appconfig_environment` and `appconfig_profile` 29 | are the names or IDs of the AWS AppConfig application, environment and 30 | profile (configuration) respectively. 31 | 32 | `max_config_age` is the minimum interval in seconds between attempts to 33 | update the configuration. Set it low enough that your application 34 | receives new configuration in good time, but not so high that you are 35 | unnecessarily polling the AWS AppConfig service. A minimum of 15 seconds 36 | is enforced to help avoid throttling. Note that the value can be updated 37 | internally by the response from AppConfig. 38 | 39 | If you need to override credentials or AWS Region, set `session` to a 40 | preconfigured `boto3.Session` object. 41 | 42 | If `fetch_on_init` is set, attempt to fetch configuration when the 43 | instance is created. 44 | 45 | If `fetch_on_read` is set, every time the `config` property is read, the 46 | configuration will be refreshed (if it has been at least `max_config_age` 47 | seconds since the last refresh). 48 | """ 49 | 50 | def __init__( 51 | self, 52 | appconfig_application: str, 53 | appconfig_environment: str, 54 | appconfig_profile: str, 55 | max_config_age: int, 56 | *, 57 | session: Optional[boto3.Session] = None, 58 | fetch_on_init: bool = False, 59 | fetch_on_read: bool = False, 60 | ) -> None: 61 | if isinstance(session, boto3.Session): 62 | self._client = session.client("appconfigdata") 63 | else: 64 | self._client = boto3.client("appconfigdata") 65 | self._appconfig_profile = appconfig_profile 66 | self._appconfig_environment = appconfig_environment 67 | self._appconfig_application = appconfig_application 68 | if max_config_age < 15: 69 | raise ValueError("max_config_age must be at least 15 seconds") 70 | self._max_config_age = max_config_age 71 | self._last_update_time = 0.0 72 | self._config = None # type: Union[None, Dict[Any, Any], str, bytes] 73 | self._raw_config = None # type: Union[None, bytes] 74 | self._content_type = None # type: Union[None, str] 75 | self._fetch_on_read = fetch_on_read 76 | self._next_config_token = None # type: Optional[str] 77 | self._poll_interval = max_config_age 78 | self._version_label = None # type: Optional[str] 79 | if fetch_on_init: 80 | self.update_config() 81 | 82 | @property 83 | def appconfig_profile(self) -> str: 84 | """The profile in use.""" 85 | return self._appconfig_profile 86 | 87 | @property 88 | def appconfig_environment(self) -> str: 89 | """The environment in use.""" 90 | return self._appconfig_environment 91 | 92 | @property 93 | def appconfig_application(self) -> str: 94 | """The application in use.""" 95 | return self._appconfig_application 96 | 97 | @property 98 | def config(self) -> Union[None, Dict[Any, Any], str, bytes]: 99 | """The application configuration content. 100 | 101 | If initialsed with `fetch_on_read` = True, will attempt to update the 102 | config before returning it to you.""" 103 | if self._fetch_on_read: 104 | self.update_config() 105 | return self._config 106 | 107 | @property 108 | def raw_config(self) -> Union[None, bytes]: 109 | """The application configuration content retrieved from AppConfig. 110 | 111 | No processing is performed on this content. Accessing this property does not 112 | trigger an update, even if `fetch_on_read` is True.""" 113 | return self._raw_config 114 | 115 | @property 116 | def content_type(self) -> Union[None, str]: 117 | """The content type of the configuration retrieved from AppConfig.""" 118 | return self._content_type 119 | 120 | @property 121 | def version_label(self) -> Optional[str]: 122 | """The version label of the configuration retrieved from AppConfig.""" 123 | return self._version_label 124 | 125 | def start_session(self) -> None: 126 | """Start the config session and receive the next config token and poll interval""" 127 | response = self._client.start_configuration_session( 128 | ApplicationIdentifier=self._appconfig_application, 129 | ConfigurationProfileIdentifier=self._appconfig_profile, 130 | EnvironmentIdentifier=self._appconfig_environment, 131 | RequiredMinimumPollIntervalInSeconds=self._max_config_age, 132 | ) 133 | self._next_config_token = response["InitialConfigurationToken"] 134 | self._poll_interval = self._max_config_age 135 | 136 | def update_config(self, force_update: bool = False) -> bool: 137 | """Request the lastest configration. 138 | 139 | `force_update`: set to True to request configuration event if it's not time yet 140 | 141 | Returns True if a new version of configuration was received. False 142 | indicates that no attempt was made, or that no new version was found. 143 | """ 144 | if ( 145 | time.time() - self._last_update_time < self._poll_interval 146 | ) and not force_update: 147 | return False 148 | 149 | if self._next_config_token is None: 150 | self.start_session() 151 | 152 | try: 153 | response = self._client.get_latest_configuration( 154 | ConfigurationToken=self._next_config_token 155 | ) 156 | except botocore.exceptions.ClientError: 157 | self.start_session() 158 | response = self._client.get_latest_configuration( 159 | ConfigurationToken=self._next_config_token 160 | ) 161 | 162 | self._next_config_token = response["NextPollConfigurationToken"] 163 | self._poll_interval = int(response["NextPollIntervalInSeconds"]) 164 | 165 | content = response["Configuration"].read() # type: bytes 166 | if content == b"": 167 | self._last_update_time = time.time() 168 | return False 169 | 170 | if response["ContentType"] == "application/x-yaml": 171 | if not yaml_available: 172 | raise RuntimeError( 173 | "Configuration in YAML format received and missing " 174 | "yaml library; pip install pyyaml?" 175 | ) 176 | try: 177 | self._config = yaml.safe_load(content) 178 | except yaml.YAMLError as error: 179 | message = "Unable to parse YAML configuration data" 180 | if hasattr(error, "problem_mark"): 181 | message = ( 182 | f"{message} at line {error.problem_mark.line + 1} " 183 | f"column {error.problem_mark.column + 1}" 184 | ) 185 | raise ValueError(message) from error 186 | elif response["ContentType"] == "application/json": 187 | try: 188 | self._config = json.loads(content.decode("utf-8")) 189 | except json.JSONDecodeError as error: 190 | raise ValueError(error.msg) from error 191 | elif response["ContentType"] == "text/plain": 192 | self._config = content.decode("utf-8") 193 | else: 194 | self._config = content 195 | 196 | self._last_update_time = time.time() 197 | self._raw_config = content 198 | self._content_type = response["ContentType"] 199 | self._version_label = cast(Optional[str], response.get("VersionLabel")) 200 | return True 201 | -------------------------------------------------------------------------------- /appconfig_helper/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /appconfig_helper/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "2.0.2" 2 | -------------------------------------------------------------------------------- /example/appconfig-resources-stack.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Description: Launch AWS AppConfig resources for the sample Python AWS AppConfig helper example code 4 | 5 | Resources: 6 | Application: 7 | Type: AWS::AppConfig::Application 8 | Properties: 9 | Name: DemoApp 10 | Description: AppConfig helper demo application 11 | 12 | Environment: 13 | Type: AWS::AppConfig::Environment 14 | Properties: 15 | ApplicationId: !Ref Application 16 | Name: prod 17 | Description: AppConfig helper demo environment 18 | 19 | Profile: 20 | Type: AWS::AppConfig::ConfigurationProfile 21 | Properties: 22 | LocationUri: hosted 23 | ApplicationId: !Ref Application 24 | Name: main 25 | Description: AppConfig helper demo profile 26 | 27 | ConfigVersion: 28 | Type: AWS::AppConfig::HostedConfigurationVersion 29 | Properties: 30 | ConfigurationProfileId: !Ref Profile 31 | ContentType: application/json 32 | Content: | 33 | { 34 | "transform_reverse": true, 35 | "transform_allcaps": false 36 | } 37 | ApplicationId: !Ref Application 38 | 39 | DeploymentStrategy: 40 | Type: AWS::AppConfig::DeploymentStrategy 41 | Properties: 42 | ReplicateTo: NONE 43 | DeploymentDurationInMinutes: 0 44 | GrowthFactor: 100 45 | Name: AllAtOnceNoBake 46 | FinalBakeTimeInMinutes: 0 47 | 48 | Deployment: 49 | Type: AWS::AppConfig::Deployment 50 | Properties: 51 | DeploymentStrategyId: !Ref DeploymentStrategy 52 | ConfigurationProfileId: !Ref Profile 53 | EnvironmentId: !Ref Environment 54 | ConfigurationVersion: !Ref ConfigVersion 55 | ApplicationId: !Ref Application 56 | -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example application for AppConfigHelper 3 | 4 | Create the required AppConfig resources with the CloudFormation template. 5 | 6 | Run this file with `uvicorn main:app` 7 | """ 8 | 9 | from fastapi import FastAPI 10 | 11 | from appconfig_helper import AppConfigHelper 12 | 13 | app = FastAPI() 14 | 15 | appconfig = AppConfigHelper( 16 | "DemoApp", 17 | "prod", 18 | "main", 19 | 15, 20 | ) 21 | 22 | 23 | @app.get("/process-string/{input_string}") 24 | def index(input_string): 25 | if appconfig.update_config(): 26 | print("Received new configuration") 27 | output_string = input_string 28 | if appconfig.config.get("transform_reverse", True): 29 | output_string = "".join(reversed(output_string)) 30 | if appconfig.config.get("transform_allcaps", False): 31 | output_string = output_string.upper() 32 | 33 | return { 34 | "output": output_string, 35 | } 36 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | sample-helper-aws-appconfig 4 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "boto3" 5 | version = "1.33.13" 6 | description = "The AWS SDK for Python" 7 | optional = false 8 | python-versions = ">= 3.7" 9 | files = [ 10 | {file = "boto3-1.33.13-py3-none-any.whl", hash = "sha256:5f278b95fb2b32f3d09d950759a05664357ba35d81107bab1537c4ddd212cd8c"}, 11 | {file = "boto3-1.33.13.tar.gz", hash = "sha256:0e966b8a475ecb06cc0846304454b8da2473d4c8198a45dfb2c5304871986883"}, 12 | ] 13 | 14 | [package.dependencies] 15 | botocore = ">=1.33.13,<1.34.0" 16 | jmespath = ">=0.7.1,<2.0.0" 17 | s3transfer = ">=0.8.2,<0.9.0" 18 | 19 | [package.extras] 20 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 21 | 22 | [[package]] 23 | name = "boto3-stubs" 24 | version = "1.34.4" 25 | description = "Type annotations for boto3 1.34.4 generated with mypy-boto3-builder 7.22.0" 26 | optional = false 27 | python-versions = ">=3.7" 28 | files = [ 29 | {file = "boto3-stubs-1.34.4.tar.gz", hash = "sha256:70b3440feccc83995fd5132c0dcf1bc1bc40b3c49ccd5dc25ea2b05aba4f142f"}, 30 | {file = "boto3_stubs-1.34.4-py3-none-any.whl", hash = "sha256:0ef231923edfe40a3f0d07897a329d5d6a7c4f8edea2ce18c82319bdfeede18a"}, 31 | ] 32 | 33 | [package.dependencies] 34 | botocore-stubs = "*" 35 | types-s3transfer = "*" 36 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} 37 | 38 | [package.extras] 39 | accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.34.0,<1.35.0)"] 40 | account = ["mypy-boto3-account (>=1.34.0,<1.35.0)"] 41 | acm = ["mypy-boto3-acm (>=1.34.0,<1.35.0)"] 42 | acm-pca = ["mypy-boto3-acm-pca (>=1.34.0,<1.35.0)"] 43 | alexaforbusiness = ["mypy-boto3-alexaforbusiness (>=1.34.0,<1.35.0)"] 44 | all = ["mypy-boto3-accessanalyzer (>=1.34.0,<1.35.0)", "mypy-boto3-account (>=1.34.0,<1.35.0)", "mypy-boto3-acm (>=1.34.0,<1.35.0)", "mypy-boto3-acm-pca (>=1.34.0,<1.35.0)", "mypy-boto3-alexaforbusiness (>=1.34.0,<1.35.0)", "mypy-boto3-amp (>=1.34.0,<1.35.0)", "mypy-boto3-amplify (>=1.34.0,<1.35.0)", "mypy-boto3-amplifybackend (>=1.34.0,<1.35.0)", "mypy-boto3-amplifyuibuilder (>=1.34.0,<1.35.0)", "mypy-boto3-apigateway (>=1.34.0,<1.35.0)", "mypy-boto3-apigatewaymanagementapi (>=1.34.0,<1.35.0)", "mypy-boto3-apigatewayv2 (>=1.34.0,<1.35.0)", "mypy-boto3-appconfig (>=1.34.0,<1.35.0)", "mypy-boto3-appconfigdata (>=1.34.0,<1.35.0)", "mypy-boto3-appfabric (>=1.34.0,<1.35.0)", "mypy-boto3-appflow (>=1.34.0,<1.35.0)", "mypy-boto3-appintegrations (>=1.34.0,<1.35.0)", "mypy-boto3-application-autoscaling (>=1.34.0,<1.35.0)", "mypy-boto3-application-insights (>=1.34.0,<1.35.0)", "mypy-boto3-applicationcostprofiler (>=1.34.0,<1.35.0)", "mypy-boto3-appmesh (>=1.34.0,<1.35.0)", "mypy-boto3-apprunner (>=1.34.0,<1.35.0)", "mypy-boto3-appstream (>=1.34.0,<1.35.0)", "mypy-boto3-appsync (>=1.34.0,<1.35.0)", "mypy-boto3-arc-zonal-shift (>=1.34.0,<1.35.0)", "mypy-boto3-athena (>=1.34.0,<1.35.0)", "mypy-boto3-auditmanager (>=1.34.0,<1.35.0)", "mypy-boto3-autoscaling (>=1.34.0,<1.35.0)", "mypy-boto3-autoscaling-plans (>=1.34.0,<1.35.0)", "mypy-boto3-b2bi (>=1.34.0,<1.35.0)", "mypy-boto3-backup (>=1.34.0,<1.35.0)", "mypy-boto3-backup-gateway (>=1.34.0,<1.35.0)", "mypy-boto3-backupstorage (>=1.34.0,<1.35.0)", "mypy-boto3-batch (>=1.34.0,<1.35.0)", "mypy-boto3-bcm-data-exports (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-agent (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-agent-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-bedrock-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-billingconductor (>=1.34.0,<1.35.0)", "mypy-boto3-braket (>=1.34.0,<1.35.0)", "mypy-boto3-budgets (>=1.34.0,<1.35.0)", "mypy-boto3-ce (>=1.34.0,<1.35.0)", "mypy-boto3-chime (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-identity (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-meetings (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-messaging (>=1.34.0,<1.35.0)", "mypy-boto3-chime-sdk-voice (>=1.34.0,<1.35.0)", "mypy-boto3-cleanrooms (>=1.34.0,<1.35.0)", "mypy-boto3-cleanroomsml (>=1.34.0,<1.35.0)", "mypy-boto3-cloud9 (>=1.34.0,<1.35.0)", "mypy-boto3-cloudcontrol (>=1.34.0,<1.35.0)", "mypy-boto3-clouddirectory (>=1.34.0,<1.35.0)", "mypy-boto3-cloudformation (>=1.34.0,<1.35.0)", "mypy-boto3-cloudfront (>=1.34.0,<1.35.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.34.0,<1.35.0)", "mypy-boto3-cloudhsm (>=1.34.0,<1.35.0)", "mypy-boto3-cloudhsmv2 (>=1.34.0,<1.35.0)", "mypy-boto3-cloudsearch (>=1.34.0,<1.35.0)", "mypy-boto3-cloudsearchdomain (>=1.34.0,<1.35.0)", "mypy-boto3-cloudtrail (>=1.34.0,<1.35.0)", "mypy-boto3-cloudtrail-data (>=1.34.0,<1.35.0)", "mypy-boto3-cloudwatch (>=1.34.0,<1.35.0)", "mypy-boto3-codeartifact (>=1.34.0,<1.35.0)", "mypy-boto3-codebuild (>=1.34.0,<1.35.0)", "mypy-boto3-codecatalyst (>=1.34.0,<1.35.0)", "mypy-boto3-codecommit (>=1.34.0,<1.35.0)", "mypy-boto3-codedeploy (>=1.34.0,<1.35.0)", "mypy-boto3-codeguru-reviewer (>=1.34.0,<1.35.0)", "mypy-boto3-codeguru-security (>=1.34.0,<1.35.0)", "mypy-boto3-codeguruprofiler (>=1.34.0,<1.35.0)", "mypy-boto3-codepipeline (>=1.34.0,<1.35.0)", "mypy-boto3-codestar (>=1.34.0,<1.35.0)", "mypy-boto3-codestar-connections (>=1.34.0,<1.35.0)", "mypy-boto3-codestar-notifications (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-identity (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-idp (>=1.34.0,<1.35.0)", "mypy-boto3-cognito-sync (>=1.34.0,<1.35.0)", "mypy-boto3-comprehend (>=1.34.0,<1.35.0)", "mypy-boto3-comprehendmedical (>=1.34.0,<1.35.0)", "mypy-boto3-compute-optimizer (>=1.34.0,<1.35.0)", "mypy-boto3-config (>=1.34.0,<1.35.0)", "mypy-boto3-connect (>=1.34.0,<1.35.0)", "mypy-boto3-connect-contact-lens (>=1.34.0,<1.35.0)", "mypy-boto3-connectcampaigns (>=1.34.0,<1.35.0)", "mypy-boto3-connectcases (>=1.34.0,<1.35.0)", "mypy-boto3-connectparticipant (>=1.34.0,<1.35.0)", "mypy-boto3-controltower (>=1.34.0,<1.35.0)", "mypy-boto3-cost-optimization-hub (>=1.34.0,<1.35.0)", "mypy-boto3-cur (>=1.34.0,<1.35.0)", "mypy-boto3-customer-profiles (>=1.34.0,<1.35.0)", "mypy-boto3-databrew (>=1.34.0,<1.35.0)", "mypy-boto3-dataexchange (>=1.34.0,<1.35.0)", "mypy-boto3-datapipeline (>=1.34.0,<1.35.0)", "mypy-boto3-datasync (>=1.34.0,<1.35.0)", "mypy-boto3-datazone (>=1.34.0,<1.35.0)", "mypy-boto3-dax (>=1.34.0,<1.35.0)", "mypy-boto3-detective (>=1.34.0,<1.35.0)", "mypy-boto3-devicefarm (>=1.34.0,<1.35.0)", "mypy-boto3-devops-guru (>=1.34.0,<1.35.0)", "mypy-boto3-directconnect (>=1.34.0,<1.35.0)", "mypy-boto3-discovery (>=1.34.0,<1.35.0)", "mypy-boto3-dlm (>=1.34.0,<1.35.0)", "mypy-boto3-dms (>=1.34.0,<1.35.0)", "mypy-boto3-docdb (>=1.34.0,<1.35.0)", "mypy-boto3-docdb-elastic (>=1.34.0,<1.35.0)", "mypy-boto3-drs (>=1.34.0,<1.35.0)", "mypy-boto3-ds (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodb (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodbstreams (>=1.34.0,<1.35.0)", "mypy-boto3-ebs (>=1.34.0,<1.35.0)", "mypy-boto3-ec2 (>=1.34.0,<1.35.0)", "mypy-boto3-ec2-instance-connect (>=1.34.0,<1.35.0)", "mypy-boto3-ecr (>=1.34.0,<1.35.0)", "mypy-boto3-ecr-public (>=1.34.0,<1.35.0)", "mypy-boto3-ecs (>=1.34.0,<1.35.0)", "mypy-boto3-efs (>=1.34.0,<1.35.0)", "mypy-boto3-eks (>=1.34.0,<1.35.0)", "mypy-boto3-eks-auth (>=1.34.0,<1.35.0)", "mypy-boto3-elastic-inference (>=1.34.0,<1.35.0)", "mypy-boto3-elasticache (>=1.34.0,<1.35.0)", "mypy-boto3-elasticbeanstalk (>=1.34.0,<1.35.0)", "mypy-boto3-elastictranscoder (>=1.34.0,<1.35.0)", "mypy-boto3-elb (>=1.34.0,<1.35.0)", "mypy-boto3-elbv2 (>=1.34.0,<1.35.0)", "mypy-boto3-emr (>=1.34.0,<1.35.0)", "mypy-boto3-emr-containers (>=1.34.0,<1.35.0)", "mypy-boto3-emr-serverless (>=1.34.0,<1.35.0)", "mypy-boto3-entityresolution (>=1.34.0,<1.35.0)", "mypy-boto3-es (>=1.34.0,<1.35.0)", "mypy-boto3-events (>=1.34.0,<1.35.0)", "mypy-boto3-evidently (>=1.34.0,<1.35.0)", "mypy-boto3-finspace (>=1.34.0,<1.35.0)", "mypy-boto3-finspace-data (>=1.34.0,<1.35.0)", "mypy-boto3-firehose (>=1.34.0,<1.35.0)", "mypy-boto3-fis (>=1.34.0,<1.35.0)", "mypy-boto3-fms (>=1.34.0,<1.35.0)", "mypy-boto3-forecast (>=1.34.0,<1.35.0)", "mypy-boto3-forecastquery (>=1.34.0,<1.35.0)", "mypy-boto3-frauddetector (>=1.34.0,<1.35.0)", "mypy-boto3-freetier (>=1.34.0,<1.35.0)", "mypy-boto3-fsx (>=1.34.0,<1.35.0)", "mypy-boto3-gamelift (>=1.34.0,<1.35.0)", "mypy-boto3-glacier (>=1.34.0,<1.35.0)", "mypy-boto3-globalaccelerator (>=1.34.0,<1.35.0)", "mypy-boto3-glue (>=1.34.0,<1.35.0)", "mypy-boto3-grafana (>=1.34.0,<1.35.0)", "mypy-boto3-greengrass (>=1.34.0,<1.35.0)", "mypy-boto3-greengrassv2 (>=1.34.0,<1.35.0)", "mypy-boto3-groundstation (>=1.34.0,<1.35.0)", "mypy-boto3-guardduty (>=1.34.0,<1.35.0)", "mypy-boto3-health (>=1.34.0,<1.35.0)", "mypy-boto3-healthlake (>=1.34.0,<1.35.0)", "mypy-boto3-honeycode (>=1.34.0,<1.35.0)", "mypy-boto3-iam (>=1.34.0,<1.35.0)", "mypy-boto3-identitystore (>=1.34.0,<1.35.0)", "mypy-boto3-imagebuilder (>=1.34.0,<1.35.0)", "mypy-boto3-importexport (>=1.34.0,<1.35.0)", "mypy-boto3-inspector (>=1.34.0,<1.35.0)", "mypy-boto3-inspector-scan (>=1.34.0,<1.35.0)", "mypy-boto3-inspector2 (>=1.34.0,<1.35.0)", "mypy-boto3-internetmonitor (>=1.34.0,<1.35.0)", "mypy-boto3-iot (>=1.34.0,<1.35.0)", "mypy-boto3-iot-data (>=1.34.0,<1.35.0)", "mypy-boto3-iot-jobs-data (>=1.34.0,<1.35.0)", "mypy-boto3-iot-roborunner (>=1.34.0,<1.35.0)", "mypy-boto3-iot1click-devices (>=1.34.0,<1.35.0)", "mypy-boto3-iot1click-projects (>=1.34.0,<1.35.0)", "mypy-boto3-iotanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-iotdeviceadvisor (>=1.34.0,<1.35.0)", "mypy-boto3-iotevents (>=1.34.0,<1.35.0)", "mypy-boto3-iotevents-data (>=1.34.0,<1.35.0)", "mypy-boto3-iotfleethub (>=1.34.0,<1.35.0)", "mypy-boto3-iotfleetwise (>=1.34.0,<1.35.0)", "mypy-boto3-iotsecuretunneling (>=1.34.0,<1.35.0)", "mypy-boto3-iotsitewise (>=1.34.0,<1.35.0)", "mypy-boto3-iotthingsgraph (>=1.34.0,<1.35.0)", "mypy-boto3-iottwinmaker (>=1.34.0,<1.35.0)", "mypy-boto3-iotwireless (>=1.34.0,<1.35.0)", "mypy-boto3-ivs (>=1.34.0,<1.35.0)", "mypy-boto3-ivs-realtime (>=1.34.0,<1.35.0)", "mypy-boto3-ivschat (>=1.34.0,<1.35.0)", "mypy-boto3-kafka (>=1.34.0,<1.35.0)", "mypy-boto3-kafkaconnect (>=1.34.0,<1.35.0)", "mypy-boto3-kendra (>=1.34.0,<1.35.0)", "mypy-boto3-kendra-ranking (>=1.34.0,<1.35.0)", "mypy-boto3-keyspaces (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-archived-media (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-media (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-signaling (>=1.34.0,<1.35.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.34.0,<1.35.0)", "mypy-boto3-kinesisvideo (>=1.34.0,<1.35.0)", "mypy-boto3-kms (>=1.34.0,<1.35.0)", "mypy-boto3-lakeformation (>=1.34.0,<1.35.0)", "mypy-boto3-lambda (>=1.34.0,<1.35.0)", "mypy-boto3-launch-wizard (>=1.34.0,<1.35.0)", "mypy-boto3-lex-models (>=1.34.0,<1.35.0)", "mypy-boto3-lex-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-lexv2-models (>=1.34.0,<1.35.0)", "mypy-boto3-lexv2-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.34.0,<1.35.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.34.0,<1.35.0)", "mypy-boto3-lightsail (>=1.34.0,<1.35.0)", "mypy-boto3-location (>=1.34.0,<1.35.0)", "mypy-boto3-logs (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutequipment (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutmetrics (>=1.34.0,<1.35.0)", "mypy-boto3-lookoutvision (>=1.34.0,<1.35.0)", "mypy-boto3-m2 (>=1.34.0,<1.35.0)", "mypy-boto3-machinelearning (>=1.34.0,<1.35.0)", "mypy-boto3-macie2 (>=1.34.0,<1.35.0)", "mypy-boto3-managedblockchain (>=1.34.0,<1.35.0)", "mypy-boto3-managedblockchain-query (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-agreement (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-catalog (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-deployment (>=1.34.0,<1.35.0)", "mypy-boto3-marketplace-entitlement (>=1.34.0,<1.35.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.34.0,<1.35.0)", "mypy-boto3-mediaconnect (>=1.34.0,<1.35.0)", "mypy-boto3-mediaconvert (>=1.34.0,<1.35.0)", "mypy-boto3-medialive (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackage (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackage-vod (>=1.34.0,<1.35.0)", "mypy-boto3-mediapackagev2 (>=1.34.0,<1.35.0)", "mypy-boto3-mediastore (>=1.34.0,<1.35.0)", "mypy-boto3-mediastore-data (>=1.34.0,<1.35.0)", "mypy-boto3-mediatailor (>=1.34.0,<1.35.0)", "mypy-boto3-medical-imaging (>=1.34.0,<1.35.0)", "mypy-boto3-memorydb (>=1.34.0,<1.35.0)", "mypy-boto3-meteringmarketplace (>=1.34.0,<1.35.0)", "mypy-boto3-mgh (>=1.34.0,<1.35.0)", "mypy-boto3-mgn (>=1.34.0,<1.35.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhub-config (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhuborchestrator (>=1.34.0,<1.35.0)", "mypy-boto3-migrationhubstrategy (>=1.34.0,<1.35.0)", "mypy-boto3-mobile (>=1.34.0,<1.35.0)", "mypy-boto3-mq (>=1.34.0,<1.35.0)", "mypy-boto3-mturk (>=1.34.0,<1.35.0)", "mypy-boto3-mwaa (>=1.34.0,<1.35.0)", "mypy-boto3-neptune (>=1.34.0,<1.35.0)", "mypy-boto3-neptune-graph (>=1.34.0,<1.35.0)", "mypy-boto3-neptunedata (>=1.34.0,<1.35.0)", "mypy-boto3-network-firewall (>=1.34.0,<1.35.0)", "mypy-boto3-networkmanager (>=1.34.0,<1.35.0)", "mypy-boto3-nimble (>=1.34.0,<1.35.0)", "mypy-boto3-oam (>=1.34.0,<1.35.0)", "mypy-boto3-omics (>=1.34.0,<1.35.0)", "mypy-boto3-opensearch (>=1.34.0,<1.35.0)", "mypy-boto3-opensearchserverless (>=1.34.0,<1.35.0)", "mypy-boto3-opsworks (>=1.34.0,<1.35.0)", "mypy-boto3-opsworkscm (>=1.34.0,<1.35.0)", "mypy-boto3-organizations (>=1.34.0,<1.35.0)", "mypy-boto3-osis (>=1.34.0,<1.35.0)", "mypy-boto3-outposts (>=1.34.0,<1.35.0)", "mypy-boto3-panorama (>=1.34.0,<1.35.0)", "mypy-boto3-payment-cryptography (>=1.34.0,<1.35.0)", "mypy-boto3-payment-cryptography-data (>=1.34.0,<1.35.0)", "mypy-boto3-pca-connector-ad (>=1.34.0,<1.35.0)", "mypy-boto3-personalize (>=1.34.0,<1.35.0)", "mypy-boto3-personalize-events (>=1.34.0,<1.35.0)", "mypy-boto3-personalize-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-pi (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-email (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-sms-voice (>=1.34.0,<1.35.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.34.0,<1.35.0)", "mypy-boto3-pipes (>=1.34.0,<1.35.0)", "mypy-boto3-polly (>=1.34.0,<1.35.0)", "mypy-boto3-pricing (>=1.34.0,<1.35.0)", "mypy-boto3-privatenetworks (>=1.34.0,<1.35.0)", "mypy-boto3-proton (>=1.34.0,<1.35.0)", "mypy-boto3-qbusiness (>=1.34.0,<1.35.0)", "mypy-boto3-qconnect (>=1.34.0,<1.35.0)", "mypy-boto3-qldb (>=1.34.0,<1.35.0)", "mypy-boto3-qldb-session (>=1.34.0,<1.35.0)", "mypy-boto3-quicksight (>=1.34.0,<1.35.0)", "mypy-boto3-ram (>=1.34.0,<1.35.0)", "mypy-boto3-rbin (>=1.34.0,<1.35.0)", "mypy-boto3-rds (>=1.34.0,<1.35.0)", "mypy-boto3-rds-data (>=1.34.0,<1.35.0)", "mypy-boto3-redshift (>=1.34.0,<1.35.0)", "mypy-boto3-redshift-data (>=1.34.0,<1.35.0)", "mypy-boto3-redshift-serverless (>=1.34.0,<1.35.0)", "mypy-boto3-rekognition (>=1.34.0,<1.35.0)", "mypy-boto3-repostspace (>=1.34.0,<1.35.0)", "mypy-boto3-resiliencehub (>=1.34.0,<1.35.0)", "mypy-boto3-resource-explorer-2 (>=1.34.0,<1.35.0)", "mypy-boto3-resource-groups (>=1.34.0,<1.35.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.34.0,<1.35.0)", "mypy-boto3-robomaker (>=1.34.0,<1.35.0)", "mypy-boto3-rolesanywhere (>=1.34.0,<1.35.0)", "mypy-boto3-route53 (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-cluster (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-control-config (>=1.34.0,<1.35.0)", "mypy-boto3-route53-recovery-readiness (>=1.34.0,<1.35.0)", "mypy-boto3-route53domains (>=1.34.0,<1.35.0)", "mypy-boto3-route53resolver (>=1.34.0,<1.35.0)", "mypy-boto3-rum (>=1.34.0,<1.35.0)", "mypy-boto3-s3 (>=1.34.0,<1.35.0)", "mypy-boto3-s3control (>=1.34.0,<1.35.0)", "mypy-boto3-s3outposts (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-edge (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-geospatial (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-metrics (>=1.34.0,<1.35.0)", "mypy-boto3-sagemaker-runtime (>=1.34.0,<1.35.0)", "mypy-boto3-savingsplans (>=1.34.0,<1.35.0)", "mypy-boto3-scheduler (>=1.34.0,<1.35.0)", "mypy-boto3-schemas (>=1.34.0,<1.35.0)", "mypy-boto3-sdb (>=1.34.0,<1.35.0)", "mypy-boto3-secretsmanager (>=1.34.0,<1.35.0)", "mypy-boto3-securityhub (>=1.34.0,<1.35.0)", "mypy-boto3-securitylake (>=1.34.0,<1.35.0)", "mypy-boto3-serverlessrepo (>=1.34.0,<1.35.0)", "mypy-boto3-service-quotas (>=1.34.0,<1.35.0)", "mypy-boto3-servicecatalog (>=1.34.0,<1.35.0)", "mypy-boto3-servicecatalog-appregistry (>=1.34.0,<1.35.0)", "mypy-boto3-servicediscovery (>=1.34.0,<1.35.0)", "mypy-boto3-ses (>=1.34.0,<1.35.0)", "mypy-boto3-sesv2 (>=1.34.0,<1.35.0)", "mypy-boto3-shield (>=1.34.0,<1.35.0)", "mypy-boto3-signer (>=1.34.0,<1.35.0)", "mypy-boto3-simspaceweaver (>=1.34.0,<1.35.0)", "mypy-boto3-sms (>=1.34.0,<1.35.0)", "mypy-boto3-sms-voice (>=1.34.0,<1.35.0)", "mypy-boto3-snow-device-management (>=1.34.0,<1.35.0)", "mypy-boto3-snowball (>=1.34.0,<1.35.0)", "mypy-boto3-sns (>=1.34.0,<1.35.0)", "mypy-boto3-sqs (>=1.34.0,<1.35.0)", "mypy-boto3-ssm (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-contacts (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-incidents (>=1.34.0,<1.35.0)", "mypy-boto3-ssm-sap (>=1.34.0,<1.35.0)", "mypy-boto3-sso (>=1.34.0,<1.35.0)", "mypy-boto3-sso-admin (>=1.34.0,<1.35.0)", "mypy-boto3-sso-oidc (>=1.34.0,<1.35.0)", "mypy-boto3-stepfunctions (>=1.34.0,<1.35.0)", "mypy-boto3-storagegateway (>=1.34.0,<1.35.0)", "mypy-boto3-sts (>=1.34.0,<1.35.0)", "mypy-boto3-support (>=1.34.0,<1.35.0)", "mypy-boto3-support-app (>=1.34.0,<1.35.0)", "mypy-boto3-swf (>=1.34.0,<1.35.0)", "mypy-boto3-synthetics (>=1.34.0,<1.35.0)", "mypy-boto3-textract (>=1.34.0,<1.35.0)", "mypy-boto3-timestream-query (>=1.34.0,<1.35.0)", "mypy-boto3-timestream-write (>=1.34.0,<1.35.0)", "mypy-boto3-tnb (>=1.34.0,<1.35.0)", "mypy-boto3-transcribe (>=1.34.0,<1.35.0)", "mypy-boto3-transfer (>=1.34.0,<1.35.0)", "mypy-boto3-translate (>=1.34.0,<1.35.0)", "mypy-boto3-trustedadvisor (>=1.34.0,<1.35.0)", "mypy-boto3-verifiedpermissions (>=1.34.0,<1.35.0)", "mypy-boto3-voice-id (>=1.34.0,<1.35.0)", "mypy-boto3-vpc-lattice (>=1.34.0,<1.35.0)", "mypy-boto3-waf (>=1.34.0,<1.35.0)", "mypy-boto3-waf-regional (>=1.34.0,<1.35.0)", "mypy-boto3-wafv2 (>=1.34.0,<1.35.0)", "mypy-boto3-wellarchitected (>=1.34.0,<1.35.0)", "mypy-boto3-wisdom (>=1.34.0,<1.35.0)", "mypy-boto3-workdocs (>=1.34.0,<1.35.0)", "mypy-boto3-worklink (>=1.34.0,<1.35.0)", "mypy-boto3-workmail (>=1.34.0,<1.35.0)", "mypy-boto3-workmailmessageflow (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces-thin-client (>=1.34.0,<1.35.0)", "mypy-boto3-workspaces-web (>=1.34.0,<1.35.0)", "mypy-boto3-xray (>=1.34.0,<1.35.0)"] 45 | amp = ["mypy-boto3-amp (>=1.34.0,<1.35.0)"] 46 | amplify = ["mypy-boto3-amplify (>=1.34.0,<1.35.0)"] 47 | amplifybackend = ["mypy-boto3-amplifybackend (>=1.34.0,<1.35.0)"] 48 | amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.34.0,<1.35.0)"] 49 | apigateway = ["mypy-boto3-apigateway (>=1.34.0,<1.35.0)"] 50 | apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.34.0,<1.35.0)"] 51 | apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.34.0,<1.35.0)"] 52 | appconfig = ["mypy-boto3-appconfig (>=1.34.0,<1.35.0)"] 53 | appconfigdata = ["mypy-boto3-appconfigdata (>=1.34.0,<1.35.0)"] 54 | appfabric = ["mypy-boto3-appfabric (>=1.34.0,<1.35.0)"] 55 | appflow = ["mypy-boto3-appflow (>=1.34.0,<1.35.0)"] 56 | appintegrations = ["mypy-boto3-appintegrations (>=1.34.0,<1.35.0)"] 57 | application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.34.0,<1.35.0)"] 58 | application-insights = ["mypy-boto3-application-insights (>=1.34.0,<1.35.0)"] 59 | applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.34.0,<1.35.0)"] 60 | appmesh = ["mypy-boto3-appmesh (>=1.34.0,<1.35.0)"] 61 | apprunner = ["mypy-boto3-apprunner (>=1.34.0,<1.35.0)"] 62 | appstream = ["mypy-boto3-appstream (>=1.34.0,<1.35.0)"] 63 | appsync = ["mypy-boto3-appsync (>=1.34.0,<1.35.0)"] 64 | arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.34.0,<1.35.0)"] 65 | athena = ["mypy-boto3-athena (>=1.34.0,<1.35.0)"] 66 | auditmanager = ["mypy-boto3-auditmanager (>=1.34.0,<1.35.0)"] 67 | autoscaling = ["mypy-boto3-autoscaling (>=1.34.0,<1.35.0)"] 68 | autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.34.0,<1.35.0)"] 69 | b2bi = ["mypy-boto3-b2bi (>=1.34.0,<1.35.0)"] 70 | backup = ["mypy-boto3-backup (>=1.34.0,<1.35.0)"] 71 | backup-gateway = ["mypy-boto3-backup-gateway (>=1.34.0,<1.35.0)"] 72 | backupstorage = ["mypy-boto3-backupstorage (>=1.34.0,<1.35.0)"] 73 | batch = ["mypy-boto3-batch (>=1.34.0,<1.35.0)"] 74 | bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.34.0,<1.35.0)"] 75 | bedrock = ["mypy-boto3-bedrock (>=1.34.0,<1.35.0)"] 76 | bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.34.0,<1.35.0)"] 77 | bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.34.0,<1.35.0)"] 78 | bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.34.0,<1.35.0)"] 79 | billingconductor = ["mypy-boto3-billingconductor (>=1.34.0,<1.35.0)"] 80 | boto3 = ["boto3 (==1.34.4)", "botocore (==1.34.4)"] 81 | braket = ["mypy-boto3-braket (>=1.34.0,<1.35.0)"] 82 | budgets = ["mypy-boto3-budgets (>=1.34.0,<1.35.0)"] 83 | ce = ["mypy-boto3-ce (>=1.34.0,<1.35.0)"] 84 | chime = ["mypy-boto3-chime (>=1.34.0,<1.35.0)"] 85 | chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.34.0,<1.35.0)"] 86 | chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.34.0,<1.35.0)"] 87 | chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.34.0,<1.35.0)"] 88 | chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.34.0,<1.35.0)"] 89 | chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.34.0,<1.35.0)"] 90 | cleanrooms = ["mypy-boto3-cleanrooms (>=1.34.0,<1.35.0)"] 91 | cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.34.0,<1.35.0)"] 92 | cloud9 = ["mypy-boto3-cloud9 (>=1.34.0,<1.35.0)"] 93 | cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.34.0,<1.35.0)"] 94 | clouddirectory = ["mypy-boto3-clouddirectory (>=1.34.0,<1.35.0)"] 95 | cloudformation = ["mypy-boto3-cloudformation (>=1.34.0,<1.35.0)"] 96 | cloudfront = ["mypy-boto3-cloudfront (>=1.34.0,<1.35.0)"] 97 | cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.34.0,<1.35.0)"] 98 | cloudhsm = ["mypy-boto3-cloudhsm (>=1.34.0,<1.35.0)"] 99 | cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.34.0,<1.35.0)"] 100 | cloudsearch = ["mypy-boto3-cloudsearch (>=1.34.0,<1.35.0)"] 101 | cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.34.0,<1.35.0)"] 102 | cloudtrail = ["mypy-boto3-cloudtrail (>=1.34.0,<1.35.0)"] 103 | cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.34.0,<1.35.0)"] 104 | cloudwatch = ["mypy-boto3-cloudwatch (>=1.34.0,<1.35.0)"] 105 | codeartifact = ["mypy-boto3-codeartifact (>=1.34.0,<1.35.0)"] 106 | codebuild = ["mypy-boto3-codebuild (>=1.34.0,<1.35.0)"] 107 | codecatalyst = ["mypy-boto3-codecatalyst (>=1.34.0,<1.35.0)"] 108 | codecommit = ["mypy-boto3-codecommit (>=1.34.0,<1.35.0)"] 109 | codedeploy = ["mypy-boto3-codedeploy (>=1.34.0,<1.35.0)"] 110 | codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.34.0,<1.35.0)"] 111 | codeguru-security = ["mypy-boto3-codeguru-security (>=1.34.0,<1.35.0)"] 112 | codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.34.0,<1.35.0)"] 113 | codepipeline = ["mypy-boto3-codepipeline (>=1.34.0,<1.35.0)"] 114 | codestar = ["mypy-boto3-codestar (>=1.34.0,<1.35.0)"] 115 | codestar-connections = ["mypy-boto3-codestar-connections (>=1.34.0,<1.35.0)"] 116 | codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.34.0,<1.35.0)"] 117 | cognito-identity = ["mypy-boto3-cognito-identity (>=1.34.0,<1.35.0)"] 118 | cognito-idp = ["mypy-boto3-cognito-idp (>=1.34.0,<1.35.0)"] 119 | cognito-sync = ["mypy-boto3-cognito-sync (>=1.34.0,<1.35.0)"] 120 | comprehend = ["mypy-boto3-comprehend (>=1.34.0,<1.35.0)"] 121 | comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.34.0,<1.35.0)"] 122 | compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.34.0,<1.35.0)"] 123 | config = ["mypy-boto3-config (>=1.34.0,<1.35.0)"] 124 | connect = ["mypy-boto3-connect (>=1.34.0,<1.35.0)"] 125 | connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.34.0,<1.35.0)"] 126 | connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.34.0,<1.35.0)"] 127 | connectcases = ["mypy-boto3-connectcases (>=1.34.0,<1.35.0)"] 128 | connectparticipant = ["mypy-boto3-connectparticipant (>=1.34.0,<1.35.0)"] 129 | controltower = ["mypy-boto3-controltower (>=1.34.0,<1.35.0)"] 130 | cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.34.0,<1.35.0)"] 131 | cur = ["mypy-boto3-cur (>=1.34.0,<1.35.0)"] 132 | customer-profiles = ["mypy-boto3-customer-profiles (>=1.34.0,<1.35.0)"] 133 | databrew = ["mypy-boto3-databrew (>=1.34.0,<1.35.0)"] 134 | dataexchange = ["mypy-boto3-dataexchange (>=1.34.0,<1.35.0)"] 135 | datapipeline = ["mypy-boto3-datapipeline (>=1.34.0,<1.35.0)"] 136 | datasync = ["mypy-boto3-datasync (>=1.34.0,<1.35.0)"] 137 | datazone = ["mypy-boto3-datazone (>=1.34.0,<1.35.0)"] 138 | dax = ["mypy-boto3-dax (>=1.34.0,<1.35.0)"] 139 | detective = ["mypy-boto3-detective (>=1.34.0,<1.35.0)"] 140 | devicefarm = ["mypy-boto3-devicefarm (>=1.34.0,<1.35.0)"] 141 | devops-guru = ["mypy-boto3-devops-guru (>=1.34.0,<1.35.0)"] 142 | directconnect = ["mypy-boto3-directconnect (>=1.34.0,<1.35.0)"] 143 | discovery = ["mypy-boto3-discovery (>=1.34.0,<1.35.0)"] 144 | dlm = ["mypy-boto3-dlm (>=1.34.0,<1.35.0)"] 145 | dms = ["mypy-boto3-dms (>=1.34.0,<1.35.0)"] 146 | docdb = ["mypy-boto3-docdb (>=1.34.0,<1.35.0)"] 147 | docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.34.0,<1.35.0)"] 148 | drs = ["mypy-boto3-drs (>=1.34.0,<1.35.0)"] 149 | ds = ["mypy-boto3-ds (>=1.34.0,<1.35.0)"] 150 | dynamodb = ["mypy-boto3-dynamodb (>=1.34.0,<1.35.0)"] 151 | dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.34.0,<1.35.0)"] 152 | ebs = ["mypy-boto3-ebs (>=1.34.0,<1.35.0)"] 153 | ec2 = ["mypy-boto3-ec2 (>=1.34.0,<1.35.0)"] 154 | ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.34.0,<1.35.0)"] 155 | ecr = ["mypy-boto3-ecr (>=1.34.0,<1.35.0)"] 156 | ecr-public = ["mypy-boto3-ecr-public (>=1.34.0,<1.35.0)"] 157 | ecs = ["mypy-boto3-ecs (>=1.34.0,<1.35.0)"] 158 | efs = ["mypy-boto3-efs (>=1.34.0,<1.35.0)"] 159 | eks = ["mypy-boto3-eks (>=1.34.0,<1.35.0)"] 160 | eks-auth = ["mypy-boto3-eks-auth (>=1.34.0,<1.35.0)"] 161 | elastic-inference = ["mypy-boto3-elastic-inference (>=1.34.0,<1.35.0)"] 162 | elasticache = ["mypy-boto3-elasticache (>=1.34.0,<1.35.0)"] 163 | elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.34.0,<1.35.0)"] 164 | elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.34.0,<1.35.0)"] 165 | elb = ["mypy-boto3-elb (>=1.34.0,<1.35.0)"] 166 | elbv2 = ["mypy-boto3-elbv2 (>=1.34.0,<1.35.0)"] 167 | emr = ["mypy-boto3-emr (>=1.34.0,<1.35.0)"] 168 | emr-containers = ["mypy-boto3-emr-containers (>=1.34.0,<1.35.0)"] 169 | emr-serverless = ["mypy-boto3-emr-serverless (>=1.34.0,<1.35.0)"] 170 | entityresolution = ["mypy-boto3-entityresolution (>=1.34.0,<1.35.0)"] 171 | es = ["mypy-boto3-es (>=1.34.0,<1.35.0)"] 172 | essential = ["mypy-boto3-cloudformation (>=1.34.0,<1.35.0)", "mypy-boto3-dynamodb (>=1.34.0,<1.35.0)", "mypy-boto3-ec2 (>=1.34.0,<1.35.0)", "mypy-boto3-lambda (>=1.34.0,<1.35.0)", "mypy-boto3-rds (>=1.34.0,<1.35.0)", "mypy-boto3-s3 (>=1.34.0,<1.35.0)", "mypy-boto3-sqs (>=1.34.0,<1.35.0)"] 173 | events = ["mypy-boto3-events (>=1.34.0,<1.35.0)"] 174 | evidently = ["mypy-boto3-evidently (>=1.34.0,<1.35.0)"] 175 | finspace = ["mypy-boto3-finspace (>=1.34.0,<1.35.0)"] 176 | finspace-data = ["mypy-boto3-finspace-data (>=1.34.0,<1.35.0)"] 177 | firehose = ["mypy-boto3-firehose (>=1.34.0,<1.35.0)"] 178 | fis = ["mypy-boto3-fis (>=1.34.0,<1.35.0)"] 179 | fms = ["mypy-boto3-fms (>=1.34.0,<1.35.0)"] 180 | forecast = ["mypy-boto3-forecast (>=1.34.0,<1.35.0)"] 181 | forecastquery = ["mypy-boto3-forecastquery (>=1.34.0,<1.35.0)"] 182 | frauddetector = ["mypy-boto3-frauddetector (>=1.34.0,<1.35.0)"] 183 | freetier = ["mypy-boto3-freetier (>=1.34.0,<1.35.0)"] 184 | fsx = ["mypy-boto3-fsx (>=1.34.0,<1.35.0)"] 185 | gamelift = ["mypy-boto3-gamelift (>=1.34.0,<1.35.0)"] 186 | glacier = ["mypy-boto3-glacier (>=1.34.0,<1.35.0)"] 187 | globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.34.0,<1.35.0)"] 188 | glue = ["mypy-boto3-glue (>=1.34.0,<1.35.0)"] 189 | grafana = ["mypy-boto3-grafana (>=1.34.0,<1.35.0)"] 190 | greengrass = ["mypy-boto3-greengrass (>=1.34.0,<1.35.0)"] 191 | greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.34.0,<1.35.0)"] 192 | groundstation = ["mypy-boto3-groundstation (>=1.34.0,<1.35.0)"] 193 | guardduty = ["mypy-boto3-guardduty (>=1.34.0,<1.35.0)"] 194 | health = ["mypy-boto3-health (>=1.34.0,<1.35.0)"] 195 | healthlake = ["mypy-boto3-healthlake (>=1.34.0,<1.35.0)"] 196 | honeycode = ["mypy-boto3-honeycode (>=1.34.0,<1.35.0)"] 197 | iam = ["mypy-boto3-iam (>=1.34.0,<1.35.0)"] 198 | identitystore = ["mypy-boto3-identitystore (>=1.34.0,<1.35.0)"] 199 | imagebuilder = ["mypy-boto3-imagebuilder (>=1.34.0,<1.35.0)"] 200 | importexport = ["mypy-boto3-importexport (>=1.34.0,<1.35.0)"] 201 | inspector = ["mypy-boto3-inspector (>=1.34.0,<1.35.0)"] 202 | inspector-scan = ["mypy-boto3-inspector-scan (>=1.34.0,<1.35.0)"] 203 | inspector2 = ["mypy-boto3-inspector2 (>=1.34.0,<1.35.0)"] 204 | internetmonitor = ["mypy-boto3-internetmonitor (>=1.34.0,<1.35.0)"] 205 | iot = ["mypy-boto3-iot (>=1.34.0,<1.35.0)"] 206 | iot-data = ["mypy-boto3-iot-data (>=1.34.0,<1.35.0)"] 207 | iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.34.0,<1.35.0)"] 208 | iot-roborunner = ["mypy-boto3-iot-roborunner (>=1.34.0,<1.35.0)"] 209 | iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.34.0,<1.35.0)"] 210 | iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.34.0,<1.35.0)"] 211 | iotanalytics = ["mypy-boto3-iotanalytics (>=1.34.0,<1.35.0)"] 212 | iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.34.0,<1.35.0)"] 213 | iotevents = ["mypy-boto3-iotevents (>=1.34.0,<1.35.0)"] 214 | iotevents-data = ["mypy-boto3-iotevents-data (>=1.34.0,<1.35.0)"] 215 | iotfleethub = ["mypy-boto3-iotfleethub (>=1.34.0,<1.35.0)"] 216 | iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.34.0,<1.35.0)"] 217 | iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.34.0,<1.35.0)"] 218 | iotsitewise = ["mypy-boto3-iotsitewise (>=1.34.0,<1.35.0)"] 219 | iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.34.0,<1.35.0)"] 220 | iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.34.0,<1.35.0)"] 221 | iotwireless = ["mypy-boto3-iotwireless (>=1.34.0,<1.35.0)"] 222 | ivs = ["mypy-boto3-ivs (>=1.34.0,<1.35.0)"] 223 | ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.34.0,<1.35.0)"] 224 | ivschat = ["mypy-boto3-ivschat (>=1.34.0,<1.35.0)"] 225 | kafka = ["mypy-boto3-kafka (>=1.34.0,<1.35.0)"] 226 | kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.34.0,<1.35.0)"] 227 | kendra = ["mypy-boto3-kendra (>=1.34.0,<1.35.0)"] 228 | kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.34.0,<1.35.0)"] 229 | keyspaces = ["mypy-boto3-keyspaces (>=1.34.0,<1.35.0)"] 230 | kinesis = ["mypy-boto3-kinesis (>=1.34.0,<1.35.0)"] 231 | kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.34.0,<1.35.0)"] 232 | kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.34.0,<1.35.0)"] 233 | kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.34.0,<1.35.0)"] 234 | kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.34.0,<1.35.0)"] 235 | kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.34.0,<1.35.0)"] 236 | kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.34.0,<1.35.0)"] 237 | kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.34.0,<1.35.0)"] 238 | kms = ["mypy-boto3-kms (>=1.34.0,<1.35.0)"] 239 | lakeformation = ["mypy-boto3-lakeformation (>=1.34.0,<1.35.0)"] 240 | lambda = ["mypy-boto3-lambda (>=1.34.0,<1.35.0)"] 241 | launch-wizard = ["mypy-boto3-launch-wizard (>=1.34.0,<1.35.0)"] 242 | lex-models = ["mypy-boto3-lex-models (>=1.34.0,<1.35.0)"] 243 | lex-runtime = ["mypy-boto3-lex-runtime (>=1.34.0,<1.35.0)"] 244 | lexv2-models = ["mypy-boto3-lexv2-models (>=1.34.0,<1.35.0)"] 245 | lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.34.0,<1.35.0)"] 246 | license-manager = ["mypy-boto3-license-manager (>=1.34.0,<1.35.0)"] 247 | license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.34.0,<1.35.0)"] 248 | license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.34.0,<1.35.0)"] 249 | lightsail = ["mypy-boto3-lightsail (>=1.34.0,<1.35.0)"] 250 | location = ["mypy-boto3-location (>=1.34.0,<1.35.0)"] 251 | logs = ["mypy-boto3-logs (>=1.34.0,<1.35.0)"] 252 | lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.34.0,<1.35.0)"] 253 | lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.34.0,<1.35.0)"] 254 | lookoutvision = ["mypy-boto3-lookoutvision (>=1.34.0,<1.35.0)"] 255 | m2 = ["mypy-boto3-m2 (>=1.34.0,<1.35.0)"] 256 | machinelearning = ["mypy-boto3-machinelearning (>=1.34.0,<1.35.0)"] 257 | macie2 = ["mypy-boto3-macie2 (>=1.34.0,<1.35.0)"] 258 | managedblockchain = ["mypy-boto3-managedblockchain (>=1.34.0,<1.35.0)"] 259 | managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.34.0,<1.35.0)"] 260 | marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.34.0,<1.35.0)"] 261 | marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.34.0,<1.35.0)"] 262 | marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.34.0,<1.35.0)"] 263 | marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.34.0,<1.35.0)"] 264 | marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.34.0,<1.35.0)"] 265 | mediaconnect = ["mypy-boto3-mediaconnect (>=1.34.0,<1.35.0)"] 266 | mediaconvert = ["mypy-boto3-mediaconvert (>=1.34.0,<1.35.0)"] 267 | medialive = ["mypy-boto3-medialive (>=1.34.0,<1.35.0)"] 268 | mediapackage = ["mypy-boto3-mediapackage (>=1.34.0,<1.35.0)"] 269 | mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.34.0,<1.35.0)"] 270 | mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.34.0,<1.35.0)"] 271 | mediastore = ["mypy-boto3-mediastore (>=1.34.0,<1.35.0)"] 272 | mediastore-data = ["mypy-boto3-mediastore-data (>=1.34.0,<1.35.0)"] 273 | mediatailor = ["mypy-boto3-mediatailor (>=1.34.0,<1.35.0)"] 274 | medical-imaging = ["mypy-boto3-medical-imaging (>=1.34.0,<1.35.0)"] 275 | memorydb = ["mypy-boto3-memorydb (>=1.34.0,<1.35.0)"] 276 | meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.34.0,<1.35.0)"] 277 | mgh = ["mypy-boto3-mgh (>=1.34.0,<1.35.0)"] 278 | mgn = ["mypy-boto3-mgn (>=1.34.0,<1.35.0)"] 279 | migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.34.0,<1.35.0)"] 280 | migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.34.0,<1.35.0)"] 281 | migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.34.0,<1.35.0)"] 282 | migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.34.0,<1.35.0)"] 283 | mobile = ["mypy-boto3-mobile (>=1.34.0,<1.35.0)"] 284 | mq = ["mypy-boto3-mq (>=1.34.0,<1.35.0)"] 285 | mturk = ["mypy-boto3-mturk (>=1.34.0,<1.35.0)"] 286 | mwaa = ["mypy-boto3-mwaa (>=1.34.0,<1.35.0)"] 287 | neptune = ["mypy-boto3-neptune (>=1.34.0,<1.35.0)"] 288 | neptune-graph = ["mypy-boto3-neptune-graph (>=1.34.0,<1.35.0)"] 289 | neptunedata = ["mypy-boto3-neptunedata (>=1.34.0,<1.35.0)"] 290 | network-firewall = ["mypy-boto3-network-firewall (>=1.34.0,<1.35.0)"] 291 | networkmanager = ["mypy-boto3-networkmanager (>=1.34.0,<1.35.0)"] 292 | nimble = ["mypy-boto3-nimble (>=1.34.0,<1.35.0)"] 293 | oam = ["mypy-boto3-oam (>=1.34.0,<1.35.0)"] 294 | omics = ["mypy-boto3-omics (>=1.34.0,<1.35.0)"] 295 | opensearch = ["mypy-boto3-opensearch (>=1.34.0,<1.35.0)"] 296 | opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.34.0,<1.35.0)"] 297 | opsworks = ["mypy-boto3-opsworks (>=1.34.0,<1.35.0)"] 298 | opsworkscm = ["mypy-boto3-opsworkscm (>=1.34.0,<1.35.0)"] 299 | organizations = ["mypy-boto3-organizations (>=1.34.0,<1.35.0)"] 300 | osis = ["mypy-boto3-osis (>=1.34.0,<1.35.0)"] 301 | outposts = ["mypy-boto3-outposts (>=1.34.0,<1.35.0)"] 302 | panorama = ["mypy-boto3-panorama (>=1.34.0,<1.35.0)"] 303 | payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.34.0,<1.35.0)"] 304 | payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.34.0,<1.35.0)"] 305 | pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.34.0,<1.35.0)"] 306 | personalize = ["mypy-boto3-personalize (>=1.34.0,<1.35.0)"] 307 | personalize-events = ["mypy-boto3-personalize-events (>=1.34.0,<1.35.0)"] 308 | personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.34.0,<1.35.0)"] 309 | pi = ["mypy-boto3-pi (>=1.34.0,<1.35.0)"] 310 | pinpoint = ["mypy-boto3-pinpoint (>=1.34.0,<1.35.0)"] 311 | pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.34.0,<1.35.0)"] 312 | pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.34.0,<1.35.0)"] 313 | pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.34.0,<1.35.0)"] 314 | pipes = ["mypy-boto3-pipes (>=1.34.0,<1.35.0)"] 315 | polly = ["mypy-boto3-polly (>=1.34.0,<1.35.0)"] 316 | pricing = ["mypy-boto3-pricing (>=1.34.0,<1.35.0)"] 317 | privatenetworks = ["mypy-boto3-privatenetworks (>=1.34.0,<1.35.0)"] 318 | proton = ["mypy-boto3-proton (>=1.34.0,<1.35.0)"] 319 | qbusiness = ["mypy-boto3-qbusiness (>=1.34.0,<1.35.0)"] 320 | qconnect = ["mypy-boto3-qconnect (>=1.34.0,<1.35.0)"] 321 | qldb = ["mypy-boto3-qldb (>=1.34.0,<1.35.0)"] 322 | qldb-session = ["mypy-boto3-qldb-session (>=1.34.0,<1.35.0)"] 323 | quicksight = ["mypy-boto3-quicksight (>=1.34.0,<1.35.0)"] 324 | ram = ["mypy-boto3-ram (>=1.34.0,<1.35.0)"] 325 | rbin = ["mypy-boto3-rbin (>=1.34.0,<1.35.0)"] 326 | rds = ["mypy-boto3-rds (>=1.34.0,<1.35.0)"] 327 | rds-data = ["mypy-boto3-rds-data (>=1.34.0,<1.35.0)"] 328 | redshift = ["mypy-boto3-redshift (>=1.34.0,<1.35.0)"] 329 | redshift-data = ["mypy-boto3-redshift-data (>=1.34.0,<1.35.0)"] 330 | redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.34.0,<1.35.0)"] 331 | rekognition = ["mypy-boto3-rekognition (>=1.34.0,<1.35.0)"] 332 | repostspace = ["mypy-boto3-repostspace (>=1.34.0,<1.35.0)"] 333 | resiliencehub = ["mypy-boto3-resiliencehub (>=1.34.0,<1.35.0)"] 334 | resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.34.0,<1.35.0)"] 335 | resource-groups = ["mypy-boto3-resource-groups (>=1.34.0,<1.35.0)"] 336 | resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.34.0,<1.35.0)"] 337 | robomaker = ["mypy-boto3-robomaker (>=1.34.0,<1.35.0)"] 338 | rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.34.0,<1.35.0)"] 339 | route53 = ["mypy-boto3-route53 (>=1.34.0,<1.35.0)"] 340 | route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.34.0,<1.35.0)"] 341 | route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.34.0,<1.35.0)"] 342 | route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.34.0,<1.35.0)"] 343 | route53domains = ["mypy-boto3-route53domains (>=1.34.0,<1.35.0)"] 344 | route53resolver = ["mypy-boto3-route53resolver (>=1.34.0,<1.35.0)"] 345 | rum = ["mypy-boto3-rum (>=1.34.0,<1.35.0)"] 346 | s3 = ["mypy-boto3-s3 (>=1.34.0,<1.35.0)"] 347 | s3control = ["mypy-boto3-s3control (>=1.34.0,<1.35.0)"] 348 | s3outposts = ["mypy-boto3-s3outposts (>=1.34.0,<1.35.0)"] 349 | sagemaker = ["mypy-boto3-sagemaker (>=1.34.0,<1.35.0)"] 350 | sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.34.0,<1.35.0)"] 351 | sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.34.0,<1.35.0)"] 352 | sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.34.0,<1.35.0)"] 353 | sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.34.0,<1.35.0)"] 354 | sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.34.0,<1.35.0)"] 355 | sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.34.0,<1.35.0)"] 356 | savingsplans = ["mypy-boto3-savingsplans (>=1.34.0,<1.35.0)"] 357 | scheduler = ["mypy-boto3-scheduler (>=1.34.0,<1.35.0)"] 358 | schemas = ["mypy-boto3-schemas (>=1.34.0,<1.35.0)"] 359 | sdb = ["mypy-boto3-sdb (>=1.34.0,<1.35.0)"] 360 | secretsmanager = ["mypy-boto3-secretsmanager (>=1.34.0,<1.35.0)"] 361 | securityhub = ["mypy-boto3-securityhub (>=1.34.0,<1.35.0)"] 362 | securitylake = ["mypy-boto3-securitylake (>=1.34.0,<1.35.0)"] 363 | serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.34.0,<1.35.0)"] 364 | service-quotas = ["mypy-boto3-service-quotas (>=1.34.0,<1.35.0)"] 365 | servicecatalog = ["mypy-boto3-servicecatalog (>=1.34.0,<1.35.0)"] 366 | servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.34.0,<1.35.0)"] 367 | servicediscovery = ["mypy-boto3-servicediscovery (>=1.34.0,<1.35.0)"] 368 | ses = ["mypy-boto3-ses (>=1.34.0,<1.35.0)"] 369 | sesv2 = ["mypy-boto3-sesv2 (>=1.34.0,<1.35.0)"] 370 | shield = ["mypy-boto3-shield (>=1.34.0,<1.35.0)"] 371 | signer = ["mypy-boto3-signer (>=1.34.0,<1.35.0)"] 372 | simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.34.0,<1.35.0)"] 373 | sms = ["mypy-boto3-sms (>=1.34.0,<1.35.0)"] 374 | sms-voice = ["mypy-boto3-sms-voice (>=1.34.0,<1.35.0)"] 375 | snow-device-management = ["mypy-boto3-snow-device-management (>=1.34.0,<1.35.0)"] 376 | snowball = ["mypy-boto3-snowball (>=1.34.0,<1.35.0)"] 377 | sns = ["mypy-boto3-sns (>=1.34.0,<1.35.0)"] 378 | sqs = ["mypy-boto3-sqs (>=1.34.0,<1.35.0)"] 379 | ssm = ["mypy-boto3-ssm (>=1.34.0,<1.35.0)"] 380 | ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.34.0,<1.35.0)"] 381 | ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.34.0,<1.35.0)"] 382 | ssm-sap = ["mypy-boto3-ssm-sap (>=1.34.0,<1.35.0)"] 383 | sso = ["mypy-boto3-sso (>=1.34.0,<1.35.0)"] 384 | sso-admin = ["mypy-boto3-sso-admin (>=1.34.0,<1.35.0)"] 385 | sso-oidc = ["mypy-boto3-sso-oidc (>=1.34.0,<1.35.0)"] 386 | stepfunctions = ["mypy-boto3-stepfunctions (>=1.34.0,<1.35.0)"] 387 | storagegateway = ["mypy-boto3-storagegateway (>=1.34.0,<1.35.0)"] 388 | sts = ["mypy-boto3-sts (>=1.34.0,<1.35.0)"] 389 | support = ["mypy-boto3-support (>=1.34.0,<1.35.0)"] 390 | support-app = ["mypy-boto3-support-app (>=1.34.0,<1.35.0)"] 391 | swf = ["mypy-boto3-swf (>=1.34.0,<1.35.0)"] 392 | synthetics = ["mypy-boto3-synthetics (>=1.34.0,<1.35.0)"] 393 | textract = ["mypy-boto3-textract (>=1.34.0,<1.35.0)"] 394 | timestream-query = ["mypy-boto3-timestream-query (>=1.34.0,<1.35.0)"] 395 | timestream-write = ["mypy-boto3-timestream-write (>=1.34.0,<1.35.0)"] 396 | tnb = ["mypy-boto3-tnb (>=1.34.0,<1.35.0)"] 397 | transcribe = ["mypy-boto3-transcribe (>=1.34.0,<1.35.0)"] 398 | transfer = ["mypy-boto3-transfer (>=1.34.0,<1.35.0)"] 399 | translate = ["mypy-boto3-translate (>=1.34.0,<1.35.0)"] 400 | trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.34.0,<1.35.0)"] 401 | verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.34.0,<1.35.0)"] 402 | voice-id = ["mypy-boto3-voice-id (>=1.34.0,<1.35.0)"] 403 | vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.34.0,<1.35.0)"] 404 | waf = ["mypy-boto3-waf (>=1.34.0,<1.35.0)"] 405 | waf-regional = ["mypy-boto3-waf-regional (>=1.34.0,<1.35.0)"] 406 | wafv2 = ["mypy-boto3-wafv2 (>=1.34.0,<1.35.0)"] 407 | wellarchitected = ["mypy-boto3-wellarchitected (>=1.34.0,<1.35.0)"] 408 | wisdom = ["mypy-boto3-wisdom (>=1.34.0,<1.35.0)"] 409 | workdocs = ["mypy-boto3-workdocs (>=1.34.0,<1.35.0)"] 410 | worklink = ["mypy-boto3-worklink (>=1.34.0,<1.35.0)"] 411 | workmail = ["mypy-boto3-workmail (>=1.34.0,<1.35.0)"] 412 | workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.34.0,<1.35.0)"] 413 | workspaces = ["mypy-boto3-workspaces (>=1.34.0,<1.35.0)"] 414 | workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.34.0,<1.35.0)"] 415 | workspaces-web = ["mypy-boto3-workspaces-web (>=1.34.0,<1.35.0)"] 416 | xray = ["mypy-boto3-xray (>=1.34.0,<1.35.0)"] 417 | 418 | [[package]] 419 | name = "botocore" 420 | version = "1.33.13" 421 | description = "Low-level, data-driven core of boto 3." 422 | optional = false 423 | python-versions = ">= 3.7" 424 | files = [ 425 | {file = "botocore-1.33.13-py3-none-any.whl", hash = "sha256:aeadccf4b7c674c7d47e713ef34671b834bc3e89723ef96d994409c9f54666e6"}, 426 | {file = "botocore-1.33.13.tar.gz", hash = "sha256:fb577f4cb175605527458b04571451db1bd1a2036976b626206036acd4496617"}, 427 | ] 428 | 429 | [package.dependencies] 430 | jmespath = ">=0.7.1,<2.0.0" 431 | python-dateutil = ">=2.1,<3.0.0" 432 | urllib3 = [ 433 | {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, 434 | {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""}, 435 | ] 436 | 437 | [package.extras] 438 | crt = ["awscrt (==0.19.17)"] 439 | 440 | [[package]] 441 | name = "botocore-stubs" 442 | version = "1.34.4" 443 | description = "Type annotations and code completion for botocore" 444 | optional = false 445 | python-versions = ">=3.7,<4.0" 446 | files = [ 447 | {file = "botocore_stubs-1.34.4-py3-none-any.whl", hash = "sha256:efa7e2e8ea9f4881b60703c7ddf5a4e64e66d6d03a20db046653235753b8d829"}, 448 | {file = "botocore_stubs-1.34.4.tar.gz", hash = "sha256:c408265a12d97a467d58e0940d52323dda1faf4a02a07a4105d00e384a4e39c2"}, 449 | ] 450 | 451 | [package.dependencies] 452 | types-awscrt = "*" 453 | typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.9\""} 454 | 455 | [package.extras] 456 | botocore = ["botocore"] 457 | 458 | [[package]] 459 | name = "colorama" 460 | version = "0.4.6" 461 | description = "Cross-platform colored terminal text." 462 | optional = false 463 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 464 | files = [ 465 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 466 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 467 | ] 468 | 469 | [[package]] 470 | name = "exceptiongroup" 471 | version = "1.1.3" 472 | description = "Backport of PEP 654 (exception groups)" 473 | optional = false 474 | python-versions = ">=3.7" 475 | files = [ 476 | {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, 477 | {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, 478 | ] 479 | 480 | [package.extras] 481 | test = ["pytest (>=6)"] 482 | 483 | [[package]] 484 | name = "freezegun" 485 | version = "1.2.2" 486 | description = "Let your Python tests travel through time" 487 | optional = false 488 | python-versions = ">=3.6" 489 | files = [ 490 | {file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"}, 491 | {file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"}, 492 | ] 493 | 494 | [package.dependencies] 495 | python-dateutil = ">=2.7" 496 | 497 | [[package]] 498 | name = "importlib-metadata" 499 | version = "6.7.0" 500 | description = "Read metadata from Python packages" 501 | optional = false 502 | python-versions = ">=3.7" 503 | files = [ 504 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 505 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 506 | ] 507 | 508 | [package.dependencies] 509 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 510 | zipp = ">=0.5" 511 | 512 | [package.extras] 513 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 514 | perf = ["ipython"] 515 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 516 | 517 | [[package]] 518 | name = "iniconfig" 519 | version = "2.0.0" 520 | description = "brain-dead simple config-ini parsing" 521 | optional = false 522 | python-versions = ">=3.7" 523 | files = [ 524 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 525 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 526 | ] 527 | 528 | [[package]] 529 | name = "jmespath" 530 | version = "1.0.1" 531 | description = "JSON Matching Expressions" 532 | optional = false 533 | python-versions = ">=3.7" 534 | files = [ 535 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 536 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 537 | ] 538 | 539 | [[package]] 540 | name = "packaging" 541 | version = "23.2" 542 | description = "Core utilities for Python packages" 543 | optional = false 544 | python-versions = ">=3.7" 545 | files = [ 546 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 547 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 548 | ] 549 | 550 | [[package]] 551 | name = "pluggy" 552 | version = "1.2.0" 553 | description = "plugin and hook calling mechanisms for python" 554 | optional = false 555 | python-versions = ">=3.7" 556 | files = [ 557 | {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, 558 | {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, 559 | ] 560 | 561 | [package.dependencies] 562 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 563 | 564 | [package.extras] 565 | dev = ["pre-commit", "tox"] 566 | testing = ["pytest", "pytest-benchmark"] 567 | 568 | [[package]] 569 | name = "pytest" 570 | version = "7.4.3" 571 | description = "pytest: simple powerful testing with Python" 572 | optional = false 573 | python-versions = ">=3.7" 574 | files = [ 575 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 576 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 577 | ] 578 | 579 | [package.dependencies] 580 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 581 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 582 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 583 | iniconfig = "*" 584 | packaging = "*" 585 | pluggy = ">=0.12,<2.0" 586 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 587 | 588 | [package.extras] 589 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 590 | 591 | [[package]] 592 | name = "pytest-mock" 593 | version = "3.11.1" 594 | description = "Thin-wrapper around the mock package for easier use with pytest" 595 | optional = false 596 | python-versions = ">=3.7" 597 | files = [ 598 | {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, 599 | {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, 600 | ] 601 | 602 | [package.dependencies] 603 | pytest = ">=5.0" 604 | 605 | [package.extras] 606 | dev = ["pre-commit", "pytest-asyncio", "tox"] 607 | 608 | [[package]] 609 | name = "python-dateutil" 610 | version = "2.8.2" 611 | description = "Extensions to the standard Python datetime module" 612 | optional = false 613 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 614 | files = [ 615 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 616 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 617 | ] 618 | 619 | [package.dependencies] 620 | six = ">=1.5" 621 | 622 | [[package]] 623 | name = "pyyaml" 624 | version = "6.0.1" 625 | description = "YAML parser and emitter for Python" 626 | optional = false 627 | python-versions = ">=3.6" 628 | files = [ 629 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 630 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 631 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 632 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 633 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 634 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 635 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 636 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 637 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 638 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 639 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 640 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 641 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 642 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 643 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 644 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 645 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 646 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 647 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 648 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 649 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 650 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 651 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 652 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 653 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 654 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 655 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 656 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 657 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 658 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 659 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 660 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 661 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 662 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 663 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 664 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 665 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 666 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 667 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 668 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 669 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 670 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 671 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 672 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 673 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 674 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 675 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 676 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 677 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 678 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 679 | ] 680 | 681 | [[package]] 682 | name = "s3transfer" 683 | version = "0.8.2" 684 | description = "An Amazon S3 Transfer Manager" 685 | optional = false 686 | python-versions = ">= 3.7" 687 | files = [ 688 | {file = "s3transfer-0.8.2-py3-none-any.whl", hash = "sha256:c9e56cbe88b28d8e197cf841f1f0c130f246595e77ae5b5a05b69fe7cb83de76"}, 689 | {file = "s3transfer-0.8.2.tar.gz", hash = "sha256:368ac6876a9e9ed91f6bc86581e319be08188dc60d50e0d56308ed5765446283"}, 690 | ] 691 | 692 | [package.dependencies] 693 | botocore = ">=1.33.2,<2.0a.0" 694 | 695 | [package.extras] 696 | crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] 697 | 698 | [[package]] 699 | name = "six" 700 | version = "1.16.0" 701 | description = "Python 2 and 3 compatibility utilities" 702 | optional = false 703 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 704 | files = [ 705 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 706 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 707 | ] 708 | 709 | [[package]] 710 | name = "tomli" 711 | version = "2.0.1" 712 | description = "A lil' TOML parser" 713 | optional = false 714 | python-versions = ">=3.7" 715 | files = [ 716 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 717 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 718 | ] 719 | 720 | [[package]] 721 | name = "types-awscrt" 722 | version = "0.19.8" 723 | description = "Type annotations and code completion for awscrt" 724 | optional = false 725 | python-versions = ">=3.7,<4.0" 726 | files = [ 727 | {file = "types_awscrt-0.19.8-py3-none-any.whl", hash = "sha256:4eb4f3bd0c41a2710cacda13098374da9faa76c5a0fb901aa5659e0fd48ceda1"}, 728 | {file = "types_awscrt-0.19.8.tar.gz", hash = "sha256:a2d534b7017c3476ee69a44bd8aeaf3b588c42baa8322473d100a45ee67510d7"}, 729 | ] 730 | 731 | [[package]] 732 | name = "types-s3transfer" 733 | version = "0.7.0" 734 | description = "Type annotations and code completion for s3transfer" 735 | optional = false 736 | python-versions = ">=3.7,<4.0" 737 | files = [ 738 | {file = "types_s3transfer-0.7.0-py3-none-any.whl", hash = "sha256:ae9ed9273465d9f43da8b96307383da410c6b59c3b2464c88d20b578768e97c6"}, 739 | {file = "types_s3transfer-0.7.0.tar.gz", hash = "sha256:aca0f2486d0a3a5037cd5b8f3e20a4522a29579a8dd183281ff0aa1c4e2c8aa7"}, 740 | ] 741 | 742 | [[package]] 743 | name = "typing-extensions" 744 | version = "4.7.1" 745 | description = "Backported and Experimental Type Hints for Python 3.7+" 746 | optional = false 747 | python-versions = ">=3.7" 748 | files = [ 749 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 750 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 751 | ] 752 | 753 | [[package]] 754 | name = "urllib3" 755 | version = "1.26.19" 756 | description = "HTTP library with thread-safe connection pooling, file post, and more." 757 | optional = false 758 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 759 | files = [ 760 | {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, 761 | {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, 762 | ] 763 | 764 | [package.extras] 765 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 766 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 767 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 768 | 769 | [[package]] 770 | name = "zipp" 771 | version = "3.15.0" 772 | description = "Backport of pathlib-compatible object wrapper for zip files" 773 | optional = false 774 | python-versions = ">=3.7" 775 | files = [ 776 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 777 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 778 | ] 779 | 780 | [package.extras] 781 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 782 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 783 | 784 | [metadata] 785 | lock-version = "2.0" 786 | python-versions = "^3.7" 787 | content-hash = "f029778cd7ebd5032c788368f1a6f9fefd382f28705350fead8167393baa6205" 788 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sample-helper-aws-appconfig" 3 | version = "2.2.1" 4 | description = "Sample helper library for AWS AppConfig" 5 | authors = ["Amazon Web Services"] 6 | maintainers = ["James Seward "] 7 | license = "OSI Approved (Apache-2.0)" 8 | readme = "README.md" 9 | repository = "https://github.com/aws-samples/sample-python-helper-aws-appconfig" 10 | keywords = ["aws", "appconfig"] 11 | classifiers=[ 12 | "Development Status :: 4 - Beta", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Typing :: Typed", 23 | ] 24 | packages = [ { include="appconfig_helper" } ] 25 | 26 | 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.7" 30 | boto3 = "^1.20.8" 31 | botocore = "^1.23.8" 32 | PyYAML = "^6.0" 33 | 34 | [tool.poetry.dev-dependencies] 35 | boto3-stubs = "^1.20.8" 36 | pytest = "^7.4.0" 37 | pytest-mock = "^3.6.1" 38 | freezegun = "^1.1.0" 39 | 40 | [build-system] 41 | requires = ["poetry-core>=1.0.0"] 42 | build-backend = "poetry.core.masonry.api" 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | ignore = E501 4 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | 3 | import datetime 4 | import io 5 | import json 6 | import time 7 | 8 | import boto3 9 | import botocore 10 | import botocore.exceptions 11 | import botocore.session 12 | import pytest 13 | import yaml 14 | from botocore.response import StreamingBody 15 | from botocore.stub import Stubber 16 | from freezegun import freeze_time 17 | 18 | from appconfig_helper import AppConfigHelper 19 | 20 | 21 | @pytest.fixture(autouse=True) 22 | def appconfig_stub(): 23 | session = botocore.session.get_session() 24 | client = session.create_client("appconfigdata", region_name="us-east-1") 25 | with Stubber(client) as stubber: 26 | yield (client, stubber, session) 27 | stubber.assert_no_pending_responses() 28 | 29 | 30 | @pytest.fixture(autouse=True) 31 | def appconfig_stub_ignore_pending(): 32 | session = botocore.session.get_session() 33 | client = session.create_client("appconfigdata", region_name="us-east-1") 34 | with Stubber(client) as stubber: 35 | yield (client, stubber, session) 36 | 37 | 38 | def _build_request(next_token="token1234"): 39 | return {"ConfigurationToken": next_token} 40 | 41 | 42 | def _build_response( 43 | content, content_type, next_token="token5678", poll=30, version_label="v1" 44 | ): 45 | if content_type == "application/json": 46 | content_text = json.dumps(content).encode("utf-8") 47 | elif content_type == "application/x-yaml": 48 | content_text = str(yaml.dump(content)).encode("utf-8") 49 | else: 50 | content_text = content.encode("utf-8") 51 | return { 52 | "Configuration": StreamingBody( 53 | io.BytesIO(bytes(content_text)), len(content_text) 54 | ), 55 | "ContentType": content_type, 56 | "NextPollConfigurationToken": next_token, 57 | "NextPollIntervalInSeconds": poll, 58 | "VersionLabel": version_label, 59 | } 60 | 61 | 62 | def _add_start_stub( 63 | stub, 64 | app_id="AppConfig-App", 65 | config_id="AppConfig-Profile", 66 | env_id="AppConfig-Env", 67 | poll=15, 68 | next_token="token1234", 69 | ): 70 | stub.add_response( 71 | "start_configuration_session", 72 | {"InitialConfigurationToken": next_token}, 73 | { 74 | "ApplicationIdentifier": app_id, 75 | "ConfigurationProfileIdentifier": config_id, 76 | "EnvironmentIdentifier": env_id, 77 | "RequiredMinimumPollIntervalInSeconds": poll, 78 | }, 79 | ) 80 | 81 | 82 | def test_appconfig_init(appconfig_stub, mocker): 83 | client, stub, _ = appconfig_stub 84 | mocker.patch.object(boto3, "client", return_value=client) 85 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 86 | 87 | assert isinstance(a, AppConfigHelper) 88 | assert a.appconfig_application == "AppConfig-App" 89 | assert a.appconfig_environment == "AppConfig-Env" 90 | assert a.appconfig_profile == "AppConfig-Profile" 91 | assert a.config is None 92 | assert a._last_update_time == 0.0 93 | assert a.raw_config is None 94 | assert a.content_type is None 95 | assert a._poll_interval == 15 96 | assert a._next_config_token is None 97 | 98 | 99 | def test_appconfig_update(appconfig_stub, mocker): 100 | client, stub, _ = appconfig_stub 101 | _add_start_stub(stub) 102 | stub.add_response( 103 | "get_latest_configuration", 104 | _build_response("hello", "text/plain"), 105 | _build_request(), 106 | ) 107 | mocker.patch.object(boto3, "client", return_value=client) 108 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 109 | result = a.update_config() 110 | assert result 111 | assert a.config == "hello" 112 | assert a.raw_config == b"hello" 113 | assert a.content_type == "text/plain" 114 | assert a._next_config_token == "token5678" 115 | assert a._poll_interval == 30 116 | 117 | 118 | def test_appconfig_update_interval(appconfig_stub, mocker): 119 | client, stub, _ = appconfig_stub 120 | _add_start_stub(stub) 121 | stub.add_response( 122 | "get_latest_configuration", 123 | _build_response("hello", "text/plain"), 124 | _build_request(), 125 | ) 126 | mocker.patch.object(boto3, "client", return_value=client) 127 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 128 | result = a.update_config() 129 | assert result 130 | assert a.config == "hello" 131 | assert a._next_config_token == "token5678" 132 | assert a._poll_interval == 30 133 | 134 | result = a.update_config() 135 | assert not result 136 | assert a.config == "hello" 137 | assert a._next_config_token == "token5678" 138 | 139 | 140 | def test_appconfig_force_update_same(appconfig_stub, mocker): 141 | client, stub, _ = appconfig_stub 142 | _add_start_stub(stub) 143 | stub.add_response( 144 | "get_latest_configuration", 145 | _build_response("hello", "text/plain"), 146 | _build_request(), 147 | ) 148 | stub.add_response( 149 | "get_latest_configuration", 150 | _build_response("", "text/plain"), 151 | _build_request(next_token="token5678"), 152 | ) 153 | mocker.patch.object(boto3, "client", return_value=client) 154 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 155 | result = a.update_config() 156 | assert result 157 | assert a.config == "hello" 158 | assert a.raw_config == b"hello" 159 | assert a.content_type == "text/plain" 160 | assert a._next_config_token == "token5678" 161 | assert a._poll_interval == 30 162 | 163 | result = a.update_config(force_update=True) 164 | assert not result 165 | assert a.config == "hello" 166 | assert a.raw_config == b"hello" 167 | assert a.content_type == "text/plain" 168 | assert a._next_config_token == "token5678" 169 | assert a._poll_interval == 30 170 | 171 | 172 | def test_appconfig_force_update_new(appconfig_stub, mocker): 173 | client, stub, _ = appconfig_stub 174 | _add_start_stub(stub) 175 | stub.add_response( 176 | "get_latest_configuration", 177 | _build_response("hello", "text/plain"), 178 | _build_request(), 179 | ) 180 | stub.add_response( 181 | "get_latest_configuration", 182 | _build_response("world", "text/plain", next_token="token9012"), 183 | _build_request(next_token="token5678"), 184 | ) 185 | mocker.patch.object(boto3, "client", return_value=client) 186 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 187 | result = a.update_config() 188 | assert result 189 | assert a.config == "hello" 190 | assert a.raw_config == b"hello" 191 | assert a.content_type == "text/plain" 192 | assert a._next_config_token == "token5678" 193 | assert a._poll_interval == 30 194 | 195 | result = a.update_config(force_update=True) 196 | assert result 197 | assert a.config == "world" 198 | assert a.raw_config == b"world" 199 | assert a.content_type == "text/plain" 200 | assert a._next_config_token == "token9012" 201 | assert a._poll_interval == 30 202 | 203 | 204 | def test_appconfig_update_bad_request(appconfig_stub, mocker): 205 | client, stub, _ = appconfig_stub 206 | _add_start_stub(stub) 207 | stub.add_response( 208 | "get_latest_configuration", 209 | _build_response("hello", "text/plain", next_token="token5678"), 210 | _build_request(), 211 | ) 212 | stub.add_client_error( 213 | "get_latest_configuration", 214 | service_error_code="BadRequestException", 215 | ) 216 | _add_start_stub(stub) 217 | stub.add_response( 218 | "get_latest_configuration", 219 | _build_response("world", "text/plain", next_token="token9012"), 220 | _build_request(), 221 | ) 222 | mocker.patch.object(boto3, "client", return_value=client) 223 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 224 | result = a.update_config() 225 | assert result 226 | assert a.config == "hello" 227 | assert a.raw_config == b"hello" 228 | assert a.content_type == "text/plain" 229 | assert a._next_config_token == "token5678" 230 | assert a._poll_interval == 30 231 | 232 | result = a.update_config(force_update=True) 233 | assert result 234 | assert a.config == "world" 235 | assert a.raw_config == b"world" 236 | assert a.content_type == "text/plain" 237 | assert a._next_config_token == "token9012" 238 | assert a._poll_interval == 30 239 | 240 | 241 | def test_appconfig_fetch_on_init(appconfig_stub, mocker): 242 | client, stub, _ = appconfig_stub 243 | _add_start_stub(stub) 244 | stub.add_response( 245 | "get_latest_configuration", 246 | _build_response("hello", "text/plain"), 247 | _build_request(), 248 | ) 249 | mocker.patch.object(boto3, "client", return_value=client) 250 | a = AppConfigHelper( 251 | "AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15, fetch_on_init=True 252 | ) 253 | assert a.config == "hello" 254 | 255 | 256 | @freeze_time("2020-08-01 12:00:00", auto_tick_seconds=20) 257 | def test_appconfig_fetch_on_read(appconfig_stub, mocker): 258 | client, stub, _ = appconfig_stub 259 | _add_start_stub(stub) 260 | stub.add_response( 261 | "get_latest_configuration", 262 | _build_response("hello", "text/plain", poll=15), 263 | _build_request(), 264 | ) 265 | stub.add_response( 266 | "get_latest_configuration", 267 | _build_response("world", "text/plain", next_token="token9012"), 268 | _build_request(next_token="token5678"), 269 | ) 270 | mocker.patch.object(boto3, "client", return_value=client) 271 | a = AppConfigHelper( 272 | "AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15, fetch_on_read=True 273 | ) 274 | assert a.config == "hello" 275 | assert a._next_config_token == "token5678" 276 | assert a.config == "world" 277 | assert a._next_config_token == "token9012" 278 | 279 | 280 | def test_appconfig_fetch_interval(appconfig_stub, mocker): 281 | with freeze_time("2020-08-01 12:00:00") as frozen_time: 282 | tick_amount = datetime.timedelta(seconds=10) 283 | client, stub, _ = appconfig_stub 284 | _add_start_stub(stub) 285 | stub.add_response( 286 | "get_latest_configuration", 287 | _build_response("hello", "text/plain", poll=15), 288 | _build_request(), 289 | ) 290 | stub.add_response( 291 | "get_latest_configuration", 292 | _build_response("world", "text/plain", poll=15, next_token="token1234"), 293 | _build_request(next_token="token5678"), 294 | ) 295 | mocker.patch.object(boto3, "client", return_value=client) 296 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 297 | result = a.update_config() 298 | update_time = time.time() 299 | assert result 300 | assert a.config == "hello" 301 | assert a._last_update_time == update_time 302 | 303 | frozen_time.tick(tick_amount) 304 | result = a.update_config() 305 | assert not result 306 | assert a.config == "hello" 307 | assert a._last_update_time == update_time 308 | 309 | frozen_time.tick(tick_amount) 310 | result = a.update_config() 311 | assert result 312 | assert a.config == "world" 313 | assert a._next_config_token == "token1234" 314 | assert a._last_update_time == time.time() 315 | 316 | 317 | def test_appconfig_fetch_no_change(appconfig_stub, mocker): 318 | with freeze_time("2020-08-01 12:00:00") as frozen_time: 319 | tick_amount = datetime.timedelta(seconds=10) 320 | client, stub, _ = appconfig_stub 321 | _add_start_stub(stub) 322 | stub.add_response( 323 | "get_latest_configuration", 324 | _build_response("hello", "text/plain", poll=15), 325 | _build_request(), 326 | ) 327 | stub.add_response( 328 | "get_latest_configuration", 329 | _build_response("", "text/plain", poll=15, next_token="token1234"), 330 | _build_request(next_token="token5678"), 331 | ) 332 | mocker.patch.object(boto3, "client", return_value=client) 333 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 334 | result = a.update_config() 335 | update_time = time.time() 336 | assert result 337 | assert a.config == "hello" 338 | assert a._last_update_time == update_time 339 | 340 | frozen_time.tick(tick_amount) 341 | frozen_time.tick(tick_amount) 342 | 343 | result = a.update_config() 344 | assert not result 345 | assert a.config == "hello" 346 | assert a._next_config_token == "token1234" 347 | assert a._last_update_time == time.time() 348 | 349 | 350 | def test_appconfig_yaml(appconfig_stub, mocker): 351 | client, stub, _ = appconfig_stub 352 | _add_start_stub(stub) 353 | stub.add_response( 354 | "get_latest_configuration", 355 | _build_response({"hello": "world"}, "application/x-yaml"), 356 | _build_request(), 357 | ) 358 | mocker.patch.object(boto3, "client", return_value=client) 359 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 360 | a.update_config() 361 | assert a.config == {"hello": "world"} 362 | assert a.content_type == "application/x-yaml" 363 | 364 | 365 | def test_appconfig_json(appconfig_stub, mocker): 366 | client, stub, _ = appconfig_stub 367 | _add_start_stub(stub) 368 | stub.add_response( 369 | "get_latest_configuration", 370 | _build_response({"hello": "world"}, "application/json"), 371 | _build_request(), 372 | ) 373 | mocker.patch.object(boto3, "client", return_value=client) 374 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 375 | a.update_config() 376 | assert a.config == {"hello": "world"} 377 | assert a.content_type == "application/json" 378 | 379 | 380 | def test_appconfig_session(appconfig_stub, mocker): 381 | client, stub, session = appconfig_stub 382 | _add_start_stub(stub) 383 | stub.add_response( 384 | "get_latest_configuration", 385 | _build_response({"hello": "world"}, "application/json"), 386 | _build_request(), 387 | ) 388 | mocker.patch.object(boto3, "client", return_value=client) 389 | mocker.patch.object(boto3.Session, "client", return_value=client) 390 | a = AppConfigHelper( 391 | "AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15, session=session 392 | ) 393 | a.update_config() 394 | 395 | 396 | def test_bad_json(appconfig_stub, mocker): 397 | client, stub, session = appconfig_stub 398 | content_text = """{"broken": "json",}""".encode("utf-8") 399 | _add_start_stub(stub) 400 | broken_response = _build_response({}, "application/json") 401 | broken_response["Configuration"] = StreamingBody( 402 | io.BytesIO(bytes(content_text)), len(content_text) 403 | ) 404 | stub.add_response( 405 | "get_latest_configuration", 406 | broken_response, 407 | _build_request(), 408 | ) 409 | mocker.patch.object(boto3, "client", return_value=client) 410 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 411 | with pytest.raises(ValueError): 412 | a.update_config() 413 | 414 | 415 | def test_bad_yaml(appconfig_stub, mocker): 416 | client, stub, session = appconfig_stub 417 | content_text = """ 418 | broken: 419 | - yaml 420 | - content 421 | """.encode( 422 | "utf-8" 423 | ) 424 | _add_start_stub(stub) 425 | broken_response = _build_response({}, "application/x-yaml") 426 | broken_response["Configuration"] = StreamingBody( 427 | io.BytesIO(bytes(content_text)), len(content_text) 428 | ) 429 | stub.add_response( 430 | "get_latest_configuration", 431 | broken_response, 432 | _build_request(), 433 | ) 434 | mocker.patch.object(boto3, "client", return_value=client) 435 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 436 | with pytest.raises(ValueError): 437 | a.update_config() 438 | 439 | 440 | def test_unknown_content_type(appconfig_stub, mocker): 441 | client, stub, session = appconfig_stub 442 | content_text = "hello world" 443 | _add_start_stub(stub) 444 | stub.add_response( 445 | "get_latest_configuration", 446 | _build_response(content_text, "image/jpeg"), 447 | _build_request(), 448 | ) 449 | mocker.patch.object(boto3, "client", return_value=client) 450 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 451 | a.update_config() 452 | assert a.config == b"hello world" 453 | assert a.content_type == "image/jpeg" 454 | assert a.raw_config == content_text.encode("utf-8") 455 | 456 | 457 | def test_bad_request(appconfig_stub_ignore_pending, mocker): 458 | client, stub, session = appconfig_stub_ignore_pending 459 | content_text = "hello world" 460 | _add_start_stub(stub, "", "", "") 461 | stub.add_response( 462 | "get_latest_configuration", 463 | _build_response(content_text, "image/jpeg"), 464 | _build_request(), 465 | ) 466 | mocker.patch.object(boto3, "client", return_value=client) 467 | with pytest.raises(botocore.exceptions.ParamValidationError): 468 | a = AppConfigHelper("", "", "", 15) 469 | a.update_config() 470 | 471 | 472 | def test_bad_interval(appconfig_stub, mocker): 473 | client, stub, session = appconfig_stub 474 | mocker.patch.object(boto3, "client", return_value=client) 475 | with pytest.raises(ValueError): 476 | _ = AppConfigHelper("Any", "Any", "Any", 10) 477 | 478 | 479 | def test_version_label(appconfig_stub, mocker): 480 | client, stub, session = appconfig_stub 481 | _add_start_stub(stub) 482 | stub.add_response( 483 | "get_latest_configuration", 484 | _build_response(content="test", content_type="text/plain"), 485 | _build_request(), 486 | ) 487 | mocker.patch.object(boto3, "client", return_value=client) 488 | a = AppConfigHelper("AppConfig-App", "AppConfig-Env", "AppConfig-Profile", 15) 489 | a.update_config() 490 | assert a.version_label == "v1" 491 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,py38,py39,py310 3 | isolated_build = True 4 | 5 | [gh-actions] 6 | python = 7 | 3.6: py36 8 | 3.7: py37 9 | 3.8: py38 10 | 3.9: py39 11 | 12 | [testenv] 13 | deps = pytest 14 | pytest-mock 15 | boto3 16 | pyyaml 17 | freezegun 18 | commands = pytest 19 | --------------------------------------------------------------------------------