├── .flake8 ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── pull_request.yml │ ├── push.yml │ └── update-readme.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── NOTICE ├── README.md ├── README.template.md ├── boto3_refresh_session ├── __init__.py ├── session.py └── sts.py ├── bump_version.py ├── doc ├── Makefile ├── _static │ └── custom.css ├── authorization.rst ├── brs.png ├── conf.py ├── contributing.rst ├── downloads.png ├── index.rst ├── installation.rst ├── make.bat ├── modules │ ├── generated │ │ ├── boto3_refresh_session.session.RefreshableSession.rst │ │ └── boto3_refresh_session.sts.STSRefreshableSession.rst │ ├── index.rst │ ├── session.rst │ └── sts.rst ├── qanda.rst ├── raison.rst └── usage.rst ├── pyproject.toml ├── readme.py └── tests ├── __init__.py └── test_session.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .gitignore 4 | .pre-commit-config.yaml 5 | poetry.lock 6 | pyproject.toml 7 | README.md 8 | __pycache__ 9 | .poetry 10 | .pytest_cache 11 | .DS_Store 12 | tests/* 13 | dist/* 14 | Makefile 15 | *.rst 16 | *.md 17 | *.html 18 | *.css 19 | *.bat 20 | *.png 21 | extend-ignore = 22 | E501 23 | F404 24 | W291 25 | max-line-length = 79 26 | per-file-ignores = 27 | __init__.py:F401 28 | session.py:F821 29 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @michaelthomasletts 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## All submissions 2 | 3 | * [ ] Did you include the version part parameter, i.e. [major | minor | patch], to the beginning of the pull request title so that the version is bumped correctly? 4 | * Example pull request title: '[minor] Added a new parameter to the `RefreshableSession` object.' 5 | * Note: the version part parameter is only required for major and minor updates. Patches may exclude the part parameter from the pull request title, as the default is 'patch'. 6 | * [ ] Did you verify that your changes pass pre-commit checks before opening this pull request? 7 | * The pre-commit checks are identical to required status checks for pull requests in this repository. Know that suppressing pre-commit checks via the `--no-verify` | `-nv` arguments will not help you avoid the status checks! 8 | * To ensure that pre-commit checks work on your branch before running `git commit`, run `pre-commit install` and `pre-commit install-hooks` beforehand. 9 | * [ ] Have you checked that your changes don't relate to other open pull requests? 10 | 11 | 12 | 13 | ## New feature submissions 14 | 15 | * [ ] Does your new feature include documentation? If not, why not? 16 | * [ ] Does that documentation match the numpydoc guidelines? 17 | * [ ] Did you locally test your documentation changes using `sphinx-build doc doc/_build` from the root directory? 18 | * [ ] Did you write unit tests for the new feature? If not, why not? 19 | * [ ] Did the unit tests pass? 20 | * [ ] Did you know that locally running unit tests requires an AWS account? 21 | * You must create a ROLE_ARN environment variable on your machine using `export ROLE_ARN=`. 22 | 23 | ## Submission details 24 | 25 | Describe your changes here. Be detailed! -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Lint, format, and test 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, opened, synchronize, reopened] 6 | branches: 7 | - '*' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | test: 15 | name: Run tests, linting, and formatting 16 | runs-on: ubuntu-latest 17 | env: 18 | ROLE_ARN: ${{ secrets.ROLE_ARN }} 19 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | steps: 22 | - name: Check out repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Python 26 | id: setup-python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.10' 30 | 31 | - name: Install Poetry 32 | uses: snok/install-poetry@v1 33 | with: 34 | virtualenvs-create: true 35 | virtualenvs-in-project: true 36 | virtualenvs-path: .venv 37 | installer-parallel: true 38 | 39 | - name: Cache Poetry dependencies 40 | id: cached-poetry-dependencies 41 | uses: actions/cache@v4 42 | with: 43 | path: .venv 44 | key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock', '**/pyproject.toml') }} 45 | 46 | - name: Install dependencies 47 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 48 | run: poetry install --no-interaction --no-root --all-groups 49 | 50 | - name: Install project 51 | run: poetry install --no-interaction --all-groups 52 | 53 | - name: Check formatting with Black 54 | uses: psf/black@stable 55 | with: 56 | options: ". --check" 57 | 58 | - name: Lint with Flake8 59 | uses: reviewdog/action-flake8@v3 60 | with: 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | - name: Run unit tests 64 | run: | 65 | source .venv/bin/activate 66 | pytest tests/ -v 67 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Bump version, publish to PyPI, tag, and deploy docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "boto3_refresh_session/**" 9 | - "README.md" 10 | - "doc/brs.png" 11 | 12 | jobs: 13 | bump_version: 14 | if: | 15 | !contains(github.event.head_commit.message, '[skip release]') && 16 | github.event_name == 'push' && 17 | github.ref == 'refs/heads/main' 18 | 19 | name: Bump Version, Publish to PyPI, tag, and deploy docs 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Check out repository 23 | uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.GH_PAT }} # Use the PAT for push permissions 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.10' 31 | 32 | - name: Install dependencies 33 | run: | 34 | pip install tomlkit 35 | pipx install poetry 36 | 37 | - name: Determine version part to update 38 | run: | 39 | # Default to patch 40 | VERSION_TYPE="patch" 41 | MESSAGE=$(git log -1 --pretty=%B) 42 | 43 | # Check for version bump keywords 44 | if echo "$MESSAGE" | grep -qi '\[major\]'; then 45 | VERSION_TYPE="major" 46 | elif echo "$MESSAGE" | grep -qi '\[minor\]'; then 47 | VERSION_TYPE="minor" 48 | fi 49 | 50 | echo "Determined VERSION_TYPE=$VERSION_TYPE" 51 | echo "VERSION_TYPE=$VERSION_TYPE" >> $GITHUB_ENV 52 | 53 | - name: Bump version 54 | run: python bump_version.py $VERSION_TYPE 55 | 56 | - name: Commit version bump 57 | run: | 58 | git config --global user.name "github-actions[bot]" 59 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 60 | git add pyproject.toml 61 | git add boto3_refresh_session/__init__.py 62 | git commit -m "bump $VERSION_TYPE version [skip ci]" # <--- Prevents infinite loop 63 | git push origin main # Push only the version bump commit 64 | 65 | - name: Fetch latest commit after push 66 | run: | 67 | git fetch origin main 68 | git reset --hard origin/main 69 | 70 | - name: Create tag 71 | run: | 72 | VERSION=$(grep '^version' pyproject.toml | awk -F'"' '{print $2}') 73 | git tag "$VERSION" 74 | git push origin "$VERSION" 75 | 76 | - name: Build wheel and publish to PyPI 77 | env: 78 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }} 79 | run: | 80 | poetry install --no-interaction --all-groups 81 | poetry build 82 | poetry publish --no-interaction 83 | 84 | - name: Install Poetry 85 | uses: snok/install-poetry@v1 86 | with: 87 | virtualenvs-create: true 88 | virtualenvs-in-project: true 89 | virtualenvs-path: .venv 90 | installer-parallel: true 91 | 92 | - name: Cache Poetry dependencies 93 | id: cached-poetry-dependencies 94 | uses: actions/cache@v4 95 | with: 96 | path: .venv 97 | key: poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock', '**/pyproject.toml') }} 98 | 99 | - name: Install dependencies 100 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 101 | run: poetry install --no-interaction --no-root --all-groups 102 | 103 | - name: Install project 104 | run: poetry install --no-interaction --all-groups 105 | 106 | - name: Build Documentation 107 | run: | 108 | source .venv/bin/activate 109 | cd doc/ && make clean && cd .. 110 | sphinx-build doc _build 111 | 112 | - name: Deploy to GitHub Pages 113 | uses: peaceiris/actions-gh-pages@v3 114 | with: 115 | publish_branch: gh-pages 116 | github_token: ${{ secrets.GITHUB_TOKEN }} 117 | publish_dir: _build/ 118 | force_orphan: true -------------------------------------------------------------------------------- /.github/workflows/update-readme.yml: -------------------------------------------------------------------------------- 1 | name: Update README Download Badge 2 | 3 | on: 4 | schedule: 5 | - cron: "0 11 * * *" # 6 AM EST = 11 AM UTC 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - .github/workflows/update-readme.yml 12 | - readme.py 13 | - README.template.md 14 | 15 | jobs: 16 | update: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Check out repository 21 | uses: actions/checkout@v4 22 | with: 23 | token: ${{ secrets.GH_PAT }} 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.x" 29 | 30 | - name: Install Jinja2 31 | run: pip install Jinja2 requests git+https://github.com/michaelthomasletts/pepy-chart.git 32 | 33 | - name: Render README with PePy stats 34 | run: python3 readme.py 35 | 36 | - name: Update download stats 37 | run: | 38 | pepy-chart -p boto3-refresh-session -op doc/downloads.png 39 | 40 | - name: Commit and push if README and/or downloads.png changed 41 | run: | 42 | git config --global user.name "github-actions[bot]" 43 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 44 | git add -A 45 | if ! git diff --cached --quiet; then 46 | git commit -m "Updating README.md [skip release]" 47 | git pull https://x-access-token:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git main --rebase 48 | git push https://x-access-token:${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git HEAD:main 49 | else 50 | echo "No changes to commit" 51 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | *.DS_Store 29 | .DS_Store 30 | .tox/* 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # UV 101 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | #uv.lock 105 | 106 | # poetry 107 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 108 | # This is especially recommended for binary packages to ensure reproducibility, and is more 109 | # commonly ignored for libraries. 110 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 111 | poetry.lock 112 | .poetry 113 | 114 | # pdm 115 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 116 | #pdm.lock 117 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 118 | # in version control. 119 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 120 | .pdm.toml 121 | .pdm-python 122 | .pdm-build/ 123 | 124 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 125 | __pypackages__/ 126 | 127 | # Celery stuff 128 | celerybeat-schedule 129 | celerybeat.pid 130 | 131 | # SageMath parsed files 132 | *.sage.py 133 | 134 | # Environments 135 | .env 136 | .venv 137 | env/ 138 | venv/ 139 | ENV/ 140 | env.bak/ 141 | venv.bak/ 142 | 143 | # Spyder project settings 144 | .spyderproject 145 | .spyproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | 150 | # mkdocs documentation 151 | /site 152 | 153 | # mypy 154 | .mypy_cache/ 155 | .dmypy.json 156 | dmypy.json 157 | 158 | # Pyre type checker 159 | .pyre/ 160 | 161 | # pytype static type analyzer 162 | .pytype/ 163 | 164 | # Cython debug symbols 165 | cython_debug/ 166 | 167 | # PyCharm 168 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 169 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 170 | # and can be added to the global gitignore or merged into this file. For a more nuclear 171 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 172 | #.idea/ 173 | 174 | # PyPI configuration file 175 | .pypirc 176 | 177 | # docs 178 | doc/_build 179 | _build -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | 4 | repos: 5 | - repo: https://github.com/psf/black 6 | rev: 24.10.0 7 | hooks: 8 | - id: black 9 | alias: black 10 | args: ['--config=pyproject.toml'] 11 | - repo: https://github.com/PyCQA/flake8 12 | rev: 7.1.1 13 | hooks: 14 | - id: flake8 15 | alias: flake8 16 | args: ['--config=.flake8'] 17 | - repo: https://github.com/PyCQA/isort 18 | rev: 5.13.2 19 | hooks: 20 | - id: isort 21 | alias: isort 22 | - repo: local 23 | hooks: 24 | - id: pytest 25 | name: pytest 26 | alias: pytest 27 | types: [python] 28 | entry: python -m pytest -v tests/ -s 29 | language: system 30 | always_run: true 31 | pass_filenames: false -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mike Letts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | boto3-refresh-session 2 | Copyright 2024 Michael Letts 3 | 4 | boto3-refresh-session (BRS) includes software designed and developed by Michael Letts (the author). 5 | 6 | Although the author was formerly employed by Amazon, this project was conceived, designed, developed, and released independently after that period of employment. It is not affiliated with or endorsed by Amazon Web Services (AWS), Amazon, or any contributors to boto3 or botocore, regardless of their employment status. 7 | 8 | Developers are welcome and encouraged to modify and adapt this software to suit their needs — this is in fact already common practice among some of the largest users of BRS. 9 | 10 | If you find boto3-refresh-session (BRS) helpful in your work, a note of thanks or a mention in your project’s documentation or acknowledgments is appreciated — though not required under the terms of the MIT License. 11 | 12 | Licensed under the MIT License. See the LICENSE file for details. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically. 9 |
10 | 11 |
12 | 13 |
14 | 15 | 16 | PyPI - Version 17 | 18 | 19 | 20 | Python Version 21 | 22 | 23 | 24 | Workflow 25 | 26 | 27 | 28 | GitHub last commit 29 | 30 | 31 | 32 | Stars 33 | 34 | 35 | 36 | Downloads 37 | 38 | 39 | 40 | Documentation Badge 41 | 42 | 43 | 44 | Source Code Badge 45 | 46 | 47 | 48 | Q&A Badge 49 | 50 | 51 |
52 | 53 | ## Features 54 | 55 | - Auto-refreshing credentials for long-lived `boto3` sessions 56 | - Drop-in replacement for `boto3.session.Session` 57 | - Supports `assume_role` configuration, custom STS clients, and profile / region configuration, as well as all other parameters supported by `boto3.session.Session` 58 | - Tested, documented, and published to PyPI 59 | 60 | ## Recognition, Adoption, and Testimonials 61 | 62 | [Featured in TL;DR Sec.](https://tldrsec.com/p/tldr-sec-282) 63 | 64 | Recognized during AWS Community Day Midwest on June 5th, 2025. 65 | 66 | A testimonial from a Cyber Security Engineer at a FAANG company: 67 | 68 | > _Most of my work is on tooling related to AWS security, so I'm pretty choosy about boto3 credentials-adjacent code. I often opt to just write this sort of thing myself so I at least know that I can reason about it. But I found boto3-refresh-session to be very clean and intuitive [...] We're using the RefreshableSession class as part of a client cache construct [...] We're using AWS Lambda to perform lots of operations across several regions in hundreds of accounts, over and over again, all day every day. And it turns out that there's a surprising amount of overhead to creating boto3 clients (mostly deserializing service definition json), so we can run MUCH more efficiently if we keep a cache of clients, all equipped with automatically refreshing sessions._ 69 | 70 | The following line plot illustrates the adoption of BRS over the last three months in terms of average daily downloads over a rolling seven day window. 71 | 72 |

73 | 74 |

75 | 76 | ## Installation 77 | 78 | ```bash 79 | pip install boto3-refresh-session 80 | ``` 81 | 82 | ## Usage 83 | 84 | ```python 85 | import boto3_refresh_session as brs 86 | 87 | # you can pass all of the params associated with boto3.session.Session 88 | profile_name = '' 89 | region_name = 'us-east-1' 90 | ... 91 | 92 | # as well as all of the params associated with STS.Client.assume_role 93 | assume_role_kwargs = { 94 | 'RoleArn': '', 95 | 'RoleSessionName': '', 96 | 'DurationSeconds': '', 97 | ... 98 | } 99 | 100 | # as well as all of the params associated with STS.Client, except for 'service_name' 101 | sts_client_kwargs = { 102 | 'region_name': region_name, 103 | ... 104 | } 105 | 106 | # basic initialization of boto3.session.Session 107 | session = brs.RefreshableSession( 108 | assume_role_kwargs=assume_role_kwargs, # required 109 | sts_client_kwargs=sts_client_kwargs, 110 | region_name=region_name, 111 | profile_name=profile_name, 112 | ... 113 | ) 114 | 115 | # now you can create clients, resources, etc. without worrying about expired temporary 116 | # security credentials 117 | s3 = session.client(service_name='s3') 118 | buckets = s3.list_buckets() 119 | ``` 120 | 121 | ## Raison d'être 122 | 123 | Long-running data pipelines, security tooling, ETL jobs, and cloud automation scripts frequently interact with the AWS API using boto3 — and often run into the same problem: 124 | 125 | **Temporary credentials expire.** 126 | 127 | When that happens, engineers typically fall back on one of two strategies: 128 | 129 | - Wrapping AWS calls in try/except blocks that catch ClientError exceptions 130 | - Writing ad hoc logic to refresh credentials using botocore credentials internals 131 | 132 | Both approaches are fragile, tedious to maintain, and error-prone at scale. 133 | 134 | Over the years, I noticed that every company I worked for — whether a scrappy startup or FAANG — ended up with some variation of the same pattern: 135 | a small in-house module to manage credential refresh, written in haste, duplicated across services, and riddled with edge cases. Things only 136 | got more strange and difficult when I needed to run things in parallel. 137 | 138 | Eventually, I decided to build boto3-refresh-session as a proper open-source Python package: 139 | 140 | - Fully tested 141 | - Extensible 142 | - Integrated with boto3 idioms 143 | - Equipped with automatic documentation and CI tooling 144 | 145 | **The goal:** to solve a real, recurring problem once — cleanly, consistently, and for everyone - with multiple refresh strategies. 146 | 147 | If you've ever written the same AWS credential-refresh boilerplate more than once, this library is for you. -------------------------------------------------------------------------------- /README.template.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically. 9 |
10 | 11 |
12 | 13 |
14 | 15 | 16 | PyPI - Version 17 | 18 | 19 | 20 | Python Version 21 | 22 | 23 | 24 | Workflow 25 | 26 | 27 | 28 | GitHub last commit 29 | 30 | 31 | 32 | Stars 33 | 34 | 35 | 36 | Downloads 37 | 38 | 39 | 40 | Documentation Badge 41 | 42 | 43 | 44 | Source Code Badge 45 | 46 | 47 | 48 | Q&A Badge 49 | 50 | 51 |
52 | 53 | ## Features 54 | 55 | - Auto-refreshing credentials for long-lived `boto3` sessions 56 | - Drop-in replacement for `boto3.session.Session` 57 | - Supports `assume_role` configuration, custom STS clients, and profile / region configuration, as well as all other parameters supported by `boto3.session.Session` 58 | - Tested, documented, and published to PyPI 59 | 60 | ## Recognition, Adoption, and Testimonials 61 | 62 | [Featured in TL;DR Sec.](https://tldrsec.com/p/tldr-sec-282) 63 | 64 | Recognized during AWS Community Day Midwest on June 5th, 2025. 65 | 66 | A testimonial from a Cyber Security Engineer at a FAANG company: 67 | 68 | > _Most of my work is on tooling related to AWS security, so I'm pretty choosy about boto3 credentials-adjacent code. I often opt to just write this sort of thing myself so I at least know that I can reason about it. But I found boto3-refresh-session to be very clean and intuitive [...] We're using the RefreshableSession class as part of a client cache construct [...] We're using AWS Lambda to perform lots of operations across several regions in hundreds of accounts, over and over again, all day every day. And it turns out that there's a surprising amount of overhead to creating boto3 clients (mostly deserializing service definition json), so we can run MUCH more efficiently if we keep a cache of clients, all equipped with automatically refreshing sessions._ 69 | 70 | The following line plot illustrates the adoption of BRS over the last three months in terms of average daily downloads over a rolling seven day window. 71 | 72 |

73 | 74 |

75 | 76 | ## Installation 77 | 78 | ```bash 79 | pip install boto3-refresh-session 80 | ``` 81 | 82 | ## Usage 83 | 84 | ```python 85 | import boto3_refresh_session as brs 86 | 87 | # you can pass all of the params associated with boto3.session.Session 88 | profile_name = '' 89 | region_name = 'us-east-1' 90 | ... 91 | 92 | # as well as all of the params associated with STS.Client.assume_role 93 | assume_role_kwargs = { 94 | 'RoleArn': '', 95 | 'RoleSessionName': '', 96 | 'DurationSeconds': '', 97 | ... 98 | } 99 | 100 | # as well as all of the params associated with STS.Client, except for 'service_name' 101 | sts_client_kwargs = { 102 | 'region_name': region_name, 103 | ... 104 | } 105 | 106 | # basic initialization of boto3.session.Session 107 | session = brs.RefreshableSession( 108 | assume_role_kwargs=assume_role_kwargs, # required 109 | sts_client_kwargs=sts_client_kwargs, 110 | region_name=region_name, 111 | profile_name=profile_name, 112 | ... 113 | ) 114 | 115 | # now you can create clients, resources, etc. without worrying about expired temporary 116 | # security credentials 117 | s3 = session.client(service_name='s3') 118 | buckets = s3.list_buckets() 119 | ``` 120 | 121 | ## Raison d'être 122 | 123 | Long-running data pipelines, security tooling, ETL jobs, and cloud automation scripts frequently interact with the AWS API using boto3 — and often run into the same problem: 124 | 125 | **Temporary credentials expire.** 126 | 127 | When that happens, engineers typically fall back on one of two strategies: 128 | 129 | - Wrapping AWS calls in try/except blocks that catch ClientError exceptions 130 | - Writing ad hoc logic to refresh credentials using botocore credentials internals 131 | 132 | Both approaches are fragile, tedious to maintain, and error-prone at scale. 133 | 134 | Over the years, I noticed that every company I worked for — whether a scrappy startup or FAANG — ended up with some variation of the same pattern: 135 | a small in-house module to manage credential refresh, written in haste, duplicated across services, and riddled with edge cases. Things only 136 | got more strange and difficult when I needed to run things in parallel. 137 | 138 | Eventually, I decided to build boto3-refresh-session as a proper open-source Python package: 139 | 140 | - Fully tested 141 | - Extensible 142 | - Integrated with boto3 idioms 143 | - Equipped with automatic documentation and CI tooling 144 | 145 | **The goal:** to solve a real, recurring problem once — cleanly, consistently, and for everyone - with multiple refresh strategies. 146 | 147 | If you've ever written the same AWS credential-refresh boilerplate more than once, this library is for you. -------------------------------------------------------------------------------- /boto3_refresh_session/__init__.py: -------------------------------------------------------------------------------- 1 | from .session import RefreshableSession 2 | from .sts import STSRefreshableSession 3 | 4 | __all__ = ["RefreshableSession"] 5 | __version__ = "1.1.3" 6 | __author__ = "Mike Letts" 7 | -------------------------------------------------------------------------------- /boto3_refresh_session/session.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __doc__ = """ 4 | boto3_refresh_session.session 5 | ============================= 6 | 7 | This module provides the main interface for constructing refreshable boto3 sessions. 8 | 9 | The ``RefreshableSession`` class serves as a factory that dynamically selects the appropriate 10 | credential refresh strategy based on the ``method`` parameter, e.g., ``sts``. 11 | 12 | Users can interact with AWS services just like they would with a normal :class:`boto3.session.Session`, 13 | with the added benefit of automatic credential refreshing. 14 | 15 | Examples 16 | -------- 17 | >>> from boto3_refresh_session import RefreshableSession 18 | >>> session = RefreshableSession( 19 | ... assume_role_kwargs={"RoleArn": "...", "RoleSessionName": "..."}, 20 | ... region_name="us-east-1" 21 | ... ) 22 | >>> s3 = session.client("s3") 23 | >>> s3.list_buckets() 24 | 25 | .. seealso:: 26 | :class:`boto3_refresh_session.sts.STSRefreshableSession` 27 | 28 | Factory interface 29 | ----------------- 30 | .. autosummary:: 31 | :toctree: generated/ 32 | :nosignatures: 33 | 34 | RefreshableSession 35 | """ 36 | 37 | __all__ = ["RefreshableSession"] 38 | 39 | from abc import ABC, abstractmethod 40 | from typing import Any, Callable, ClassVar, Literal, get_args 41 | from warnings import warn 42 | 43 | from boto3.session import Session 44 | from botocore.credentials import ( 45 | DeferredRefreshableCredentials, 46 | RefreshableCredentials, 47 | ) 48 | 49 | #: Type alias for all currently available credential refresh methods. 50 | Method = Literal["sts"] 51 | RefreshMethod = Literal["sts-assume-role"] 52 | 53 | 54 | class BaseRefreshableSession(ABC, Session): 55 | """Abstract base class for implementing refreshable AWS sessions. 56 | 57 | Provides a common interface and factory registration mechanism 58 | for subclasses that generate temporary credentials using various 59 | AWS authentication methods (e.g., STS). 60 | 61 | Subclasses must implement ``_get_credentials()`` and ``get_identity()``. 62 | They should also register themselves using the ``method=...`` argument 63 | to ``__init_subclass__``. 64 | 65 | Parameters 66 | ---------- 67 | registry : dict[str, type[BaseRefreshableSession]] 68 | Class-level registry mapping method names to registered session types. 69 | """ 70 | 71 | # adding this and __init_subclass__ to avoid circular imports 72 | # as well as simplify future addition of new methods 73 | registry: ClassVar[dict[Method, type[BaseRefreshableSession]]] = {} 74 | 75 | def __init_subclass__(cls, method: Method): 76 | super().__init_subclass__() 77 | 78 | # guarantees that methods are unique 79 | if method in BaseRefreshableSession.registry: 80 | warn(f"Method '{method}' is already registered. Overwriting.") 81 | 82 | BaseRefreshableSession.registry[method] = cls 83 | 84 | def __init__(self, **kwargs): 85 | super().__init__(**kwargs) 86 | 87 | @abstractmethod 88 | def _get_credentials(self) -> dict[str, str]: ... 89 | 90 | @abstractmethod 91 | def get_identity(self) -> dict[str, Any]: ... 92 | 93 | def _refresh_using( 94 | self, 95 | credentials_method: Callable, 96 | defer_refresh: bool, 97 | refresh_method: RefreshMethod, 98 | ): 99 | # determining how exactly to refresh expired temporary credentials 100 | if not defer_refresh: 101 | self._credentials = RefreshableCredentials.create_from_metadata( 102 | metadata=credentials_method(), 103 | refresh_using=credentials_method, 104 | method=refresh_method, 105 | ) 106 | else: 107 | self._credentials = DeferredRefreshableCredentials( 108 | refresh_using=credentials_method, method=refresh_method 109 | ) 110 | 111 | 112 | class RefreshableSession: 113 | """Factory class for constructing refreshable boto3 sessions using various authentication 114 | methods, e.g. STS. 115 | 116 | This class provides a unified interface for creating boto3 sessions whose credentials are 117 | automatically refreshed in the background. 118 | 119 | Use ``RefreshableSession(method="...")`` to construct an instance using the desired method. 120 | 121 | For additional information on required parameters, refer to the See Also section below. 122 | 123 | Parameters 124 | ---------- 125 | method : Method 126 | The authentication and refresh method to use for the session. Must match a registered method name. 127 | Default is "sts". 128 | 129 | Other Parameters 130 | ---------------- 131 | **kwargs : dict 132 | Additional keyword arguments forwarded to the constructor of the selected session class. 133 | 134 | See Also 135 | -------- 136 | boto3_refresh_session.sts.STSRefreshableSession 137 | """ 138 | 139 | def __new__( 140 | cls, method: Method = "sts", **kwargs 141 | ) -> BaseRefreshableSession: 142 | obj = BaseRefreshableSession.registry[method] 143 | return obj(**kwargs) 144 | 145 | @classmethod 146 | def get_available_methods(cls) -> list[str]: 147 | """Lists all currently available credential refresh methods. 148 | 149 | Returns 150 | ------- 151 | list[str] 152 | A list of all currently available credential refresh methods, e.g. 'sts'. 153 | """ 154 | 155 | return list(get_args(Method)) 156 | -------------------------------------------------------------------------------- /boto3_refresh_session/sts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __doc__ = """ 4 | boto3_refresh_session.sts 5 | ========================= 6 | 7 | Implements the STS-based credential refresh strategy for use with 8 | :class:`boto3_refresh_session.session.RefreshableSession`. 9 | 10 | This module defines the :class:`STSRefreshableSession` class, which uses 11 | IAM role assumption via STS to automatically refresh temporary credentials 12 | in the background. 13 | 14 | .. versionadded:: 1.1.0 15 | 16 | Examples 17 | -------- 18 | >>> from boto3_refresh_session import RefreshableSession 19 | >>> session = RefreshableSession( 20 | ... method="sts", 21 | ... assume_role_kwargs={ 22 | ... "RoleArn": "arn:aws:iam::123456789012:role/MyRole", 23 | ... "RoleSessionName": "my-session" 24 | ... }, 25 | ... region_name="us-east-1" 26 | ... ) 27 | >>> s3 = session.client("s3") 28 | >>> s3.list_buckets() 29 | 30 | .. seealso:: 31 | :class:`boto3_refresh_session.session.RefreshableSession` 32 | 33 | STS 34 | --- 35 | 36 | .. autosummary:: 37 | :toctree: generated/ 38 | :nosignatures: 39 | 40 | STSRefreshableSession 41 | """ 42 | __all__ = ["STSRefreshableSession"] 43 | 44 | from typing import Any 45 | from warnings import warn 46 | 47 | from .session import BaseRefreshableSession 48 | 49 | 50 | class STSRefreshableSession(BaseRefreshableSession, method="sts"): 51 | """A :class:`boto3.session.Session` object that automatically refreshes temporary AWS 52 | credentials using an IAM role that is assumed via STS. 53 | 54 | Parameters 55 | ---------- 56 | assume_role_kwargs : dict 57 | Required keyword arguments for :meth:`STS.Client.assume_role` (i.e. boto3 STS client). 58 | defer_refresh : bool, optional 59 | If ``True`` then temporary credentials are not automatically refreshed until 60 | they are explicitly needed. If ``False`` then temporary credentials refresh 61 | immediately upon expiration. It is highly recommended that you use ``True``. 62 | Default is ``True``. 63 | sts_client_kwargs : dict, optional 64 | Optional keyword arguments for the :class:`STS.Client` object. Do not provide 65 | values for ``service_name`` as they are unnecessary. Default is None. 66 | 67 | Other Parameters 68 | ---------------- 69 | kwargs : dict 70 | Optional keyword arguments for the :class:`boto3.session.Session` object. 71 | """ 72 | 73 | def __init__( 74 | self, 75 | assume_role_kwargs: dict, 76 | defer_refresh: bool = None, 77 | sts_client_kwargs: dict | None = None, 78 | **kwargs, 79 | ): 80 | super().__init__(**kwargs) 81 | defer_refresh = defer_refresh is not False 82 | self.assume_role_kwargs = assume_role_kwargs 83 | 84 | if sts_client_kwargs is not None: 85 | # overwriting 'service_name' in case it appears in sts_client_kwargs 86 | if "service_name" in sts_client_kwargs: 87 | warn( 88 | "The sts_client_kwargs parameter cannot contain values for service_name. Reverting to service_name = 'sts'." 89 | ) 90 | del sts_client_kwargs["service_name"] 91 | self._sts_client = self.client( 92 | service_name="sts", **sts_client_kwargs 93 | ) 94 | else: 95 | self._sts_client = self.client(service_name="sts") 96 | 97 | # mounting refreshable credentials 98 | self._refresh_using( 99 | credentials_method=self._get_credentials, 100 | defer_refresh=defer_refresh, 101 | refresh_method="sts-assume-role", 102 | ) 103 | 104 | def _get_credentials(self) -> dict[str, str]: 105 | temporary_credentials = self._sts_client.assume_role( 106 | **self.assume_role_kwargs 107 | )["Credentials"] 108 | return { 109 | "access_key": temporary_credentials.get("AccessKeyId"), 110 | "secret_key": temporary_credentials.get("SecretAccessKey"), 111 | "token": temporary_credentials.get("SessionToken"), 112 | "expiry_time": temporary_credentials.get("Expiration").isoformat(), 113 | } 114 | 115 | def get_identity(self) -> dict[str, Any]: 116 | """Returns metadata about the identity assumed. 117 | 118 | Returns 119 | ------- 120 | dict[str, Any] 121 | Dict containing caller identity according to AWS STS. 122 | """ 123 | 124 | return self._sts_client.get_caller_identity() 125 | -------------------------------------------------------------------------------- /bump_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import re 5 | import sys 6 | from pathlib import Path 7 | 8 | import tomlkit 9 | 10 | HELP_MSG = """Which part of the version to bump (default: patch). 11 | Example: python bump_version.py minor""" 12 | 13 | 14 | def bump_version(version: str, part: str): 15 | """Bumps version according to the part parameter.""" 16 | 17 | major, minor, patch = map(int, version.split(".")) 18 | 19 | if part == "major": 20 | major += 1 21 | minor = patch = 0 22 | elif part == "minor": 23 | minor += 1 24 | patch = 0 25 | elif part == "patch": 26 | patch += 1 27 | else: 28 | print("Invalid part. Use 'major', 'minor', or 'patch'.") 29 | sys.exit(1) 30 | 31 | return f"{major}.{minor}.{patch}" 32 | 33 | 34 | def run(part: str): 35 | """Runs the bump_version method using the part parameter.""" 36 | 37 | # reading current version from pyproject.toml 38 | path = Path("pyproject.toml") 39 | pyproject = path.read_text(encoding="utf-8") 40 | pyproject = tomlkit.parse(pyproject) 41 | 42 | # bumping version 43 | new_version = bump_version( 44 | current_version := str(pyproject["project"]["version"]), part 45 | ) 46 | pyproject["project"]["version"] = new_version 47 | 48 | # writing bumped version to pyproject.toml 49 | path.write_text(tomlkit.dumps(pyproject), encoding="utf-8") 50 | 51 | print( 52 | f"Version bumped from {current_version} to {new_version} in {path.name}" 53 | ) 54 | 55 | # writing bumped version to __init__.py 56 | path = Path("boto3_refresh_session/__init__.py") 57 | path.write_text(re.sub(r"\d+\.\d+\.\d+", new_version, path.read_text())) 58 | 59 | print( 60 | f"Version bumped from {current_version} to {new_version} in {path.name}" 61 | ) 62 | 63 | 64 | if __name__ == "__main__": 65 | parser = argparse.ArgumentParser(description="Bump the project version.") 66 | parser.add_argument( 67 | "part", 68 | choices=["major", "minor", "patch"], 69 | default="patch", 70 | nargs="?", 71 | help=HELP_MSG, 72 | ) 73 | args = parser.parse_args() 74 | run(part=args.part) 75 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;1,400&display=swap'); 2 | 3 | body { 4 | font-family: 'Lato', sans-serif; 5 | font-size: 18px; 6 | } 7 | 8 | html[data-theme="dark"] img { 9 | filter: none; 10 | } 11 | html[data-theme="dark"] .bd-content img:not(.only-dark):not(.dark-light) { 12 | background: unset; 13 | } 14 | 15 | section { 16 | margin-bottom: 2rem; 17 | } 18 | 19 | dl.field-list, div.seealso { 20 | margin-top: 1.5rem; 21 | margin-bottom: 1.5rem; 22 | } 23 | 24 | code { 25 | background-color: rgba(200, 200, 200, 0.07); 26 | padding: 0.1em 0.2em; 27 | } 28 | 29 | h1 { 30 | font-size: 2.2rem; 31 | } 32 | 33 | h2 { 34 | font-size: 1.6rem; 35 | } 36 | 37 | div.highlight pre { 38 | padding: 1em; 39 | } 40 | 41 | .bd-content { 42 | max-width: 90ch; 43 | } 44 | -------------------------------------------------------------------------------- /doc/authorization.rst: -------------------------------------------------------------------------------- 1 | .. _authorization: 2 | 3 | Authorization 4 | ************* 5 | 6 | In order to use this package, it is **recommended** that you follow one of the 7 | below methods for authorizing access to your AWS instance: 8 | 9 | - Create local environment variables containing your credentials, 10 | e.g. ``ACCESS_KEY``, ``SECRET_KEY``, and ``SESSION_TOKEN``. 11 | - Create a shared credentials file, i.e. ``~/.aws/credentials``. 12 | - Create an AWS config file, i.e. ``~/.aws/config``. 13 | 14 | For additional details concerning how to authorize access, check the 15 | `boto3 documentation `_. 16 | 17 | For additional details concerning how to configure an AWS credentials file 18 | on your machine, check the `AWS CLI documentation `_. -------------------------------------------------------------------------------- /doc/brs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelthomasletts/boto3-refresh-session/f05847a63d41bc78e08a0e49d0901545729c965a/doc/brs.png -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from datetime import date 4 | from pathlib import Path 5 | 6 | import tomlkit 7 | 8 | # fetching pyproject.toml 9 | path = Path("../pyproject.toml") 10 | 11 | with path.open("r", encoding="utf-8") as f: 12 | pyproject = tomlkit.parse(f.read()) 13 | 14 | # sphinx config 15 | sys.path.insert(0, os.path.abspath(".")) 16 | sys.path.insert(0, os.path.abspath("..")) 17 | extensions = [ 18 | "sphinx.ext.autodoc", 19 | "numpydoc", 20 | "sphinx.ext.intersphinx", 21 | "sphinx.ext.napoleon", 22 | "sphinx.ext.autosummary", 23 | "sphinx.ext.linkcode", 24 | "sphinx.ext.extlinks", 25 | ] 26 | language = "en" 27 | project = str(pyproject["project"]["name"]) 28 | author = "Michael Letts" 29 | copyright = f"{date.today().year}, {author}" 30 | release = str(pyproject["project"]["version"]) 31 | source_encoding = "utf-8" 32 | source_suffix = ".rst" 33 | pygments_style = "sphinx" 34 | add_function_parentheses = False 35 | templates_path = ["_templates"] 36 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "tests/"] 37 | html_logo = "brs.png" 38 | html_favicon = html_logo 39 | html_title = project 40 | html_theme = "pydata_sphinx_theme" 41 | html_static_path = ["_static"] 42 | html_file_suffix = ".html" 43 | html_sidebars = { 44 | "index": [], 45 | "usage": [], 46 | "authorization": [], 47 | "contributing": [], 48 | "raison": [], 49 | "qanda": [], 50 | "installation": [], 51 | "modules/**": ["sidebar-nav-bs.html", "search-field.html"], 52 | } 53 | html_context = { 54 | "default_mode": "dark", 55 | } 56 | htmlhelp_basename = project 57 | html_css_files = ["custom.css"] 58 | html_theme_options = { 59 | "collapse_navigation": True, 60 | "navbar_end": [ 61 | "search-button", 62 | "navbar-icon-links.html", 63 | ], 64 | "icon_links": [ 65 | { 66 | "name": "GitHub", 67 | "url": f"https://github.com/michaelthomasletts/{project}", 68 | "icon": "fab fa-github-square", 69 | "type": "fontawesome", 70 | }, 71 | { 72 | "name": "PyPI", 73 | "url": f"https://pypi.org/project/{project}/", 74 | "icon": "fab fa-python", 75 | "type": "fontawesome", 76 | }, 77 | ], 78 | } 79 | 80 | # autodoc config 81 | autodoc_default_options = { 82 | "members": True, 83 | "member-order": "bysource", 84 | "exclude-members": "__init__,__new__", 85 | } 86 | autodoc_typehints = "none" 87 | autodoc_preserve_defaults = False 88 | autodoc_class_signature = "separated" 89 | 90 | # numpydoc config 91 | numpydoc_show_class_members = False 92 | numpydoc_show_inherited_class_members = False 93 | numpydoc_attributes_as_param_list = False 94 | numpydoc_class_members_toctree = False 95 | 96 | # napoleon config 97 | napoleon_numpy_docstring = True 98 | napoleon_include_init_with_doc = False 99 | 100 | # autosummary 101 | autosummary_generate = False 102 | 103 | # intersphinx 104 | intersphinx_mapping = { 105 | "boto3": ( 106 | "https://boto3.amazonaws.com/v1/documentation/api/latest/", 107 | None, 108 | ), 109 | } 110 | extlinks = { 111 | "botocore": ( 112 | "https://botocore.amazonaws.com/v1/documentation/api/latest/%s", 113 | "", 114 | ), 115 | } 116 | 117 | 118 | def linkcode_resolve(domain, info): 119 | """Resolves 'source' link in documentation.""" 120 | 121 | if domain != "py": 122 | return None 123 | if not info["module"]: 124 | return None 125 | filename = info["module"].replace(".", "/") 126 | return f"https://github.com/michaelthomasletts/{project}/blob/main/{filename}.py" 127 | -------------------------------------------------------------------------------- /doc/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thank you for choosing to contribute to this project! 5 | 6 | Please follow the below documentation closely. 7 | 8 | Requirements 9 | ------------ 10 | 11 | - Python 3.10+ 12 | - An AWS account 13 | - An IAM role with ``s3:ListBuckets`` permissions 14 | - A local environment variable named ``ROLE_ARN`` containing your IAM role ARN 15 | 16 | Steps 17 | ----- 18 | 19 | 1. Fork this project. 20 | 2. Clone the newly created (i.e. forked) repository in your account to your local machine. 21 | 3. From your terminal, navigate to where the forked repository was cloned to your local machine, i.e. ``cd boto3-refresh-session``. 22 | 4. Run ``poetry install --all-groups`` 23 | 24 | * This command installs all developer and package dependencies. 25 | 26 | 5. Run ``pre-commit install && pre-commit install-hooks`` 27 | 28 | * This command installs all pre-commit hooks. 29 | 30 | 6. Create a local environment variable containing your role ARN from your terminal by running ``export ROLE_ARN=`` 31 | 7. Make your changes. 32 | 33 | * You will be met by a pull request checklist when you attempt to create a pull request with your changes. Follow that checklist to ensure your changes satisfy the requirements in order to expedite the review process. 34 | 35 | 8. If your changes include an additional dependency, then you will need to run ``poetry add ``. This command will update ``pyproject.toml`` with your dependency. 36 | 9. Commit and push your changes to a branch on the forked repository. 37 | 38 | * ``pre-commit`` will run a few checks when ``git commit`` is run. Those checks **must** succeed for you to proceed to ``git push``! 39 | 40 | 10. Open a pull request that compares your forked repository branch with the ``main`` branch of the production repository. 41 | 11. Upon creation (or update), your pull request will: 42 | 43 | * Trigger status checks 44 | 45 | * .. warning:: 46 | **Forked pull requests cannot use repository secrets!** Therefore, unit tests cannot be performed via Github Actions! Please bear with the codeowners as they evaluate your work, due to that limitation. Include screenshots of successful local ``pre-commit`` runs in order to expedite the review process! Apologies -- this limitation of Github is steadfast, and the codeowners are looking for additional strategies for circumventing this limitation in the meantime. We understand it is frustrating that status checks will fail, no matter what, until a solution is found. 47 | 48 | * Require code owner approval in order to be merged 49 | 50 | 12. Make and submit additional changes, if requested; else, merge your approved pull request. -------------------------------------------------------------------------------- /doc/downloads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelthomasletts/boto3-refresh-session/f05847a63d41bc78e08a0e49d0901545729c965a/doc/downloads.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. image:: brs.png 2 | :align: center 3 | 4 | | 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | :hidden: 9 | 10 | Raison d'Être 11 | Q&A 12 | Installation 13 | Usage 14 | Modules 15 | Authorization 16 | Contributing 17 | 18 | boto3-refresh-session 19 | --------------------- 20 | 21 | **Version:** |release| 22 | 23 | **Useful Links:** 24 | :ref:`Raison d'Être ` | 25 | :ref:`Q&A ` | 26 | :ref:`Installation ` | 27 | :ref:`Usage ` | 28 | :ref:`Modules ` | 29 | :ref:`Authorization ` 30 | 31 | **Authors:** `Mike Letts `_ 32 | 33 | boto3-refresh-session is a simple Python package for refreshing the temporary security credentials in a :class:`boto3.session.Session` object automatically. -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installation 4 | ************ 5 | 6 | To install `boto3-refresh-session `_ using ``pip``: 7 | 8 | .. code-block:: bash 9 | 10 | pip install boto3-refresh-session -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/modules/generated/boto3_refresh_session.session.RefreshableSession.rst: -------------------------------------------------------------------------------- 1 | boto3\_refresh\_session.session.RefreshableSession 2 | ================================================== 3 | 4 | .. currentmodule:: boto3_refresh_session.session 5 | 6 | .. autoclass:: RefreshableSession 7 | :exclude-members: __init__, __new__ 8 | 9 | .. rubric:: Methods 10 | 11 | .. autosummary:: 12 | 13 | ~RefreshableSession.get_available_methods 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /doc/modules/generated/boto3_refresh_session.sts.STSRefreshableSession.rst: -------------------------------------------------------------------------------- 1 | boto3\_refresh\_session.sts.STSRefreshableSession 2 | ================================================= 3 | 4 | .. currentmodule:: boto3_refresh_session.sts 5 | 6 | .. autoclass:: STSRefreshableSession 7 | :exclude-members: __init__, __new__ 8 | 9 | .. rubric:: Methods 10 | 11 | .. autosummary:: 12 | 13 | ~STSRefreshableSession.get_identity -------------------------------------------------------------------------------- /doc/modules/index.rst: -------------------------------------------------------------------------------- 1 | .. _modules: 2 | 3 | .. currentmodule:: boto3_refresh_session 4 | 5 | Modules 6 | ======= 7 | 8 | boto3-refresh-session includes multiple modules, grouped into two categories: 9 | 10 | - The core interface (session) 11 | - Individual modules for each supported refresh strategy (e.g., STS) 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | :hidden: 16 | 17 | session 18 | sts 19 | 20 | Core interface 21 | -------------- 22 | 23 | Basic usage of boto3-refresh-session requires familiarity only with the `session` module. 24 | The :class:`boto3_refresh_session.session.RefreshableSession` class provides a unified interface for all supported credential refresh strategies. 25 | 26 | .. tip:: 27 | 28 | For most users, STS is sufficient — there’s no need to manually specify the ``method`` parameter unless using advanced strategies like IoT or SSO. 29 | 30 | - :ref:`session` — Factory interface for creating refreshable boto3 sessions 31 | 32 | Refresh strategies 33 | ------------------ 34 | 35 | boto3-refresh-session is designed to grow. 36 | In addition to the default STS strategy, support for additional credential sources like AWS IoT, SSO, and others will be added in future releases. 37 | 38 | Each strategy supported by boto3-refresh-session is encapsulated in its own module: 39 | 40 | - :ref:`sts` — Refresh strategy using :class:`botocore.client.STS` 41 | -------------------------------------------------------------------------------- /doc/modules/session.rst: -------------------------------------------------------------------------------- 1 | .. _session: 2 | 3 | .. currentmodule:: boto3_refresh_session.session 4 | 5 | .. automodule:: boto3_refresh_session.session 6 | :no-members: 7 | :no-inherited-members: 8 | :no-special-members: -------------------------------------------------------------------------------- /doc/modules/sts.rst: -------------------------------------------------------------------------------- 1 | .. _sts: 2 | 3 | .. currentmodule:: boto3_refresh_session.sts 4 | 5 | .. automodule:: boto3_refresh_session.sts 6 | :no-members: 7 | :no-inherited-members: 8 | :no-special-members: -------------------------------------------------------------------------------- /doc/qanda.rst: -------------------------------------------------------------------------------- 1 | .. _qanda: 2 | 3 | Q&A 4 | --- 5 | 6 | Answers to common questions (and criticisms) about boto3-refresh-session. 7 | 8 | Doesn't boto3 already refresh temporary credentials? 9 | ==================================================== 10 | 11 | **No.** 12 | 13 | Botocore provides methods for *manually* refreshing temporary credentials. 14 | These methods are used internally by boto3-refresh-session, but must otherwise be applied *explicitly* by developers. 15 | 16 | There is **no built-in mechanism** in boto3 for *automatically* refreshing credentials. 17 | This omission can be problematic in production systems. 18 | 19 | The boto3 team has historically declined to support this feature — 20 | despite its availability in other SDKs like 21 | `aws-sdk-go-v2 `_. 22 | 23 | boto3-refresh-session was created specifically to address this gap. 24 | 25 | Is this package really necessary? 26 | ================================= 27 | 28 | If you’re willing to manage temporary credentials yourself, maybe not. 29 | 30 | But if you’d rather avoid boilerplate and use an actively maintained solution, boto3-refresh-session provides a drop-in interface that does the right thing — automatically. 31 | 32 | How are people using boto3-refresh-session? 33 | =========================================== 34 | 35 | Here’s a testimonial from a cybersecurity engineer at a FAANG company: 36 | 37 | *"Most of my work is on tooling related to AWS security, so I'm pretty choosy about boto3 credentials-adjacent code. 38 | I often opt to just write this sort of thing myself so I at least know that I can reason about it. 39 | But I found boto3-refresh-session to be very clean and intuitive. 40 | We're using the `RefreshableSession` class as part of a client cache construct. 41 | We're using AWS Lambda to perform lots of operations across several regions in hundreds of accounts, all day every day. 42 | And it turns out there's a surprising amount of overhead to creating boto3 clients (mostly deserializing service definition JSON), 43 | so we run MUCH more efficiently if we cache clients — all equipped with automatically refreshing sessions."* 44 | 45 | Why aren’t most constructor parameters exposed as attributes? 46 | ============================================================= 47 | 48 | Good question. 49 | 50 | boto3-refresh-session aims to be simple and intuitive. 51 | Parameters like ``defer_refresh`` and ``assume_role_kwargs`` are not part of `boto3`’s interface, and are only useful at initialization. 52 | 53 | Rather than surface them as persistent attributes (which adds noise), the decision was made to treat them as ephemeral setup-time inputs. 54 | 55 | Can I submit a feature request? 56 | =============================== 57 | 58 | It depends. 59 | 60 | If your request adds a general-purpose feature (e.g. CLI tooling), it’s likely to be considered. 61 | 62 | But if your proposal is *highly specific* or *non-generalizable*, you’re encouraged to fork the project and tailor it to your needs. 63 | boto3-refresh-session is MIT-licensed, and local modifications are fully permitted. 64 | 65 | Remember: BRS has thousands of users. 66 | Changes that break compatibility or narrow scope have wide consequences. 67 | 68 | Before submitting a request, ask yourself: 69 | 70 | *“Does this benefit everyone — or just me?”* 71 | -------------------------------------------------------------------------------- /doc/raison.rst: -------------------------------------------------------------------------------- 1 | .. _raison: 2 | 3 | Raison d'Être 4 | ------------- 5 | 6 | Long-running data pipelines, security tooling, ETL jobs, and cloud automation scripts frequently interact with the AWS API via ``boto3`` — and often run into the same problem: 7 | 8 | **Temporary credentials expire.** 9 | 10 | When that happens, engineers typically fall back on one of two strategies: 11 | 12 | - Wrapping AWS calls in ``try/except`` blocks that catch ``ClientError`` exceptions 13 | - Writing ad hoc logic to refresh credentials using ``botocore.credentials`` internals 14 | 15 | Both approaches are fragile, tedious to maintain, and error-prone at scale. 16 | 17 | Over the years, I noticed that every company I worked for — whether a scrappy startup or FAANG — ended up with some variation of the same pattern: 18 | a small in-house module to manage credential refresh, written in haste, duplicated across services, and riddled with edge cases. Things only 19 | got more strange and difficult when I needed to run things in parallel. 20 | 21 | Eventually, I decided to build ``boto3-refresh-session`` as a proper open-source Python package: 22 | 23 | - Fully tested 24 | - Extensible 25 | - Integrated with ``boto3`` idioms 26 | - Equipped with automatic documentation and CI tooling 27 | 28 | **The goal:** to solve a real, recurring problem once — cleanly, consistently, and for everyone -- with multiple refresh strategies. 29 | 30 | If you've ever written the same AWS credential-refresh boilerplate more than once, this library is for you. 31 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage 4 | ***** 5 | 6 | To use `boto3-refresh-session`, you must have AWS credentials configured locally. 7 | Refer to the :ref:`authorization documentation ` for details on supported authentication methods. 8 | 9 | Basic Initialization 10 | -------------------- 11 | 12 | Everything in `boto3` is ultimately built on the :class:`boto3.session.Session` object — 13 | including ``Client`` and ``Resource`` objects. 14 | `boto3-refresh-session` extends this interface while adding automatic credential refresh. 15 | 16 | Creating a session is straightforward: 17 | 18 | .. code-block:: python 19 | 20 | from boto3_refresh_session import RefreshableSession 21 | 22 | assume_role_kwargs = { 23 | "RoleArn": "", 24 | "RoleSessionName": "", 25 | } 26 | 27 | session = RefreshableSession( 28 | assume_role_kwargs=assume_role_kwargs 29 | ) 30 | 31 | s3 = session.client('s3') 32 | 33 | You can also create a ``Resource`` the same way: 34 | 35 | .. code-block:: python 36 | 37 | s3 = session.resource('s3') 38 | 39 | Optional: set this session globally as the default for `boto3`: 40 | 41 | .. code-block:: python 42 | 43 | import boto3 44 | boto3.DEFAULT_SESSION = session 45 | s3 = boto3.client('s3') # will use the custom session automatically 46 | 47 | Parameters 48 | ---------- 49 | 50 | At a minimum, you must provide parameters for the STS ``assume_role`` call via ``assume_role_kwargs``: 51 | 52 | .. code-block:: python 53 | 54 | assume_role_kwargs = { 55 | "RoleArn": "", 56 | "RoleSessionName": "", 57 | "DurationSeconds": 3600, # optional 58 | } 59 | 60 | Optional keyword arguments for the underlying ``boto3.client("sts")`` can be passed via ``sts_client_kwargs``: 61 | 62 | .. code-block:: python 63 | 64 | sts_client_kwargs = { 65 | "config": Config(retries={"max_attempts": 5}) 66 | } 67 | 68 | And any arguments accepted by :class:`boto3.session.Session` (e.g., ``region_name``, etc.) can be passed directly: 69 | 70 | .. code-block:: python 71 | 72 | session = RefreshableSession( 73 | assume_role_kwargs=assume_role_kwargs, 74 | sts_client_kwargs=sts_client_kwargs, 75 | region_name="us-east-1" 76 | ) 77 | 78 | Refresh Behavior 79 | ---------------- 80 | 81 | There are two ways to trigger automatic credential refresh: 82 | 83 | 1. **Deferred (default)** — Refresh occurs only when credentials are required 84 | 2. **Eager** — Credentials are refreshed as soon as they expire 85 | 86 | Set ``defer_refresh`` to False to enable eager refresh: 87 | 88 | .. code-block:: python 89 | 90 | session = RefreshableSession( 91 | defer_refresh=False, 92 | assume_role_kwargs=assume_role_kwargs 93 | ) 94 | 95 | .. warning:: 96 | It is **highly recommended** to use the default: ``defer_refresh=True``. 97 | Eager refresh adds overhead and is only suitable for low-latency systems that cannot tolerate refresh delays. 98 | 99 | Parallel Usage and Performance 100 | ------------------------------ 101 | 102 | If you're working with large datasets and sensitive value detection or redaction, 103 | you may wish to use ``boto3-refresh-session`` in parallel. 104 | 105 | The core session class is thread-safe and compatible with Python’s ``concurrent.futures`` or ``multiprocessing``. 106 | 107 | To maximize throughput: 108 | 109 | - Reuse a single ``RefreshableSession`` object across threads or subprocesses 110 | - Use ``defer_refresh=True`` to avoid concurrent refreshes at process boundaries 111 | - Mount the session into a poolable or global shared object 112 | 113 | **Example (using concurrent.futures):** 114 | 115 | .. code-block:: python 116 | 117 | from concurrent.futures import ThreadPoolExecutor 118 | from boto3_refresh_session import RefreshableSession 119 | 120 | session = RefreshableSession(assume_role_kwargs={...}) 121 | 122 | def upload_one(bucket, key, body): 123 | s3 = session.client("s3") 124 | s3.put_object(Bucket=bucket, Key=key, Body=body) 125 | 126 | with ThreadPoolExecutor() as executor: 127 | futures = [ 128 | executor.submit(upload_one, "my-bucket", f"file-{i}", b"data") 129 | for i in range(10) 130 | ] 131 | 132 | for future in futures: 133 | future.result() 134 | 135 | .. note:: 136 | 137 | For process-based concurrency (e.g., ``ProcessPoolExecutor``), initialize the session 138 | **before** spawning or forking the pool. This ensures memory is shared efficiently via copy-on-write, 139 | and avoids unnecessary duplication of temporary credentials. 140 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "boto3-refresh-session" 3 | version = "1.1.3" 4 | description = "A simple Python package for refreshing the temporary security credentials in a boto3.session.Session object automatically." 5 | authors = [ 6 | {name = "Mike Letts",email = "lettsmt@gmail.com"} 7 | ] 8 | license = {text = "MIT"} 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = ["boto3", "botocore"] 12 | keywords = ["boto3", "botocore", "aws"] 13 | maintainers = [ 14 | {name="Michael Letts", email="lettsmt@gmail.com"}, 15 | ] 16 | 17 | [tool.poetry] 18 | include = ["NOTICE"] 19 | 20 | [project.urls] 21 | repository = "https://github.com/michaelthomasletts/boto3-refresh-session" 22 | documentation = "https://michaelthomasletts.github.io/boto3-refresh-session/index.html" 23 | 24 | [build-system] 25 | requires = ["poetry-core>=2.0.0,<3.0.0"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.poetry.group.dev.dependencies] 29 | pytest = "^8.3.4" 30 | black = "^24.10.0" 31 | isort = "^5.13.2" 32 | flake8 = "^7.1.1" 33 | pre-commit = "^4.0.1" 34 | sphinx = "^8.1.3" 35 | pydata-sphinx-theme = "^0.16.1" 36 | numpydoc = "^1.8.0" 37 | tomlkit = "^0.13.2" 38 | jinja2 = "^3.1.6" 39 | 40 | [tool.black] 41 | line-length = 79 42 | target-version = ["py310"] 43 | verbose = true 44 | 45 | [tool.isort] 46 | line_length = 79 47 | ensure_newline_before_comments = true 48 | use_parentheses = true 49 | include_trailing_comma = true 50 | multi_line_output = 3 51 | 52 | [tool.pytest.ini_options] 53 | log_cli = true 54 | log_cli_level = "INFO" 55 | log_cli_date_format = "%Y-%m-%d %H:%M:%S" 56 | log_cli_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" -------------------------------------------------------------------------------- /readme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests 4 | from jinja2 import Environment, FileSystemLoader 5 | 6 | 7 | def abbreviate(n): 8 | if n >= 1_000_000_000: 9 | return f"{n / 1_000_000_000:.1f}B" 10 | elif n >= 1_000_000: 11 | return f"{n / 1_000_000:.1f}M" 12 | elif n >= 1_000: 13 | return f"{n / 1_000:.1f}K" 14 | else: 15 | return str(n) 16 | 17 | 18 | def run(): 19 | package = "boto3-refresh-session" 20 | url = f"https://pepy.tech/api/v2/projects/{package}" 21 | response = requests.get(url) 22 | response.raise_for_status() 23 | data = response.json() 24 | downloads = data.get("total_downloads", 0) 25 | downloads_abbr = abbreviate(downloads) 26 | 27 | env = Environment(loader=FileSystemLoader(".")) 28 | template = env.get_template("README.template.md") 29 | 30 | with open("README.md", "w") as f: 31 | f.write(template.render(downloads_abbr=downloads_abbr)) 32 | 33 | 34 | if __name__ == "__main__": 35 | run() 36 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michaelthomasletts/boto3-refresh-session/f05847a63d41bc78e08a0e49d0901545729c965a/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from os import getenv 3 | 4 | from boto3_refresh_session import RefreshableSession 5 | 6 | # configuring logging 7 | logging.basicConfig( 8 | level=logging.INFO, 9 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 10 | ) 11 | 12 | # creating logger 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def test_defer_refresh(): 17 | # initializing parameters 18 | region_name = "us-east-1" 19 | assume_role_kwargs = { 20 | "RoleArn": getenv("ROLE_ARN"), 21 | "RoleSessionName": "unit-testing", 22 | "DurationSeconds": 900, 23 | } 24 | sts_client_kwargs = {"region_name": region_name} 25 | 26 | # testing defer_refresh = True 27 | logger.info("Testing RefreshableSession with defer_refresh = True") 28 | session = RefreshableSession( 29 | assume_role_kwargs=assume_role_kwargs, 30 | sts_client_kwargs=sts_client_kwargs, 31 | region_name=region_name, 32 | ) 33 | s3 = session.client(service_name="s3") 34 | s3.list_buckets() 35 | 36 | # testing defer_refresh = False 37 | logger.info("Testing RefreshableSession with defer_refresh = False") 38 | session = RefreshableSession( 39 | defer_refresh=False, 40 | assume_role_kwargs=assume_role_kwargs, 41 | sts_client_kwargs=sts_client_kwargs, 42 | region_name=region_name, 43 | ) 44 | s3 = session.client(service_name="s3") 45 | s3.list_buckets() 46 | --------------------------------------------------------------------------------