From 978ec4bf793d67d438bbdf3dc2c76c83b2bb6c6f Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Wed, 26 Mar 2025 10:34:12 -0700 Subject: [PATCH 1/3] fix: `InjectSecretString` clobbering `self` arg in class methods (#24) --- src/aws_secretsmanager_caching/decorators.py | 20 ++++++++++++++++---- test/unit/test_decorators.py | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/aws_secretsmanager_caching/decorators.py b/src/aws_secretsmanager_caching/decorators.py index d38aee9..f235524 100644 --- a/src/aws_secretsmanager_caching/decorators.py +++ b/src/aws_secretsmanager_caching/decorators.py @@ -10,8 +10,10 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. -"""Decorators for use with caching library """ +"""Decorators for use with caching library""" + import json +from functools import wraps class InjectSecretString: @@ -42,11 +44,18 @@ def __call__(self, func): secret = self.cache.get_secret_string(secret_id=self.secret_id) + # Using functools.wraps preserves the metadata of the wrapped function + @wraps(func) def _wrapped_func(*args, **kwargs): """ Internal function to execute wrapped function """ - return func(secret, *args, **kwargs) + # Prevent clobbering self arg in class methods + if args and hasattr(args[0].__class__, func.__name__): + new_args = (args[0], secret) + args[1:] + else: + new_args = (secret,) + args + return func(*new_args, **kwargs) return _wrapped_func @@ -85,15 +94,18 @@ def __call__(self, func): try: secret = json.loads(self.cache.get_secret_string(secret_id=self.secret_id)) except json.decoder.JSONDecodeError: - raise RuntimeError('Cached secret is not valid JSON') from None + raise RuntimeError("Cached secret is not valid JSON") from None resolved_kwargs = {} for orig_kwarg, secret_key in self.kwarg_map.items(): try: resolved_kwargs[orig_kwarg] = secret[secret_key] except KeyError: - raise RuntimeError(f'Cached secret does not contain key {secret_key}') from None + raise RuntimeError( + f"Cached secret does not contain key {secret_key}" + ) from None + @wraps(func) def _wrapped_func(*args, **kwargs): """ Internal function to execute wrapped function diff --git a/test/unit/test_decorators.py b/test/unit/test_decorators.py index ccac0d3..f869e70 100644 --- a/test/unit/test_decorators.py +++ b/test/unit/test_decorators.py @@ -191,3 +191,22 @@ def function_to_be_decorated(arg1, arg2, arg3): self.assertEqual(arg3, 'bar') function_to_be_decorated(arg2='foo', arg3='bar') + + def test_string_with_class_method(self): + secret = 'not json' + response = {} + versions = { + '01234567890123456789012345678901': ['AWSCURRENT'] + } + version_response = {'SecretString': secret} + cache = SecretCache(client=self.get_client(response, versions, version_response)) + + class TestClass(unittest.TestCase): + @InjectSecretString('test', cache) + def class_method(self, arg1, arg2, arg3): + self.assertEqual(arg1, secret) + self.assertEqual(arg2, 'foo') + self.assertEqual(arg3, 'bar') + + t = TestClass() + t.class_method(arg2="foo", arg3="bar") From 1530d76ddf1372343015fa0a6a03024916db8d8d Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Thu, 10 Apr 2025 15:18:30 -0700 Subject: [PATCH 2/3] chore: add formatting check action + instructions --- .github/workflows/python-package.yml | 60 ++++++++++--------- README.md | 87 ++++++++++++++++++---------- 2 files changed, 91 insertions(+), 56 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index be8f6df..5395bb2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,13 +5,12 @@ name: Python package on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: - runs-on: ubuntu-latest strategy: fail-fast: false @@ -19,26 +18,35 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r dev-requirements.txt - pip install -e . - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Lint with PyLint - run: pylint --rcfile=.pylintrc src/aws_secretsmanager_caching - - name: Test with pytest - run: | - pytest test/unit/ - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r dev-requirements.txt + pip install -e . + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Lint with PyLint + run: pylint --rcfile=.pylintrc src/aws_secretsmanager_caching + + - name: Check formatting with Ruff + uses: astral-sh/ruff-action@v3 + + - name: Test with pytest + run: | + pytest test/unit/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 diff --git a/README.md b/README.md index bcb4b50..92b2984 100644 --- a/README.md +++ b/README.md @@ -11,39 +11,46 @@ The AWS Secrets Manager Python caching client enables in-process caching of secr To use this client you must have: -* Python 3.8 or newer. Use of Python versions 3.7 or older are not supported. -* An Amazon Web Services (AWS) account to access secrets stored in AWS Secrets Manager. - * **To create an AWS account**, go to [Sign In or Create an AWS Account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) and then choose **I am a new user.** Follow the instructions to create an AWS account. +- Python 3.8 or newer. Use of Python versions 3.7 or older are not supported. +- An Amazon Web Services (AWS) account to access secrets stored in AWS Secrets Manager. - * **To create a secret in AWS Secrets Manager**, go to [Creating Secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) and follow the instructions on that page. + - **To create an AWS account**, go to [Sign In or Create an AWS Account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) and then choose **I am a new user.** Follow the instructions to create an AWS account. - * This library makes use of botocore, the low-level core functionality of the boto3 SDK. For more information on boto3 and botocore, please review the [AWS SDK for Python](https://aws.amazon.com/sdk-for-python/) and [Botocore](https://botocore.amazonaws.com/v1/documentation/api/latest/index.html) documentation. + - **To create a secret in AWS Secrets Manager**, go to [Creating Secrets](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) and follow the instructions on that page. + + - This library makes use of botocore, the low-level core functionality of the boto3 SDK. For more information on boto3 and botocore, please review the [AWS SDK for Python](https://aws.amazon.com/sdk-for-python/) and [Botocore](https://botocore.amazonaws.com/v1/documentation/api/latest/index.html) documentation. ### Dependencies + This library requires the following standard dependencies: -* botocore -* setuptools_scm -* setuptools + +- botocore +- setuptools_scm +- setuptools For development and testing purposes, this library requires the following additional dependencies: -* pytest -* pytest-cov -* pytest-sugar -* codecov -* pylint -* sphinx -* flake8 -* tox + +- pytest +- pytest-cov +- pytest-sugar +- codecov +- pylint +- sphinx +- flake8 +- tox Please review the `requirements.txt` and `dev-requirements.txt` file for specific version requirements. ### Installation + Installing the latest release via **pip**: + ```bash $ pip install aws-secretsmanager-caching ``` Installing the latest development release: + ```bash $ git clone https://github.com/aws/aws-secretsmanager-caching-python.git $ cd aws-secretsmanager-caching-python @@ -51,7 +58,9 @@ $ python setup.py install ``` ### Development + #### Getting Started + Assuming that you have Python and virtualenv installed, set up your environment and install the required dependencies like this instead of the `pip install aws_secretsmanager_caching` defined above: ```bash @@ -64,8 +73,12 @@ $ pip install -r requirements.txt -r dev-requirements.txt $ pip install -e . ``` +**NOTE:** Please use [Ruff](https://docs.astral.sh/ruff/formatter/) for formatting. + #### Running Tests + You can run tests in all supported Python versions using tox. By default, it will run all of the unit and integration tests, but you can also specify your own arguments to past to `pytest`. + ```bash $ tox # runs integ/unit tests, flake8 tests and pylint tests $ tox -- test/unit/test_decorators.py # runs specific test file @@ -73,16 +86,22 @@ $ tox -e py37 -- test/integ/ # runs specific test directory ``` #### Documentation + You can locally-generate the Sphinx-based documentation via: + ```bash $ tox -e docs ``` + Which will subsequently be viewable at `file://${CLONE_DIR}/.tox/docs_out/index.html` ### Usage + Using the client consists of the following steps: -1. Instantiate the client while optionally passing in a `SecretCacheConfig()` object to the `config` parameter. You can also pass in an existing `botocore.client.BaseClient` client to the client parameter. + +1. Instantiate the client while optionally passing in a `SecretCacheConfig()` object to the `config` parameter. You can also pass in an existing `botocore.client.BaseClient` client to the client parameter. 2. Request the secret from the client instance. + ```python import botocore import botocore.session @@ -96,19 +115,24 @@ secret = cache.get_secret_string('mysecret') ``` #### Cache Configuration + You can configure the cache config object with the following parameters: -* `max_cache_size` - The maximum number of secrets to cache. The default value is `1024`. -* `exception_retry_delay_base` - The number of seconds to wait after an exception is encountered and before retrying the request. The default value is `1`. -* `exception_retry_growth_factor` - The growth factor to use for calculating the wait time between retries of failed requests. The default value is `2`. -* `exception_retry_delay_max` - The maximum amount of time in seconds to wait between failed requests. The default value is `3600`. -* `default_version_stage` - The default version stage to request. The default value is `'AWSCURRENT'` -* `secret_refresh_interval` - The number of seconds to wait between refreshing cached secret information. The default value is `3600.0`. -* `secret_cache_hook` - An implementation of the SecretCacheHook abstract class. The default value is `None`. + +- `max_cache_size` - The maximum number of secrets to cache. The default value is `1024`. +- `exception_retry_delay_base` - The number of seconds to wait after an exception is encountered and before retrying the request. The default value is `1`. +- `exception_retry_growth_factor` - The growth factor to use for calculating the wait time between retries of failed requests. The default value is `2`. +- `exception_retry_delay_max` - The maximum amount of time in seconds to wait between failed requests. The default value is `3600`. +- `default_version_stage` - The default version stage to request. The default value is `'AWSCURRENT'` +- `secret_refresh_interval` - The number of seconds to wait between refreshing cached secret information. The default value is `3600.0`. +- `secret_cache_hook` - An implementation of the SecretCacheHook abstract class. The default value is `None`. #### Decorators + The library also includes several decorator functions to wrap existing function calls with SecretString-based secrets: -* `@InjectedKeywordedSecretString` - This decorator expects the secret id and cache as the first and second arguments, with subsequent arguments mapping a parameter key from the function that is being wrapped to a key in the secret. The secret being retrieved from the cache must contain a SecretString and that string must be JSON-based. -* `@InjectSecretString` - This decorator also expects the secret id and cache as the first and second arguments. However, this decorator simply returns the result of the cache lookup directly to the first argument of the wrapped function. The secret does not need to be JSON-based but it must contain a SecretString. + +- `@InjectedKeywordedSecretString` - This decorator expects the secret id and cache as the first and second arguments, with subsequent arguments mapping a parameter key from the function that is being wrapped to a key in the secret. The secret being retrieved from the cache must contain a SecretString and that string must be JSON-based. +- `@InjectSecretString` - This decorator also expects the secret id and cache as the first and second arguments. However, this decorator simply returns the result of the cache lookup directly to the first argument of the wrapped function. The secret does not need to be JSON-based but it must contain a SecretString. + ```python from aws_secretsmanager_caching import SecretCache from aws_secretsmanager_caching import InjectKeywordedSecretString, InjectSecretString @@ -127,10 +151,13 @@ def function_to_be_decorated(arg1, arg2, arg3): ``` ## Getting Help + Please use these community resources for getting help: -* Ask a question on [Stack Overflow](https://stackoverflow.com/) and tag it with [aws-secrets-manager](https://stackoverflow.com/questions/tagged/aws-secrets-manager). -* Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/) -* If it turns out that you may have found a bug, or have a feature request, please [open an issue](https://github.com/aws/aws-secretsmanager-caching-python/issues/new). + +- Ask a question on [Stack Overflow](https://stackoverflow.com/) and tag it with [aws-secrets-manager](https://stackoverflow.com/questions/tagged/aws-secrets-manager). +- Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/) +- If it turns out that you may have found a bug, or have a feature request, please [open an issue](https://github.com/aws/aws-secretsmanager-caching-python/issues/new). + ## License -This library is licensed under the Apache 2.0 License. +This library is licensed under the Apache 2.0 License. From 701d2fa07ddf5f4728dbb0a2da4a58f5bbe03249 Mon Sep 17 00:00:00 2001 From: Angad Misra Date: Thu, 17 Apr 2025 14:21:14 -0700 Subject: [PATCH 3/3] fix: revert workflow formatting --- .github/workflows/python-package.yml | 62 +++++++++++++--------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5395bb2..4f589cf 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,12 +5,13 @@ name: Python package on: push: - branches: [master] + branches: [ master ] pull_request: - branches: [master] + branches: [ master ] jobs: build: + runs-on: ubuntu-latest strategy: fail-fast: false @@ -18,35 +19,28 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r dev-requirements.txt - pip install -e . - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Lint with PyLint - run: pylint --rcfile=.pylintrc src/aws_secretsmanager_caching - - - name: Check formatting with Ruff - uses: astral-sh/ruff-action@v3 - - - name: Test with pytest - run: | - pytest test/unit/ - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r dev-requirements.txt + pip install -e . + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Lint with PyLint + run: pylint --rcfile=.pylintrc src/aws_secretsmanager_caching + - name: Check formatting with Ruff + uses: astral-sh/ruff-action@v3 + - name: Test with pytest + run: | + pytest test/unit/ + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3