From 6206002b3d9af847ddf29b05da3fb2252b3ae5ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 07:27:13 +0000 Subject: [PATCH 01/24] Bump actions/checkout from 4.1.6 to 4.1.7 in the github-actions group Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 4.1.6 to 4.1.7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/a5ac7e51b41094c92402da3b24376905380afc29...692973e3d937129bcbf40652eb9f2f61becf3332) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 887587b..8797b2c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repo" - uses: "actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29" # v4.1.6 + uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 - name: "Setup Python" id: "setup-python" @@ -58,7 +58,7 @@ jobs: wheel-filename: "${{ steps.build-wheel.outputs.wheel-filename }}" steps: - name: "Checkout the repo" - uses: "actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29" # v4.1.6 + uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 - name: "Identify the week number" run: | @@ -138,7 +138,7 @@ jobs: steps: - name: "Checkout the repo" - uses: "actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29" # v4.1.6 + uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 - name: "Identify the week number" shell: "bash" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63b8098..8b28d7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 1 From 40a8f632a506b3b3124fe4fb6e77a5c93ab0349f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:38:29 +0000 Subject: [PATCH 02/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pycqa/flake8: 7.0.0 → 7.1.0](https://github.com/pycqa/flake8/compare/7.0.0...7.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ec0ed5..f18b688 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: - id: isort - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 additional_dependencies: From 733741538094bdb7759c6bd5f20dc685a26ed074 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:30:41 +0000 Subject: [PATCH 03/24] Bump certifi from 2024.6.2 to 2024.7.4 in /requirements/test Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements/test/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test/requirements.txt b/requirements/test/requirements.txt index d45deef..bbd425b 100644 --- a/requirements/test/requirements.txt +++ b/requirements/test/requirements.txt @@ -1,4 +1,4 @@ -certifi==2024.6.2 ; python_version >= "3.8" +certifi==2024.7.4 ; python_version >= "3.8" charset-normalizer==3.3.2 ; python_version >= "3.8" colorama==0.4.6 ; python_version >= "3.8" and sys_platform == "win32" coverage==7.5.3 ; python_version >= "3.8" From 477b7c1eb6933647023b61c11acbe4cdc3f0acf5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:26:18 +0000 Subject: [PATCH 04/24] Bump certifi from 2024.6.2 to 2024.7.4 in /requirements/docs Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.6.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.06.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements/docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/docs/requirements.txt b/requirements/docs/requirements.txt index f641b17..cf89f39 100644 --- a/requirements/docs/requirements.txt +++ b/requirements/docs/requirements.txt @@ -1,7 +1,7 @@ alabaster==0.7.13 ; python_version >= "3.8" babel==2.15.0 ; python_version >= "3.8" beautifulsoup4==4.12.3 ; python_version >= "3.8" -certifi==2024.6.2 ; python_version >= "3.8" +certifi==2024.7.4 ; python_version >= "3.8" charset-normalizer==3.3.2 ; python_version >= "3.8" colorama==0.4.6 ; python_version >= "3.8" and sys_platform == "win32" css-html-js-minify==2.5.5 ; python_version >= "3.8" From fdc89a51981e841d0691ce1d77cfd551dd325a50 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 8 Jul 2024 13:27:45 -0500 Subject: [PATCH 05/24] Run `tox run -m update` --- requirements/docs/poetry.lock | 22 +++--- requirements/docs/requirements.txt | 4 +- requirements/mypy/poetry.lock | 68 ++++++++--------- requirements/mypy/requirements.txt | 6 +- requirements/test/poetry.lock | 118 ++++++++++++++--------------- requirements/test/requirements.txt | 4 +- 6 files changed, 111 insertions(+), 111 deletions(-) diff --git a/requirements/docs/poetry.lock b/requirements/docs/poetry.lock index 392c40d..7c17754 100644 --- a/requirements/docs/poetry.lock +++ b/requirements/docs/poetry.lock @@ -51,13 +51,13 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -216,22 +216,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.1.0" +version = "8.0.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "jinja2" @@ -743,13 +743,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] diff --git a/requirements/docs/requirements.txt b/requirements/docs/requirements.txt index cf89f39..a600263 100644 --- a/requirements/docs/requirements.txt +++ b/requirements/docs/requirements.txt @@ -8,7 +8,7 @@ css-html-js-minify==2.5.5 ; python_version >= "3.8" docutils==0.20.1 ; python_version >= "3.8" idna==3.7 ; python_version >= "3.8" imagesize==1.4.1 ; python_version >= "3.8" -importlib-metadata==7.1.0 ; python_version < "3.10" and python_version >= "3.8" +importlib-metadata==8.0.0 ; python_version < "3.10" and python_version >= "3.8" jinja2==3.1.4 ; python_version >= "3.8" lxml==5.2.2 ; python_version >= "3.8" markupsafe==2.1.5 ; python_version >= "3.8" @@ -29,5 +29,5 @@ sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.8" sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.8" text-unidecode==1.3 ; python_version >= "3.8" unidecode==1.3.8 ; python_version >= "3.8" -urllib3==2.2.1 ; python_version >= "3.8" +urllib3==2.2.2 ; python_version >= "3.8" zipp==3.19.2 ; python_version < "3.10" and python_version >= "3.8" diff --git a/requirements/mypy/poetry.lock b/requirements/mypy/poetry.lock index 9d09290..503224f 100644 --- a/requirements/mypy/poetry.lock +++ b/requirements/mypy/poetry.lock @@ -2,38 +2,38 @@ [[package]] name = "mypy" -version = "1.10.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -93,13 +93,13 @@ files = [ [[package]] name = "types-requests" -version = "2.32.0.20240602" +version = "2.32.0.20240622" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"}, - {file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"}, + {file = "types-requests-2.32.0.20240622.tar.gz", hash = "sha256:ed5e8a412fcc39159d6319385c009d642845f250c63902718f605cd90faade31"}, + {file = "types_requests-2.32.0.20240622-py3-none-any.whl", hash = "sha256:97bac6b54b5bd4cf91d407e62f0932a74821bc2211f22116d9ee1dd643826caf"}, ] [package.dependencies] @@ -118,13 +118,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] diff --git a/requirements/mypy/requirements.txt b/requirements/mypy/requirements.txt index 4a021e2..b13d58c 100644 --- a/requirements/mypy/requirements.txt +++ b/requirements/mypy/requirements.txt @@ -1,8 +1,8 @@ mypy-extensions==1.0.0 ; python_version >= "3.8" -mypy==1.10.0 ; python_version >= "3.8" +mypy==1.10.1 ; python_version >= "3.8" tomli==2.0.1 ; python_version < "3.11" and python_version >= "3.8" types-cachetools==5.3.0.7 ; python_version >= "3.8" types-pyyaml==6.0.12.20240311 ; python_version >= "3.8" -types-requests==2.32.0.20240602 ; python_version >= "3.8" +types-requests==2.32.0.20240622 ; python_version >= "3.8" typing-extensions==4.12.2 ; python_version >= "3.8" -urllib3==2.2.1 ; python_version >= "3.8" +urllib3==2.2.2 ; python_version >= "3.8" diff --git a/requirements/test/poetry.lock b/requirements/test/poetry.lock index f4d186c..90af819 100644 --- a/requirements/test/poetry.lock +++ b/requirements/test/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -123,63 +123,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.3" +version = "7.5.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45"}, - {file = "coverage-7.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc"}, - {file = "coverage-7.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d"}, - {file = "coverage-7.5.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c"}, - {file = "coverage-7.5.3-cp310-cp310-win32.whl", hash = "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84"}, - {file = "coverage-7.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974"}, - {file = "coverage-7.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807"}, - {file = "coverage-7.5.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8"}, - {file = "coverage-7.5.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614"}, - {file = "coverage-7.5.3-cp311-cp311-win32.whl", hash = "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9"}, - {file = "coverage-7.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8"}, - {file = "coverage-7.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db"}, - {file = "coverage-7.5.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35"}, - {file = "coverage-7.5.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84"}, - {file = "coverage-7.5.3-cp312-cp312-win32.whl", hash = "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08"}, - {file = "coverage-7.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb"}, - {file = "coverage-7.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98"}, - {file = "coverage-7.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce"}, - {file = "coverage-7.5.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0"}, - {file = "coverage-7.5.3-cp38-cp38-win32.whl", hash = "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485"}, - {file = "coverage-7.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85"}, - {file = "coverage-7.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341"}, - {file = "coverage-7.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303"}, - {file = "coverage-7.5.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd"}, - {file = "coverage-7.5.3-cp39-cp39-win32.whl", hash = "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d"}, - {file = "coverage-7.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0"}, - {file = "coverage-7.5.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884"}, - {file = "coverage-7.5.3.tar.gz", hash = "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] [package.extras] @@ -410,13 +410,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] diff --git a/requirements/test/requirements.txt b/requirements/test/requirements.txt index bbd425b..688a7ef 100644 --- a/requirements/test/requirements.txt +++ b/requirements/test/requirements.txt @@ -1,7 +1,7 @@ certifi==2024.7.4 ; python_version >= "3.8" charset-normalizer==3.3.2 ; python_version >= "3.8" colorama==0.4.6 ; python_version >= "3.8" and sys_platform == "win32" -coverage==7.5.3 ; python_version >= "3.8" +coverage==7.5.4 ; python_version >= "3.8" exceptiongroup==1.2.1 ; python_version < "3.11" and python_version >= "3.8" freezegun==1.5.1 ; python_version >= "3.8" idna==3.7 ; python_version >= "3.8" @@ -15,4 +15,4 @@ requests==2.32.3 ; python_version >= "3.8" responses==0.25.3 ; python_version >= "3.8" six==1.16.0 ; python_version >= "3.8" tomli==2.0.1 ; python_version < "3.11" and python_version >= "3.8" -urllib3==2.2.1 ; python_version >= "3.8" +urllib3==2.2.2 ; python_version >= "3.8" From a9f6f0235c24affc74556f6e0a0b49691525a55f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2024 07:48:03 +0000 Subject: [PATCH 06/24] Bump the github-actions group with 3 updates Bumps the github-actions group with 3 updates: [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/setup-python` from 5.1.0 to 5.1.1 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/82c7e631bb3cdc910f68e0081d67478d79c6982d...39cd14951b08e74b54015e9e001cdefcf80e669f) Updates `actions/upload-artifact` from 4.3.3 to 4.3.4 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/65462800fd760344b1a7b4382951275a0abb4808...0b2256b8c012f0828dc542b3febcab082c67f72b) Updates `actions/download-artifact` from 4.1.7 to 4.1.8 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/65a9edc5881444af0b9093a5e628f2fe47ea3b2e...fa0a91b85d4f404e444e00e005971372dc801d16) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/release.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8797b2c..86be806 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d" # v5.1.0 + uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 with: python-version: "3.12" @@ -67,7 +67,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d" # v5.1.0 + uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 with: python-version: "3.11" cache: "pip" @@ -84,7 +84,7 @@ jobs: echo "wheel-filename=$(find globus_action_provider_tools-*.whl | head -n 1)" >> "$GITHUB_OUTPUT" - name: "Upload the artifact" - uses: "actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808" # v4.3.3 + uses: "actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b" # v4.3.4 with: name: "globus_action_provider_tools-${{ github.sha }}.whl" path: "${{ steps.build-wheel.outputs.wheel-filename }}" @@ -148,7 +148,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d" # v5.1.0 + uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 with: python-version: "${{ matrix.python-version }}" cache: "pip" @@ -185,7 +185,7 @@ jobs: ${{ env.venv-path }}/pip install tox - name: "Download the artifact" - uses: "actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e" # v4.1.7 + uses: "actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16" # v4.1.8 with: name: "globus_action_provider_tools-${{ github.sha }}.whl" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b28d7a..6c86393 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 1 - name: Set target python version - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: python-version: "3.11" From cbed05ed30645e71c86f492f42ce113c80eb793d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 07:52:19 +0000 Subject: [PATCH 07/24] Bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [actions/setup-python](https://github.com/actions/setup-python) and [actions/upload-artifact](https://github.com/actions/upload-artifact). Updates `actions/setup-python` from 5.1.1 to 5.2.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/39cd14951b08e74b54015e9e001cdefcf80e669f...f677139bbe7f9c59b41e40162b753c062f5d49a3) Updates `actions/upload-artifact` from 4.3.4 to 4.4.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b2256b8c012f0828dc542b3febcab082c67f72b...50769540e7f4bd5e21e526ee35c689e35e0d6874) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86be806..3b02829 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 + uses: "actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3" # v5.2.0 with: python-version: "3.12" @@ -67,7 +67,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 + uses: "actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3" # v5.2.0 with: python-version: "3.11" cache: "pip" @@ -84,7 +84,7 @@ jobs: echo "wheel-filename=$(find globus_action_provider_tools-*.whl | head -n 1)" >> "$GITHUB_OUTPUT" - name: "Upload the artifact" - uses: "actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b" # v4.3.4 + uses: "actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874" # v4.4.0 with: name: "globus_action_provider_tools-${{ github.sha }}.whl" path: "${{ steps.build-wheel.outputs.wheel-filename }}" @@ -148,7 +148,7 @@ jobs: - name: "Setup Python" id: "setup-python" - uses: "actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f" # v5.1.1 + uses: "actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3" # v5.2.0 with: python-version: "${{ matrix.python-version }}" cache: "pip" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c86393..f477d4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 1 - name: Set target python version - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: "3.11" From e869d0cf700ef739e3fbc1d511cbb6f85bce12b9 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Fri, 6 Sep 2024 12:01:33 -0500 Subject: [PATCH 08/24] Remove the now-unnecessary tox v3 `isolated_build` option --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index e20ee57..136d8f0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -isolated_build = true # Environments here are run in the order they appear. # They can be individually run using "tox -e ". envlist = From ca359fee79fe524a3f34158fb27d030e772edbc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:03:58 +0000 Subject: [PATCH 09/24] Bump actions/checkout from 4.1.7 to 4.2.0 in the github-actions group Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 4.1.7 to 4.2.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/692973e3d937129bcbf40652eb9f2f61becf3332...d632683dd7b4114ad314bca15554477dd762a938) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3b02829..5490201 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repo" - uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 + uses: "actions/checkout@d632683dd7b4114ad314bca15554477dd762a938" # v4.2.0 - name: "Setup Python" id: "setup-python" @@ -58,7 +58,7 @@ jobs: wheel-filename: "${{ steps.build-wheel.outputs.wheel-filename }}" steps: - name: "Checkout the repo" - uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 + uses: "actions/checkout@d632683dd7b4114ad314bca15554477dd762a938" # v4.2.0 - name: "Identify the week number" run: | @@ -138,7 +138,7 @@ jobs: steps: - name: "Checkout the repo" - uses: "actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332" # v4.1.7 + uses: "actions/checkout@d632683dd7b4114ad314bca15554477dd762a938" # v4.2.0 - name: "Identify the week number" shell: "bash" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f477d4a..684e33b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 1 From a29cc021b9d000d46a3fd7610e3c626031d87db9 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Tue, 1 Oct 2024 13:57:55 -0500 Subject: [PATCH 10/24] Add Max to CODEOWNERS; update pre-commit hooks --- .github/CODEOWNERS | 2 +- .pre-commit-config.yaml | 16 ++++++++-------- tests/conftest.py | 12 ++++++------ tests/test_data_types.py | 20 ++++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 28a0ac9..b1a9796 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # The automate team will be requested for review on all opened PRs. -* @ada-globus @derek-globus @jakeglobus @kurtmckee @sirosen +* @ada-globus @derek-globus @jakeglobus @kurtmckee @MaxTueckeGlobus @sirosen diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f18b688..6ae197d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,20 +19,20 @@ repos: - id: check-json - id: check-added-large-files - - repo: https://github.com/sirosen/alphabetize-codeowners - rev: 0.0.1 + - repo: https://github.com/sirosen/texthooks + rev: 0.6.7 hooks: - id: alphabetize-codeowners # Enforce Python 3.8+ idioms. - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black @@ -42,19 +42,19 @@ repos: - id: isort - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: - - flake8-bugbear==24.4.26 + - flake8-bugbear==24.8.19 - repo: https://github.com/sirosen/slyp - rev: 0.6.1 + rev: 0.7.1 hooks: - id: slyp - repo: https://github.com/rhysd/actionlint - rev: v1.7.1 + rev: v1.7.3 hooks: - id: actionlint diff --git a/tests/conftest.py b/tests/conftest.py index 2910b17..03042ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,12 +21,12 @@ @pytest.fixture def config(): - return dict( - client_id=canned_responses.mock_client_id(), - client_secret=canned_responses.mock_client_secret(), - expected_scopes=(canned_responses.mock_scope(),), - expected_audience=canned_responses.mock_expected_audience(), - ) + return { + "client_id": canned_responses.mock_client_id(), + "client_secret": canned_responses.mock_client_secret(), + "expected_scopes": (canned_responses.mock_scope(),), + "expected_audience": canned_responses.mock_expected_audience(), + } @pytest.fixture diff --git a/tests/test_data_types.py b/tests/test_data_types.py index cd27913..436800d 100644 --- a/tests/test_data_types.py +++ b/tests/test_data_types.py @@ -11,16 +11,16 @@ ActionStatusValue, ) -ACTION_STATUS_ARGS = dict( - status=ActionStatusValue.SUCCEEDED, - creator_id=f"urn:globus:auth:identity:{uuid.uuid4()}", - monitor_by=set(), - manage_by=set(), - completion_time=str(datetime.datetime.now().isoformat()), - release_after="P30D", - display_status=ActionStatusValue.SUCCEEDED, - details={}, -) +ACTION_STATUS_ARGS = { + "status": ActionStatusValue.SUCCEEDED, + "creator_id": f"urn:globus:auth:identity:{uuid.uuid4()}", + "monitor_by": set(), + "manage_by": set(), + "completion_time": str(datetime.datetime.now().isoformat()), + "release_after": "P30D", + "display_status": ActionStatusValue.SUCCEEDED, + "details": {}, +} @pytest.mark.parametrize( From e4c536c3dd697d58a224bbbe6a99cb1d70b9c62d Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 3 Oct 2024 11:45:20 -0500 Subject: [PATCH 11/24] Move to a src-layout --- changelog.d/20241003_114654_sirosen_infra_cleanup.rst | 4 ++++ .../globus_action_provider_tools}/__init__.py | 0 .../globus_action_provider_tools}/action_request.yaml | 0 .../globus_action_provider_tools}/action_status.yaml | 0 .../globus_action_provider_tools}/authentication.py | 0 .../globus_action_provider_tools}/authorization.py | 0 .../globus_action_provider_tools}/data_types.py | 0 .../globus_action_provider_tools}/errors.py | 0 .../globus_action_provider_tools}/flask/__init__.py | 0 .../globus_action_provider_tools}/flask/api_helpers.py | 0 .../globus_action_provider_tools}/flask/apt_blueprint.py | 0 .../globus_action_provider_tools}/flask/config.py | 0 .../globus_action_provider_tools}/flask/exceptions.py | 0 .../globus_action_provider_tools}/flask/helpers.py | 0 .../flask/request_lifecycle_hooks/__init__.py | 0 .../flask/request_lifecycle_hooks/cloudwatch_metrics.py | 0 .../globus_action_provider_tools}/flask/types.py | 0 .../globus_action_provider_tools}/storage.py | 0 .../globus_action_provider_tools}/testing/__init__.py | 0 .../globus_action_provider_tools}/testing/fixtures.py | 0 .../globus_action_provider_tools}/testing/mocks.py | 0 .../globus_action_provider_tools}/testing/patches.py | 0 .../globus_action_provider_tools}/utils.py | 0 .../globus_action_provider_tools}/validation.py | 0 tox.ini | 2 +- 25 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/20241003_114654_sirosen_infra_cleanup.rst rename {globus_action_provider_tools => src/globus_action_provider_tools}/__init__.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/action_request.yaml (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/action_status.yaml (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/authentication.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/authorization.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/data_types.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/errors.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/__init__.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/api_helpers.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/apt_blueprint.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/config.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/exceptions.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/helpers.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/request_lifecycle_hooks/__init__.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/request_lifecycle_hooks/cloudwatch_metrics.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/flask/types.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/storage.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/testing/__init__.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/testing/fixtures.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/testing/mocks.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/testing/patches.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/utils.py (100%) rename {globus_action_provider_tools => src/globus_action_provider_tools}/validation.py (100%) diff --git a/changelog.d/20241003_114654_sirosen_infra_cleanup.rst b/changelog.d/20241003_114654_sirosen_infra_cleanup.rst new file mode 100644 index 0000000..826027a --- /dev/null +++ b/changelog.d/20241003_114654_sirosen_infra_cleanup.rst @@ -0,0 +1,4 @@ +Development +----------- + +- Move to `src/` tree layout diff --git a/globus_action_provider_tools/__init__.py b/src/globus_action_provider_tools/__init__.py similarity index 100% rename from globus_action_provider_tools/__init__.py rename to src/globus_action_provider_tools/__init__.py diff --git a/globus_action_provider_tools/action_request.yaml b/src/globus_action_provider_tools/action_request.yaml similarity index 100% rename from globus_action_provider_tools/action_request.yaml rename to src/globus_action_provider_tools/action_request.yaml diff --git a/globus_action_provider_tools/action_status.yaml b/src/globus_action_provider_tools/action_status.yaml similarity index 100% rename from globus_action_provider_tools/action_status.yaml rename to src/globus_action_provider_tools/action_status.yaml diff --git a/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py similarity index 100% rename from globus_action_provider_tools/authentication.py rename to src/globus_action_provider_tools/authentication.py diff --git a/globus_action_provider_tools/authorization.py b/src/globus_action_provider_tools/authorization.py similarity index 100% rename from globus_action_provider_tools/authorization.py rename to src/globus_action_provider_tools/authorization.py diff --git a/globus_action_provider_tools/data_types.py b/src/globus_action_provider_tools/data_types.py similarity index 100% rename from globus_action_provider_tools/data_types.py rename to src/globus_action_provider_tools/data_types.py diff --git a/globus_action_provider_tools/errors.py b/src/globus_action_provider_tools/errors.py similarity index 100% rename from globus_action_provider_tools/errors.py rename to src/globus_action_provider_tools/errors.py diff --git a/globus_action_provider_tools/flask/__init__.py b/src/globus_action_provider_tools/flask/__init__.py similarity index 100% rename from globus_action_provider_tools/flask/__init__.py rename to src/globus_action_provider_tools/flask/__init__.py diff --git a/globus_action_provider_tools/flask/api_helpers.py b/src/globus_action_provider_tools/flask/api_helpers.py similarity index 100% rename from globus_action_provider_tools/flask/api_helpers.py rename to src/globus_action_provider_tools/flask/api_helpers.py diff --git a/globus_action_provider_tools/flask/apt_blueprint.py b/src/globus_action_provider_tools/flask/apt_blueprint.py similarity index 100% rename from globus_action_provider_tools/flask/apt_blueprint.py rename to src/globus_action_provider_tools/flask/apt_blueprint.py diff --git a/globus_action_provider_tools/flask/config.py b/src/globus_action_provider_tools/flask/config.py similarity index 100% rename from globus_action_provider_tools/flask/config.py rename to src/globus_action_provider_tools/flask/config.py diff --git a/globus_action_provider_tools/flask/exceptions.py b/src/globus_action_provider_tools/flask/exceptions.py similarity index 100% rename from globus_action_provider_tools/flask/exceptions.py rename to src/globus_action_provider_tools/flask/exceptions.py diff --git a/globus_action_provider_tools/flask/helpers.py b/src/globus_action_provider_tools/flask/helpers.py similarity index 100% rename from globus_action_provider_tools/flask/helpers.py rename to src/globus_action_provider_tools/flask/helpers.py diff --git a/globus_action_provider_tools/flask/request_lifecycle_hooks/__init__.py b/src/globus_action_provider_tools/flask/request_lifecycle_hooks/__init__.py similarity index 100% rename from globus_action_provider_tools/flask/request_lifecycle_hooks/__init__.py rename to src/globus_action_provider_tools/flask/request_lifecycle_hooks/__init__.py diff --git a/globus_action_provider_tools/flask/request_lifecycle_hooks/cloudwatch_metrics.py b/src/globus_action_provider_tools/flask/request_lifecycle_hooks/cloudwatch_metrics.py similarity index 100% rename from globus_action_provider_tools/flask/request_lifecycle_hooks/cloudwatch_metrics.py rename to src/globus_action_provider_tools/flask/request_lifecycle_hooks/cloudwatch_metrics.py diff --git a/globus_action_provider_tools/flask/types.py b/src/globus_action_provider_tools/flask/types.py similarity index 100% rename from globus_action_provider_tools/flask/types.py rename to src/globus_action_provider_tools/flask/types.py diff --git a/globus_action_provider_tools/storage.py b/src/globus_action_provider_tools/storage.py similarity index 100% rename from globus_action_provider_tools/storage.py rename to src/globus_action_provider_tools/storage.py diff --git a/globus_action_provider_tools/testing/__init__.py b/src/globus_action_provider_tools/testing/__init__.py similarity index 100% rename from globus_action_provider_tools/testing/__init__.py rename to src/globus_action_provider_tools/testing/__init__.py diff --git a/globus_action_provider_tools/testing/fixtures.py b/src/globus_action_provider_tools/testing/fixtures.py similarity index 100% rename from globus_action_provider_tools/testing/fixtures.py rename to src/globus_action_provider_tools/testing/fixtures.py diff --git a/globus_action_provider_tools/testing/mocks.py b/src/globus_action_provider_tools/testing/mocks.py similarity index 100% rename from globus_action_provider_tools/testing/mocks.py rename to src/globus_action_provider_tools/testing/mocks.py diff --git a/globus_action_provider_tools/testing/patches.py b/src/globus_action_provider_tools/testing/patches.py similarity index 100% rename from globus_action_provider_tools/testing/patches.py rename to src/globus_action_provider_tools/testing/patches.py diff --git a/globus_action_provider_tools/utils.py b/src/globus_action_provider_tools/utils.py similarity index 100% rename from globus_action_provider_tools/utils.py rename to src/globus_action_provider_tools/utils.py diff --git a/globus_action_provider_tools/validation.py b/src/globus_action_provider_tools/validation.py similarity index 100% rename from globus_action_provider_tools/validation.py rename to src/globus_action_provider_tools/validation.py diff --git a/tox.ini b/tox.ini index 136d8f0..a6530aa 100644 --- a/tox.ini +++ b/tox.ini @@ -47,7 +47,7 @@ skip_install = true deps = -r requirements/mypy/requirements.txt commands = - mypy --ignore-missing-imports globus_action_provider_tools/ tests/ + mypy --ignore-missing-imports src/ tests/ [testenv:docs] From 69f513636a571ebe26c66457dd42e8d57f04c701 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 3 Oct 2024 11:47:37 -0500 Subject: [PATCH 12/24] Remove poetry.toml --- poetry.toml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 poetry.toml diff --git a/poetry.toml b/poetry.toml deleted file mode 100644 index 53b35d3..0000000 --- a/poetry.toml +++ /dev/null @@ -1,3 +0,0 @@ -[virtualenvs] -create = true -in-project = true From a2dd1e330caabb57138ce78c1154b15c8d9d499f Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 7 Oct 2024 15:57:46 -0500 Subject: [PATCH 13/24] Remove an unused and deprecated module --- .../testing/patches.py | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/globus_action_provider_tools/testing/patches.py diff --git a/src/globus_action_provider_tools/testing/patches.py b/src/globus_action_provider_tools/testing/patches.py deleted file mode 100644 index 32973a9..0000000 --- a/src/globus_action_provider_tools/testing/patches.py +++ /dev/null @@ -1,25 +0,0 @@ -import warnings -from unittest.mock import patch - -from globus_action_provider_tools.testing.mocks import mock_authstate - -warnings.warn( - ( - "The globus_action_provider_tools.testing.patches module is deprecated and will " - "be removed in 0.12.0. Please consider using the " - "globus_action_provider_tools.testing.fixtures module instead." - ), - DeprecationWarning, - stacklevel=2, -) - - -flask_api_helpers_tokenchecker_patch = patch( - "globus_action_provider_tools.flask.api_helpers.TokenChecker.check_token", - return_value=mock_authstate(), -) - -flask_blueprint_tokenchecker_patch = patch( - "globus_action_provider_tools.flask.apt_blueprint.TokenChecker.check_token", - return_value=mock_authstate(), -) From bc82e4863e047c0fe48d1910923ebf81936590ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:19:18 +0000 Subject: [PATCH 14/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/psf/black-pre-commit-mirror: 24.8.0 → 24.10.0](https://github.com/psf/black-pre-commit-mirror/compare/24.8.0...24.10.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ae197d..3e30b9d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -32,7 +32,7 @@ repos: args: [--py38-plus] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black From f00718eae5fbc1bcc2aac1490cd66f5951a01957 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Oct 2024 14:27:35 -0500 Subject: [PATCH 15/24] Fix coverage reporting to count src/ properly (#170) --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f66fe17..4355ce1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,11 @@ source = [ "globus_action_provider_tools", "tests", ] +[tool.coverage.paths] +source = [ + "src/", + "*/site-packages/", +] [tool.coverage.report] # When the test coverage increases, this bar should also raise. From 4ba7b284bad476d46863bba6dff36cde72295cfc Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 8 Oct 2024 14:27:49 -0500 Subject: [PATCH 16/24] Remove 'aud' field checking and fields (#171) Removing audience checking is safe, in that we remain fully spec compliant. The field is optional for servers and optional for clients to validate even when it's present. The source for the expected audience value is typically the same as that of the client credentials, and therefore there is no additional safety (e.g., against credential confusion bugs) being added by requiring this field. It only serves to add surface area and therefore complexity. This is a breaking change, in that the interfaces for the library are changing to remove a field. --- ...25419_sirosen_remove_expected_audience.rst | 14 +++++++++ examples/watchasay/app/config.py | 1 - examples/watchasay/app/provider.py | 1 - examples/whattimeisitrightnow/app/app.py | 4 +-- examples/whattimeisitrightnow/app/config.py | 1 - .../authentication.py | 29 ++----------------- .../flask/api_helpers.py | 9 ------ .../flask/apt_blueprint.py | 10 ------- tests/conftest.py | 2 -- tests/data/canned_responses.py | 4 --- tests/test_authentication.py | 22 ++++---------- tests/test_flask_helpers/conftest.py | 1 - 12 files changed, 22 insertions(+), 76 deletions(-) create mode 100644 changelog.d/20241008_125419_sirosen_remove_expected_audience.rst diff --git a/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst b/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst new file mode 100644 index 0000000..b138abe --- /dev/null +++ b/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst @@ -0,0 +1,14 @@ +Changes +------- + +- The ``aud`` field of token introspect responses is no longer validated and + fields associated with it have been removed. This includes changes to + function and class initializer signatures. + + - The ``expected_audience`` field is no longer supported in ``AuthState`` and + ``TokenChecker``. It has been removed from the initializers for these + classes. + + - ``globus_auth_client_name`` has been removed from ``ActionProviderBlueprint``. + + - ``client_name`` has been removed from ``add_action_routes_to_blueprint``. diff --git a/examples/watchasay/app/config.py b/examples/watchasay/app/config.py index bd18049..3f8c89d 100644 --- a/examples/watchasay/app/config.py +++ b/examples/watchasay/app/config.py @@ -3,4 +3,3 @@ our_scope = ( "https://auth.globus.org/scopes/d3a66776-759f-4316-ba55-21725fe37323/action_all" ) -token_audience = None diff --git a/examples/watchasay/app/provider.py b/examples/watchasay/app/provider.py index 6920780..ac789b2 100644 --- a/examples/watchasay/app/provider.py +++ b/examples/watchasay/app/provider.py @@ -233,7 +233,6 @@ def create_app(): blueprint=skeleton_blueprint, client_id=config.client_id, client_secret=config.client_secret, - client_name=None, provider_description=provider_description, action_run_callback=action_run, action_status_callback=action_status, diff --git a/examples/whattimeisitrightnow/app/app.py b/examples/whattimeisitrightnow/app/app.py index 24bb3bc..8730ef6 100644 --- a/examples/whattimeisitrightnow/app/app.py +++ b/examples/whattimeisitrightnow/app/app.py @@ -26,9 +26,7 @@ app = Flask(__name__) assign_json_provider(app) -token_checker = TokenChecker( - config.client_id, config.client_secret, [config.our_scope], config.token_audience -) +token_checker = TokenChecker(config.client_id, config.client_secret, [config.our_scope]) COMPLETE_STATES = (ActionStatusValue.SUCCEEDED, ActionStatusValue.FAILED) INCOMPLETE_STATES = (ActionStatusValue.ACTIVE, ActionStatusValue.INACTIVE) diff --git a/examples/whattimeisitrightnow/app/config.py b/examples/whattimeisitrightnow/app/config.py index 1cf2bc6..065c333 100644 --- a/examples/whattimeisitrightnow/app/config.py +++ b/examples/whattimeisitrightnow/app/config.py @@ -1,4 +1,3 @@ client_id = "16e16447-209a-4825-ae19-25e279d91642" client_secret = "SECRET" our_scope = "https://auth.globus.org/scopes/16e16447-209a-4825-ae19-25e279d91642/action_all_with_groups" -token_audience = None diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index a215794..5f0236f 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -50,20 +50,12 @@ def __init__( auth_client: ConfidentialAppAuthClient, bearer_token: str, expected_scopes: Iterable[str], - expected_audience: str | None = None, ) -> None: self.auth_client = auth_client self.bearer_token = bearer_token self.sanitized_token = self.bearer_token[-7:] self.expected_scopes = expected_scopes - # Default to client_id unless expected_audience has been explicitly - # provided (supporting legacy clients that may have a different - # client name registered with Auth) - if expected_audience is None: - self.expected_audience = auth_client.client_id - else: - self.expected_audience = expected_audience self.errors: list[Exception] = [] self._groups_client: GroupsClient | None = None @@ -99,12 +91,6 @@ def introspect_token(self) -> GlobusHTTPResponse | None: "Token invalid scopes. " f"Expected one of: {self.expected_scopes}, got: {scopes}" ) - aud = resp.get("aud", []) - if self.expected_audience not in aud: - raise AssertionError( - "Token not intended for us: " - f"audience={aud}, expected={self.expected_audience}" - ) if "identity_set" not in resp: raise AssertionError("Missing identity_set") except AssertionError as err: @@ -366,20 +352,11 @@ def check_authorization( class TokenChecker: def __init__( - self, - client_id: str, - client_secret: str, - expected_scopes: Iterable[str], - expected_audience: str | None = None, + self, client_id: str, client_secret: str, expected_scopes: Iterable[str] ) -> None: self.auth_client = ConfidentialAppAuthClient(client_id, client_secret) self.default_expected_scopes = frozenset(expected_scopes) - if expected_audience is None: - self.expected_audience = client_id - else: - self.expected_audience = expected_audience - def check_token( self, access_token: str, expected_scopes: Iterable[str] | None = None ) -> AuthState: @@ -387,6 +364,4 @@ def check_token( expected_scopes = self.default_expected_scopes else: expected_scopes = frozenset(expected_scopes) - return AuthState( - self.auth_client, access_token, expected_scopes, self.expected_audience - ) + return AuthState(self.auth_client, access_token, expected_scopes) diff --git a/src/globus_action_provider_tools/flask/api_helpers.py b/src/globus_action_provider_tools/flask/api_helpers.py index 0505a84..65af2d4 100644 --- a/src/globus_action_provider_tools/flask/api_helpers.py +++ b/src/globus_action_provider_tools/flask/api_helpers.py @@ -106,7 +106,6 @@ def add_action_routes_to_blueprint( blueprint: flask.Blueprint, client_id: str, client_secret: str, - client_name: str | None, provider_description: ActionProviderDescription, action_run_callback: ActionRunCallback, action_status_callback: ActionStatusCallback, @@ -142,13 +141,6 @@ def add_action_routes_to_blueprint( A Globus Auth generated ``client_secret`` which will be used when validating input request tokens. - ``client_name`` (*string*) Most commonly, this will be a None value. In the rare, - legacy case where a name has been associated with a client_id, it can be provided - here. If you are not aware of a name associated with your client_id, it most likely - doesn't have one and the value should be None. This will be passed to the - (:class:`TokenChecker`) as the - `expected_audience`. - ``provider_description`` (:class:`ActionProviderDescription\ `) A structure describing the provider to be returned by the provider introspection @@ -190,7 +182,6 @@ def add_action_routes_to_blueprint( client_id=client_id, client_secret=client_secret, expected_scopes=all_accepted_scopes, - expected_audience=client_name, ) assign_json_provider(blueprint) diff --git a/src/globus_action_provider_tools/flask/apt_blueprint.py b/src/globus_action_provider_tools/flask/apt_blueprint.py index 95f8a5c..b25357b 100644 --- a/src/globus_action_provider_tools/flask/apt_blueprint.py +++ b/src/globus_action_provider_tools/flask/apt_blueprint.py @@ -53,7 +53,6 @@ def __init__( self, provider_description: ActionProviderDescription, *args, - globus_auth_client_name: t.Optional[str] = None, additional_scopes: t.Iterable[str] = (), action_repository: t.Optional[AbstractActionRepository] = None, request_lifecycle_hooks: t.Optional[t.List[t.Any]] = None, @@ -66,13 +65,6 @@ def __init__( :param provider_description: A Provider Description which will be returned from introspection calls to this Blueprint. - :param globus_auth_client_name: The name of the Globus Auth Client (also - known as the resource server name). This will be used to validate the - intended audience for tokens passed to the operations on this - Blueprint. By default, the client id will be used for checkign audience, - and unless the client has explicitly been given a resource server name - in Globus Auth, this will be proper behavior. - :param additional_scopes: Additional scope strings the Action Provider should allow scopes in addition to the one specified by the ``globus_auth_scope`` value of the input provider description. Only @@ -93,7 +85,6 @@ def __init__( provider_description, config=config, ) - self.globus_auth_client_name = globus_auth_client_name self.additional_scopes = additional_scopes self.config = config @@ -170,7 +161,6 @@ def _create_token_checker(self, setup_state: blueprints.BlueprintSetupState): client_id=client_id, client_secret=client_secret, expected_scopes=scopes, - expected_audience=self.globus_auth_client_name, ) def _action_introspect(self): diff --git a/tests/conftest.py b/tests/conftest.py index 03042ae..5cb6a90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,6 @@ def config(): "client_id": canned_responses.mock_client_id(), "client_secret": canned_responses.mock_client_secret(), "expected_scopes": (canned_responses.mock_scope(),), - "expected_audience": canned_responses.mock_expected_audience(), } @@ -53,7 +52,6 @@ def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: client_id=config["client_id"], client_secret=config["client_secret"], expected_scopes=config["expected_scopes"], - expected_audience=config["expected_audience"], ) auth_state = checker.check_token("NOT_A_TOKEN") diff --git a/tests/data/canned_responses.py b/tests/data/canned_responses.py index 4e21656..65680cf 100644 --- a/tests/data/canned_responses.py +++ b/tests/data/canned_responses.py @@ -117,7 +117,3 @@ def mock_client_secret(): def mock_effective_identity() -> str: return "00000000-0000-0000-0000-000000000000" - - -def mock_expected_audience() -> str: - return "action_provider_tools_automated_tests" diff --git a/tests/test_authentication.py b/tests/test_authentication.py index fbacaaf..b5c384c 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -9,32 +9,23 @@ from globus_action_provider_tools.authentication import AuthState, identity_principal -def get_auth_state_instance( - expected_scopes: t.Iterable[str], - expected_audience: str, -) -> AuthState: +def get_auth_state_instance(expected_scopes: t.Iterable[str]) -> AuthState: return AuthState( auth_client=globus_sdk.ConfidentialAppAuthClient("bogus", "bogus"), bearer_token="bogus", expected_scopes=expected_scopes, - expected_audience=expected_audience, ) @pytest.fixture def auth_state(mocked_responses) -> AuthState: - """Create an AuthState instance. - - AuthState compares its `expected_scopes` and `expected_audience` values - against the values present in an API request and will fail if they don't match. - Unfortunately, this currently means that these values are duplicated - in the API fixture .yaml files and here. - """ + """Create an AuthState instance.""" AuthState.dependent_tokens_cache.clear() AuthState.group_membership_cache.clear() AuthState.introspect_cache.clear() - return get_auth_state_instance(["expected-scope"], "expected-audience") + # note that expected-scope MUST match the fixture data + return get_auth_state_instance(["expected-scope"]) def test_get_identities(auth_state, freeze_time): @@ -84,10 +75,7 @@ def test_caching_groups(auth_state, freeze_time, mocked_responses): def test_auth_state_caching_across_instances(auth_state, freeze_time, mocked_responses): response = freeze_time(load_response("token-introspect", case="success")) - duplicate_auth_state = get_auth_state_instance( - auth_state.expected_scopes, - auth_state.expected_audience, - ) + duplicate_auth_state = get_auth_state_instance(auth_state.expected_scopes) assert duplicate_auth_state is not auth_state assert len(auth_state.identities) == len(response.metadata["identities"]) diff --git a/tests/test_flask_helpers/conftest.py b/tests/test_flask_helpers/conftest.py index 65bc1ae..dce4161 100644 --- a/tests/test_flask_helpers/conftest.py +++ b/tests/test_flask_helpers/conftest.py @@ -72,7 +72,6 @@ def add_routes_app(flask_helpers_noauth, auth_state): blueprint=bp, client_id="bogus", client_secret="bogus", - client_name=None, provider_description=ap_description, action_run_callback=mock_action_run_func, action_status_callback=mock_action_status_func, From 463e0c49c425af8ef915eaadbadb44659e5232ce Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Oct 2024 12:30:27 -0500 Subject: [PATCH 17/24] Remove an unreachable code block (#174) --- src/globus_action_provider_tools/authentication.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index 5f0236f..149c7ad 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -244,15 +244,7 @@ def get_authorizer_for_scope( refresh_token = cast(str, dep_tkn_resp.get("refresh_token")) access_token = cast(str, dep_tkn_resp.get("access_token")) token_expiration = dep_tkn_resp.get("expires_at_seconds", 0) - # IF for some reason the token_expiration comes in a string, or even a string - # containing a float representation, try converting to a proper int. If the - # conversion is impossible, set expiration to 0 which should force some sort of - # refresh as described elsewhere. - if not isinstance(token_expiration, int): - try: - token_expiration = int(float(token_expiration)) - except ValueError: - token_expiration = 0 + now = time() # IF we have an access token, we'll try building an authorizer from it if it is From c96ca4e0ed7515761062686cc351edc0609cfacb Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Oct 2024 12:36:31 -0500 Subject: [PATCH 18/24] Remove the 'testing' module (#172) This is a *large* surface area exposed via a very small number of low-utility test helpers. The contracts here cannot be maintained as the main source itself changes, and therefore each change to the source requires an attendant change to the testing helpers and documentation of the change in both locations. Move the only "surviving" test in `test_mocks` to a new `test_meta` test module. --- ...8_141637_sirosen_remove_testing_module.rst | 5 + docs/source/toolkit.rst | 5 - docs/source/toolkit/testing.rst | 208 ------------------ .../testing/__init__.py | 0 .../testing/fixtures.py | 42 ---- .../testing/mocks.py | 40 ---- tests/conftest.py | 35 ++- tests/test_meta.py | 33 +++ tests/test_mocks.py | 72 ------ 9 files changed, 69 insertions(+), 371 deletions(-) create mode 100644 changelog.d/20241008_141637_sirosen_remove_testing_module.rst delete mode 100644 docs/source/toolkit/testing.rst delete mode 100644 src/globus_action_provider_tools/testing/__init__.py delete mode 100644 src/globus_action_provider_tools/testing/fixtures.py delete mode 100644 src/globus_action_provider_tools/testing/mocks.py create mode 100644 tests/test_meta.py delete mode 100644 tests/test_mocks.py diff --git a/changelog.d/20241008_141637_sirosen_remove_testing_module.rst b/changelog.d/20241008_141637_sirosen_remove_testing_module.rst new file mode 100644 index 0000000..863056a --- /dev/null +++ b/changelog.d/20241008_141637_sirosen_remove_testing_module.rst @@ -0,0 +1,5 @@ +Removed +------- + +- ``globus_action_provider_tools.testing`` has been removed. Users who were + relying on these components should make use of their own fixtures and mocks. diff --git a/docs/source/toolkit.rst b/docs/source/toolkit.rst index 6b6dd14..bce0fe0 100644 --- a/docs/source/toolkit.rst +++ b/docs/source/toolkit.rst @@ -26,10 +26,6 @@ common requirements so the focus can be on the logic of the Action provided. 5. :doc:`Caching guide ` for tweaking the performance of Action Providers with relation to Globus Auth. -6. :doc:`Testing tools ` provides various resources for -stubbing Authentication out of an Action Provider and providing a simple way of -validating an Action Provider's behavior. - .. toctree:: :maxdepth: 1 :hidden: @@ -39,7 +35,6 @@ validating an Action Provider's behavior. toolkit/data_types toolkit/flask_helpers toolkit/validation - toolkit/testing .. _Pydantic: https://pydantic-docs.helpmanual.io/ diff --git a/docs/source/toolkit/testing.rst b/docs/source/toolkit/testing.rst deleted file mode 100644 index ea7a065..0000000 --- a/docs/source/toolkit/testing.rst +++ /dev/null @@ -1,208 +0,0 @@ -Testing -======= - -An Action Provider is closely integrated with Globus Auth (see -:ref:`globus_auth_setup`). This integration makes it easy to validate incoming -requests and ensures that the requester is authorized to execute actions against -an Action Provider. However, the integration can make it difficult to run tests -against an Action Provider to validate that its endpoints behave correctly. -During a CI/CD pipeline, it may be a requirement to start your Action Provider -without a valid Client ID or Secret. - -The toolkit provides various tools to enable testing and validation in the -:code:`globus_action_provider_tools.testing` module. - - -Fixtures -^^^^^^^^ - -The toolkit provides various `pytest fixtures -`_ that greatly reduce the need -to manually mock and patch interactions with Globus Auth. If an Action Provider -is created using any of the Flask helpers provided in the -:code:`globus_action_provider_tools.flask` module, we provide a fixture to -easily mock authentication out of your Action Provider. Each of the Flask -helpers internally creates a *TokenChecker* which does two things: it validates -that the Action Provider is correctly configured with a valid Globus Auth Client -ID and Secret, and it validates that incoming requests contain a valid token. -These fixtures abstract both aspects of the internal *TokenChecker* so that you -can focus on testing your Action Provider's behavior and logic. - - -Action Provider Blueprint -------------------------- - -If your Action Provider is built using the *ActionProviderBlueprint* Flask -helper, use the :code:`apt_blueprint_noauth` fixture in -:code:`globus_action_provider_tools.testing.fixtures`: - -.. code-block:: python - - from globus_action_provider_tools.testing.fixtures import ( - apt_blueprint_noauth - ) - -Once imported, it can be used just as any other pytest fixture. We recommend -passing it as a parameter to another fixture which ultimately creates the app -and returns a resource for use in tests. Provide the fixture your -*ActionProviderBlueprint* instance under test. This will update your instance -with stubbed out authentication. The example below shows how to create a -:code:`client` fixture which can be used to make unauthenticated HTTP requests -against the Action Provider: - -.. code-block:: python - - from myapp import action_provider_blueprint - - @pytest.fixture(scope="module") - def client(apt_blueprint_noauth): - apt_blueprint_noauth(action_provider_blueprint) - app = create_app() - yield app.test_client() - -Once composed like this, you can use the :code:`client` fixture in your tests to -receive and use a Flask *test_client* to make unauthenticated requests against -your Action Provider: - -.. code-block:: python - - def test_introspection_endpoint(client): - response = client.get("/") - assert response.status_code == 200 - - -Flask API Helpers ------------------ - -If your Action Provider is built using the -:code:`add_action_routes_to_blueprint` Flask helper, use the -:code:`flask_helpers_noauth` fixture in -:code:`globus_action_provider_tools.testing.fixtures*`: - -.. code-block:: python - - from globus_action_provider_tools.testing.fixtures import ( - flask_helpers_noauth - ) - -Once imported, simply pass the :code:`flask_helpers_noauth` fixture as a -parameter to another fixture which creates the app and returns a resource for -use in tests. Unlike the :code:`apt_blueprint_noauth` fixture, the -:code:`flask_helpers_noauth` fixture does not need to be explicitly executed - -simply passing it as a parameter is sufficient to temporarily stub out the -Provider's authentication. An example of how to create a :code:`client` fixture -which can be used to make unauthenticated HTTP requests against the Action -Provider is shown below: - -.. code-block:: python - - @pytest.fixture(scope="module") - def client(flask_helpers_noauth): - app = create_app() - app.config["TESTING"] = True - yield app.test_client() - -Once composed like this, you can use the :code:`client` fixture in your tests to -receive and use a Flask *test_client* to make requests against your Action -Provider: - -.. code-block:: python - - def test_introspection_endpoint(client): - response = client.get("/") - assert response.status_code == 200 - -.. note:: - - The :code:`flask_helpers_noauth` fixture will patch the TokenChecker in a - global scope during testing, meaning that any other Action Providers that - are themselves built using the Flask API Helpers will also have their - TokenChecker's patched. This may lead to unintended issues if testing - multiple Action Providers in the same pytest test session. If this is your - case, we highly recommend isolating your Action Provider tests. - - -Mocks -^^^^^ - -The toolkit provides various `mocks -`_ which -can be used individually to stub out your Action Provider's authentication. You -should use these directly if you are writing an Action Provider using a -non-Flask framework or if you've decided not to use the built in Flask helpers. - -.. note:: - - This toolkit uses these mocks within the - :code:`globus_action_provider_tools.testing.fixtures` module. - - -.. _mock-authstate: - -Mock AuthState --------------- - -An *AuthState* represents a requester's authentication status and Globus Auth -information. Every request should have its token validated via the -*TokenChecker*'s :code:`check_token` method, which in turns generates an -*AuthState* object. - -During testing, it is convenient to not provide valid tokens with every request. -Use the :code:`mock_authstate` mock to generate a stubbed out *AuthState* object -that won't validate requester properties against Globus Auth. This is most -useful when used in a patch as the return value for the *TokenChecker*'s -:code:`check_token` method: - -.. code-block:: python - - import pytest - from globus_action_provider_tools.testing.mocks import mock_authstate - - @pytest.fixture - def client(monkeypatch): - monkeypatch.setattr( - "globus_action_provider_tools.authentication.TokenChecker.check_token", - mock_authstate, - ) - yield app.test_client() - -The example above creates a fixture which can be used to create a client that -can make unauthenticated HTTP requests against an Action Provider. - - -Mock TokenChecker ------------------ - -Because the *TokenChecker* is this toolkit's authentication workhorse, it's -possible to entirely replace the the *TokenChecker* with a mock object. Doing so -will allow your Action Provider to start up without validating its Client ID or -Secret and will also allow unauthenticated requests to be made against it. This -mock provides a simple way of completely removing your app's authentication -during testing. - - -.. code-block:: python - - from unittest import mock - - import pytest - from globus_action_provider_tools.testing.mocks import mock_tokenchecker - - @pytest.fixture - def client(): - with mock.patch( - "my_package.my_app.get_tokenchecker", - return_value=mock_tokenchecker(), - ): - app = create_app() - app.config["TESTING"] = True - yield app.test_client() - - -.. note:: - - This example will only work if there's a function or method that is used to - create the TokenChecker instance. It demonstrates how you can patch a - function or a method to return the Mock TokenChecker. Internally, the Mock - TokenChecker will generate the :ref:`mock-authstate` objects described - above. diff --git a/src/globus_action_provider_tools/testing/__init__.py b/src/globus_action_provider_tools/testing/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/globus_action_provider_tools/testing/fixtures.py b/src/globus_action_provider_tools/testing/fixtures.py deleted file mode 100644 index 8506df5..0000000 --- a/src/globus_action_provider_tools/testing/fixtures.py +++ /dev/null @@ -1,42 +0,0 @@ -from unittest.mock import patch - -import pytest - -from globus_action_provider_tools.flask.apt_blueprint import ActionProviderBlueprint -from globus_action_provider_tools.testing.mocks import mock_authstate, mock_tokenchecker - - -@pytest.fixture(scope="session") -def apt_blueprint_noauth(): - """ - A fixture designed to mock an ActionProviderBlueprint instance's Globus - Auth integration. The fixture returns a function to which the instance - should be supplied as a parameter: - - i.e. apt_blueprint_noauth(aptb) - """ - - def _apt_blueprint_noauth(aptb: ActionProviderBlueprint): - # Manually remove the function that creates the internal token_checker - for f in aptb.deferred_functions: - if f.__name__ == "_create_token_checker": - aptb.deferred_functions.remove(f) - - # Use a mocked token checker internally - aptb.checker = mock_tokenchecker() - - return _apt_blueprint_noauth - - -@pytest.fixture -def flask_helpers_noauth(): - """ - A fixture designed to mock the Globus Auth integration in an Flask app - created using the api_helpers. Simply using this fixture will allow creating - the mocked app. - """ - with patch( - "globus_action_provider_tools.flask.api_helpers.TokenChecker.check_token", - return_value=mock_authstate(), - ): - yield diff --git a/src/globus_action_provider_tools/testing/mocks.py b/src/globus_action_provider_tools/testing/mocks.py deleted file mode 100644 index 874309a..0000000 --- a/src/globus_action_provider_tools/testing/mocks.py +++ /dev/null @@ -1,40 +0,0 @@ -from unittest.mock import Mock - -from globus_action_provider_tools.authentication import AuthState, TokenChecker - - -def mock_authstate(*args, **kwargs): - """ - Returns a dummy AuthState object with mocked out methods and properties. - This is particularly useful for mocking out the TokenChecker.check_token - function. Should only be used for testing because it avoids the need for - supplying valid CLIENT_IDs, CLIENT_SECRETs, and TOKENs - """ - # auth_client = ConfidentialAppAuthClient(None, None) - auth_state = Mock(spec=AuthState, name="MockedAPTAuthState") - - # Spec won't create instance variables created in __init__, so manually - # create bearer_token - auth_state.bearer_token = "MOCK_BEARER_TOKEN" - - # Set property mocks - auth_state.effective_identity = ( - "urn:globus:auth:identity:00000000-0000-0000-0000-000000000000" - ) - auth_state.identities = frozenset([auth_state.effective_identity]) - - # Mock other functions that get called - auth_state.check_authorization.return_value = True - auth_state.introspect_token.return_value = None - - return auth_state - - -def mock_tokenchecker(*args, **kwargs): - """ - Returns a dummy TokenChecker object with a mocked out check_token method. - In turn, this mock can only produce mocked_authstates. - """ - tokenchecker = Mock(spec=TokenChecker, name="MockedAPTTokenChecker") - tokenchecker.check_token.return_value = mock_authstate() - return tokenchecker diff --git a/tests/conftest.py b/tests/conftest.py index 5cb6a90..86d3efc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import datetime import pathlib import typing as t -from unittest.mock import patch +from unittest import mock import freezegun import globus_sdk @@ -16,8 +16,6 @@ from .data import canned_responses -pytest_plugins = ("globus_action_provider_tools.testing.fixtures",) - @pytest.fixture def config(): @@ -29,7 +27,7 @@ def config(): @pytest.fixture -@patch("globus_action_provider_tools.authentication.ConfidentialAppAuthClient") +@mock.patch("globus_action_provider_tools.authentication.ConfidentialAppAuthClient") def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: # Mock the introspection first because that gets called as soon as we create # a TokenChecker @@ -64,6 +62,35 @@ def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: return auth_state +@pytest.fixture +def apt_blueprint_noauth(auth_state): + """ + A fixture function which will mock an ActionProviderBlueprint instance's + TokenChecker. + """ + + def _apt_blueprint_noauth(aptb): + # Manually remove the function that creates the internal token_checker + for f in aptb.deferred_functions: + if f.__name__ == "_create_token_checker": + aptb.deferred_functions.remove(f) + + # Use a mocked auth state builder internally + aptb.checker = mock.Mock() + aptb.checker.check_token.return_value = auth_state + + return _apt_blueprint_noauth + + +@pytest.fixture +def flask_helpers_noauth(auth_state): + with mock.patch( + "globus_action_provider_tools.flask.api_helpers.TokenChecker.check_token", + return_value=auth_state, + ): + yield + + @pytest.fixture(scope="session", autouse=True) def register_api_fixtures(): for yaml_file in (pathlib.Path(__file__).parent / "api-fixtures").rglob("*.yaml"): diff --git a/tests/test_meta.py b/tests/test_meta.py new file mode 100644 index 0000000..dcc2549 --- /dev/null +++ b/tests/test_meta.py @@ -0,0 +1,33 @@ +""" +This 'meta test' module tests fixtures and testing tools. +""" + +import pytest +from globus_sdk._testing import RegisteredResponse + + +def test_freeze_time_two_freezes(freeze_time): + """Verify that it's impossible to freeze time twice. + + Did you find this test because you added new tests, got an error, + tried changing some code, and are now seeing this test fail? + Great! You've come to the right place. + + If you need to freeze time twice, it may be easier to align the time + in your mocked API response with the time in an existing mocked API response. + You can do this by changing the time values in your new API response. + Do not add `metadata["freezegun"]` in your new API response. + + If you *really* need to freeze time twice, go ahead and do so. + You can change or remove this test completely, + but please ensure that you are testing the changes you introduced + in the `freeze_time` fixture (if any). + """ + + response = RegisteredResponse( + path="does/not/matter", + metadata={"freezegun": 1000}, + ) + freeze_time(response) + with pytest.raises(AssertionError): + freeze_time(response) diff --git a/tests/test_mocks.py b/tests/test_mocks.py deleted file mode 100644 index 9de77e3..0000000 --- a/tests/test_mocks.py +++ /dev/null @@ -1,72 +0,0 @@ -import pytest -from globus_sdk._testing import RegisteredResponse - -from globus_action_provider_tools import AuthState, TokenChecker -from globus_action_provider_tools.testing.mocks import mock_authstate, mock_tokenchecker - - -def test_create_mocked_tokenchecker(): - tc = mock_tokenchecker("", "not_a_secret", bogus_kwarg="sure") - - assert tc is not None - assert isinstance(tc, TokenChecker) - - -def test_mocked_tokenchecker_checks_token(): - auth = mock_tokenchecker().check_token(None) - - assert auth is not None - assert isinstance(auth, AuthState) - - -def test_tokenchecker_is_specced(): - tc = mock_tokenchecker() - with pytest.raises(AttributeError): - tc.not_a_valid_method() - - -def test_create_mocked_authstate(): - auth = mock_authstate("", "not_a_secret", bogus_kwarg="sure") - - assert auth is not None - assert isinstance(auth, AuthState) - - -def test_authstate_is_specced(): - authstate = mock_authstate() - with pytest.raises(AttributeError): - authstate.not_a_valid_method() - - -def test_mocked_tokenchecker_creates_mocked_authstate(): - assert ( - mock_authstate().effective_identity - == mock_tokenchecker().check_token().effective_identity - ) - - -def test_freeze_time_two_freezes(freeze_time): - """Verify that it's impossible to freeze time twice. - - Did you find this test because you added new tests, got an error, - tried changing some code, and are now seeing this test fail? - Great! You've come to the right place. - - If you need to freeze time twice, it may be easier to align the time - in your mocked API response with the time in an existing mocked API response. - You can do this by changing the time values in your new API response. - Do not add `metadata["freezegun"]` in your new API response. - - If you *really* need to freeze time twice, go ahead and do so. - You can change or remove this test completely, - but please ensure that you are testing the changes you introduced - in the `freeze_time` fixture (if any). - """ - - response = RegisteredResponse( - path="does/not/matter", - metadata={"freezegun": 1000}, - ) - freeze_time(response) - with pytest.raises(AssertionError): - freeze_time(response) From 53d091dbdd46af9aea2529253ed916cf105236b3 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Oct 2024 12:43:35 -0500 Subject: [PATCH 19/24] Fix caching and checking of introspect responses (#173) - Ensure introspect is cached even if 'active=False' - Don't check claims pointlessly: exp and nbf can fail if your clock is drifted, so it's not safe to be checking them unless users can configure a leeway. Also, 'active' tells you everything you need in OAuth2. - Capture scope validation failures with an exception, not None. - Cache by token hash, not full token string. --- ...08_171503_sirosen_fix_introspect_check.rst | 19 +++++ .../authentication.py | 84 +++++++++++-------- tests/test_authentication.py | 29 +++++-- 3 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 changelog.d/20241008_171503_sirosen_fix_introspect_check.rst diff --git a/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst b/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst new file mode 100644 index 0000000..3a662a5 --- /dev/null +++ b/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst @@ -0,0 +1,19 @@ +Features +-------- + +- The token introspect checking and caching performed in ``AuthState`` has + been improved. + + - The cache is keyed off of token hashes, rather than raw token strings. + + - The ``exp`` and ``nbf`` values are no longer verified, removing the + possibility of incorrect treatment of valid tokens as invalid due to clock + drift. + + - Introspect response caching caches the raw response even for invalid + tokens, meaning that Action Providers will no longer repeatedly introspect + a token once it is known to be invalid. + + - Scope validation raises a new, dedicated error class, + ``globus_action_provider_tools.authentication.InvalidTokenScopesError``, on + failure. diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index 149c7ad..5b0ddd1 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import hashlib import logging from time import time @@ -34,6 +35,19 @@ def identity_principal(id_: str) -> str: return f"urn:globus:auth:identity:{id_}" +class InvalidTokenScopesError(ValueError): + def __init__( + self, expected_scopes: frozenset[str], actual_scopes: frozenset[str] + ) -> None: + self.expected_scopes = expected_scopes + self.actual_scopes = actual_scopes + super().__init__( + f"Token scopes were not valid. " + f"The valid scopes for this service are {self.expected_scopes} but the " + f"token contained {self.actual_scopes} upon inspection." + ) + + class AuthState: # Cache for introspection operations, max lifetime: 30 seconds introspect_cache: TTLCache = TTLCache(maxsize=100, ttl=30) @@ -49,7 +63,7 @@ def __init__( self, auth_client: ConfidentialAppAuthClient, bearer_token: str, - expected_scopes: Iterable[str], + expected_scopes: frozenset[str], ) -> None: self.auth_client = auth_client self.bearer_token = bearer_token @@ -59,48 +73,46 @@ def __init__( self.errors: list[Exception] = [] self._groups_client: GroupsClient | None = None - def introspect_token(self) -> GlobusHTTPResponse | None: - # There are cases where a null or empty string bearer token are present as a - # placeholder - if self.bearer_token is None: - return None + @functools.cached_property + def _token_hash(self) -> str: + return _hash_token(self.bearer_token) - resp = AuthState.introspect_cache.get(self.bearer_token) - if resp is not None: - log.info( - f"Using cached introspection response for token ***{self.sanitized_token}" + def _cached_introspect_call(self) -> GlobusHTTPResponse: + introspect_result = AuthState.introspect_cache.get(self._token_hash) + if introspect_result is not None: + log.debug( + f"Using cached introspection introspect_resultonse for " ) - return resp + return introspect_result - log.info(f"Introspecting token ***{self.sanitized_token}") - resp = self.auth_client.oauth2_token_introspect( + log.debug(f"Introspecting token ") + introspect_result = self.auth_client.oauth2_token_introspect( self.bearer_token, include="identity_set" ) - now = time() + self.introspect_cache[self._token_hash] = introspect_result - try: - if resp.get("active", False) is not True: - raise AssertionError("Invalid token.") - if not resp.get("nbf", now + 4) < (time() + 3): - raise AssertionError("Token not yet valid -- check system clock?") - if not resp.get("exp", 0) > (time() - 3): - raise AssertionError("Token expired.") - scopes = frozenset(resp.get("scope", "").split()) - if not scopes & set(self.expected_scopes): - raise AssertionError( - "Token invalid scopes. " - f"Expected one of: {self.expected_scopes}, got: {scopes}" - ) - if "identity_set" not in resp: - raise AssertionError("Missing identity_set") - except AssertionError as err: - self.errors.append(err) - log.info(err) + return introspect_result + + def introspect_token(self) -> GlobusHTTPResponse | None: + introspect_result = self._cached_introspect_call() + + # FIXME: convert this to an exception, rather than 'None' + # the exception could be raised in _verify_introspect_result + if not introspect_result["active"]: return None - else: - log.info(f"Caching token response for token ***{self.sanitized_token}") - AuthState.introspect_cache[self.bearer_token] = resp - return resp + + self._verify_introspect_result(introspect_result) + return introspect_result + + def _verify_introspect_result(self, introspect_result: GlobusHTTPResponse) -> None: + """ + A helper which checks token introspect properties and raises exceptions on failure. + """ + # validate scopes, ensuring that the token provided accords with the service's + # notion of what operations exist and are supported + scopes = set(introspect_result.get("scope", "").split()) + if any(s not in self.expected_scopes for s in scopes): + raise InvalidTokenScopesError(self.expected_scopes, frozenset(scopes)) @property def effective_identity(self) -> str | None: diff --git a/tests/test_authentication.py b/tests/test_authentication.py index b5c384c..04002b4 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -6,24 +6,31 @@ import pytest from globus_sdk._testing import load_response -from globus_action_provider_tools.authentication import AuthState, identity_principal +from globus_action_provider_tools.authentication import ( + AuthState, + InvalidTokenScopesError, + identity_principal, +) def get_auth_state_instance(expected_scopes: t.Iterable[str]) -> AuthState: return AuthState( auth_client=globus_sdk.ConfidentialAppAuthClient("bogus", "bogus"), bearer_token="bogus", - expected_scopes=expected_scopes, + expected_scopes=frozenset(expected_scopes), ) -@pytest.fixture -def auth_state(mocked_responses) -> AuthState: - """Create an AuthState instance.""" - +@pytest.fixture(autouse=True) +def _clear_auth_state_cache(): AuthState.dependent_tokens_cache.clear() AuthState.group_membership_cache.clear() AuthState.introspect_cache.clear() + + +@pytest.fixture +def auth_state(mocked_responses) -> AuthState: + """Create an AuthState instance.""" # note that expected-scope MUST match the fixture data return get_auth_state_instance(["expected-scope"]) @@ -90,3 +97,13 @@ def test_invalid_grant_exception(auth_state): load_response("token-introspect", case="success") load_response("token", case="invalid-grant") assert auth_state.get_authorizer_for_scope("doesn't matter") is None + + +def test_invalid_scopes_error(): + auth_state = get_auth_state_instance(["bad-scope"]) + load_response("token-introspect", case="success") + with pytest.raises(InvalidTokenScopesError) as excinfo: + auth_state.introspect_token() + + assert excinfo.value.expected_scopes == {"bad-scope"} + assert excinfo.value.actual_scopes == {"expected-scope"} From b6672439995a695b79eb930ed1e8dca2cd93ea09 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Oct 2024 15:16:38 -0500 Subject: [PATCH 20/24] Refactor `get_authorizer_for_scope` (#175) * Refactor `get_authorizer_for_scope` Better control where and how dependent token callouts happen, and note with inline FIXME comments several issues which persist even with the refined implementation. Logically, there is almost no change, although the refactor may make it appear that there is one. `get_authorizer_for_scope` now retries by clearing the cache and fetching new dependent tokens on failure, and has clearer failure semantics. This happened in the previous code as well -- if the access token is expired and the refresh token is missing, a second call to get dependent tokens is issued. However, due to an inaccurate type annotation (`refresh_token` is `cast(str, ...)`, where it should be `str | None`), it *appears* that there was a previously unreachable behavior -- a retry -- which is now reachable. In practical fact, these subtleties do not yet make any difference, as `refresh_token` will always be present and will be a string if there is any data at all. Anything else is an invalid and unreachable state, given that refresh tokens are currently always requested. With the refactor completed, we can tackle separate changes to actually alter behavior. * Add tests for dependent token handling These are just some initial unit tests which validate that some of the basic scenarios behave as expected. --- ...1011_131741_sirosen_fix_get_authorizer.rst | 7 + .../authentication.py | 214 +++++++++++------- tests/test_authentication.py | 73 +++++- 3 files changed, 205 insertions(+), 89 deletions(-) create mode 100644 changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst diff --git a/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst b/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst new file mode 100644 index 0000000..1e56436 --- /dev/null +++ b/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst @@ -0,0 +1,7 @@ +Development +----------- + +- Refactor ``AuthState.get_authorizer_for_scope`` without changing its + primary outward semantics. The ``bypass_dependent_token_cache`` argument + has been removed from its interface, as it is not necessary to expose + with the improved implementation. diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index 5b0ddd1..5d6a54b 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -4,7 +4,7 @@ import hashlib import logging from time import time -from typing import Iterable, cast +from typing import Iterable import globus_sdk from cachetools import TTLCache @@ -15,7 +15,6 @@ GlobusError, GlobusHTTPResponse, GroupsClient, - OAuthTokenResponse, RefreshTokenAuthorizer, ) @@ -77,6 +76,14 @@ def __init__( def _token_hash(self) -> str: return _hash_token(self.bearer_token) + @functools.cached_property + def _dependent_token_cache_key(self) -> str: + # Caching is done based on a hash of the token string, **not** the + # dependent_tokens_cache_id. + # This guarantees that we get a new access token for any upstream service + # calls if we get a new token, which is helpful for cache busting. + return f"dependent_tokens:{_hash_token(self.bearer_token)}" + def _cached_introspect_call(self) -> GlobusHTTPResponse: introspect_result = AuthState.introspect_cache.get(self._token_hash) if introspect_result is not None: @@ -174,23 +181,22 @@ def groups(self) -> frozenset[str]: AuthState.group_membership_cache[groups_token] = groups_set return groups_set - def get_dependent_tokens(self, bypass_cache_lookup=False) -> OAuthTokenResponse: + def get_dependent_tokens( + self, *, bypass_cache_lookup: bool = False + ) -> globus_sdk.OAuthDependentTokenResponse: """ Returns OAuthTokenResponse representing the dependent tokens associated with a particular access token. """ - # Caching is done based on a hash of the token string, **not** the - # dependent_tokens_cache_id. - # This guarantees that we get a new access token for any upstream service - # calls if we get a new token, which is helpful for cache busting. - token_cache_key = f"dependent_tokens:{_hash_token(self.bearer_token)}" + # TODO: consider deprecating and removing this method + # it is no longer used by `get_authorizer_for_scope()`, which now uses logic which cannot + # be satisfied by the contract provided by this method if not bypass_cache_lookup: - resp = AuthState.dependent_tokens_cache.get(token_cache_key) + resp = self.dependent_tokens_cache.get(self._dependent_token_cache_key) if resp is not None: log.info( - f"Using cached dependent token response (key={token_cache_key}) " - f"for token ***{self.sanitized_token}" + f"Using cached dependent token response (key={self._dependent_token_cache_key})" ) return resp @@ -201,100 +207,134 @@ def get_dependent_tokens(self, bypass_cache_lookup=False) -> OAuthTokenResponse: log.info( f"Caching dependent token response for token ***{self.sanitized_token}" ) - AuthState.dependent_tokens_cache[token_cache_key] = resp + self.dependent_tokens_cache[self._dependent_token_cache_key] = resp return resp def get_authorizer_for_scope( self, scope: str, - bypass_dependent_token_cache=False, required_authorizer_expiration_time: int = 60, ) -> RefreshTokenAuthorizer | AccessTokenAuthorizer | None: - """Retrieve a Globus SDK authorizer for use in accessing a further Globus Auth registered - service / "resource server". This authorizer can be passed to any Globus SDK - Client class for use in accessing the Client's service. - - The authorizer is created by first performing a Globus Auth dependent token grant - and looking for the requested scope in the set of tokens returned by the - grant. If a refresh token is present in the grant response, a - RefreshTokenAuthorizer is returned. If no refresh token is present, but an access - token is present, an AccessTokenAuthorizer will be returned. - - A returned AccessTokenAuthorizer is guaranteed to be usable for at least the - value passed in via required_authorizer_expiration_time which defaults to 60 - seconds. This implies that the authorizer, and the client created from it will be - usable for at least this long. It is possible that the access token in the - authorizer may expire after a time greater than this limit. - - If no dependent tokens can be generated for the requested scope, a None value is - returned. - - To avoid redundant calls to perform dependent token grants, the class - caches dependent token results for a particular incoming Bearer token used to - access this Action Provider. - - If for any reason the caller of this function does not want to use a cached - result, but would require that a new dependent grant is performed, the - bypass_dependent_token_cache parameter may be set to True. This is used - automatically in a case where an access token retrieved from cache is already - expired: the function is called recursively to perform a new dependent token - grant to get a new, valid token (even though bypass is set, the new token value - will be added to the cache). + """ + Get dependent tokens for the caller's token, then retrieve token data for the + requested scope and attempt to build an authorizer from that data. + + The class caches dependent token results, regardless of whether or not + building authorizers succeeds. + + .. warning:: + + This call converts *all* errors to `None` results. + + If a dependent refresh token is available in the response, a + RefreshTokenAuthorizer will be built and returned. + Otherwise, this will attempt to build an AccessTokenAuthorizer. + If the access token is or will be expired within the + ``required_authorizer_expiration_time``, it is treated as a failure. """ + # this block is intentionally nested under a single exception handler + # if either dependent token callout fails, the entire operation is treated as a failure try: - dep_tkn_resp = self.get_dependent_tokens( - bypass_cache_lookup=bypass_dependent_token_cache - ).by_scopes[scope] - except (KeyError, globus_sdk.AuthAPIError): + had_cached_value, dependent_tokens = self._get_cached_dependent_tokens() + + # if the dependent token data (which could have been cached) failed to meet + # the requirements... + if not self._dependent_token_response_satisfies_scope_request( + dependent_tokens, scope, required_authorizer_expiration_time + ): + # if there was no cached value, we just got fresh dependent tokens to do this work + # and they weren't sufficient + # there's no reason to expect new tokens would do better + # fail, but do not clear the cache -- it could be satisfactory for some other scope request + if not had_cached_value: + return None # FIXME: raise an exception instead + + # otherwise, the cached value was bad -- fetch and check again, by clearing the cache and + # asking for the same data + del self.dependent_tokens_cache[self._dependent_token_cache_key] + _, dependent_tokens = self._get_cached_dependent_tokens() + + # check again against requirements -- this is guaranteed to be fresh data + if not self._dependent_token_response_satisfies_scope_request( + dependent_tokens, scope, required_authorizer_expiration_time + ): + return None # FIXME: raise an exception instead + except globus_sdk.AuthAPIError: log.warning( f"Unable to create GlobusAuthorizer for scope {scope}. Using 'None'", exc_info=True, ) return None - refresh_token = cast(str, dep_tkn_resp.get("refresh_token")) - access_token = cast(str, dep_tkn_resp.get("access_token")) - token_expiration = dep_tkn_resp.get("expires_at_seconds", 0) - - now = time() - - # IF we have an access token, we'll try building an authorizer from it if it is - # valid long enough - if access_token is not None: - # If the access token will not expire for at least the required expiration - # time, we return an authorizer based on that access token. - if token_expiration > (int(now) + required_authorizer_expiration_time): - log.debug(f"Creating an AccessTokenAuthorizer for scope {scope}") - return AccessTokenAuthorizer(access_token) - elif refresh_token is not None: - # If the access token is going to expire, but we have a refresh token, ew - # build an authorizer using the refresh token which will, in turn, perform - # token refresh when needed. - - log.debug(f"Creating a RefreshTokenAuthorizer for scope {scope}") - return RefreshTokenAuthorizer( - refresh_token, - self.auth_client, - access_token=access_token, - expires_at=token_expiration, - ) - elif not bypass_dependent_token_cache: - # If we aren't already trying to force a new grant by bypassing the - # cache, try again to find a usable token, but bypass the cache so we - # force a new dependent grant - return self.get_authorizer_for_scope( - scope, - bypass_dependent_token_cache=True, - required_authorizer_expiration_time=required_authorizer_expiration_time, - ) + token_data = dependent_tokens.by_scopes[scope] - # Fall through and haven't been able to create an authorizer, so return - # none - log.warning( - f"Unable to create GlobusAuthorizer for scope {scope}. Using 'None'" + refresh_token: str | None = token_data.get("refresh_token") + if refresh_token is not None: + return RefreshTokenAuthorizer( + refresh_token, + self.auth_client, + access_token=token_data["access_token"], + expires_at=token_data["expires_at_seconds"], + ) + else: + return AccessTokenAuthorizer(token_data["access_token"]) + + def _get_cached_dependent_tokens( + self, + ) -> tuple[bool, globus_sdk.OAuthDependentTokenResponse]: + """ + Get dependent token data, potentially from cache. + Return the data paired with a bool indicating whether or not the value was + cached or a fresh callout. + """ + if self._dependent_token_cache_key in self.dependent_tokens_cache: + return (True, self.dependent_tokens_cache[self._dependent_token_cache_key]) + + # FIXME: switch from dependent refresh tokens to dependent access tokens + # + # dependent refresh tokens in an ephemeral execution context are not appropriate + # because the dependent refresh tokens are cached in-memory, not in a persistent store of state, + # this means we're fetching long-lived credentials which are then not persisted into long term + # storage + # + # the discrepancy between the credential type and the storage strategy reveals that these tokens + # are only used in a synchronous context in which the refresh token is not needed or wanted + # + token_response = self.auth_client.oauth2_get_dependent_tokens( + self.bearer_token, additional_params={"access_type": "offline"} ) - return None + self.dependent_tokens_cache[self._dependent_token_cache_key] = token_response + return (False, token_response) + + def _dependent_token_response_satisfies_scope_request( + self, + r: globus_sdk.OAuthDependentTokenResponse, + scope: str, + required_authorizer_expiration_time: int, + ) -> bool: + """ + Check a token response to see if it appears to satisfy the request for tokens + with a specific scope. + + :returns: True if the data is good, False if the data is bad + """ + try: + token_data = r.by_scopes[scope] + except LookupError: + return False + + refresh_token: str | None = token_data.get("refresh_token") + access_token: str | None = token_data.get("access_token") + token_expiration: int = token_data.get("expires_at_seconds", 0) + + if refresh_token is not None: + return True + + if token_expiration <= (int(time()) + required_authorizer_expiration_time): + return False + + return access_token is not None def _get_groups_client(self) -> GroupsClient: if self._groups_client is not None: diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 04002b4..9a78345 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -1,10 +1,12 @@ from __future__ import annotations +import time import typing as t +from unittest import mock import globus_sdk import pytest -from globus_sdk._testing import load_response +from globus_sdk._testing import RegisteredResponse, load_response from globus_action_provider_tools.authentication import ( AuthState, @@ -14,8 +16,11 @@ def get_auth_state_instance(expected_scopes: t.Iterable[str]) -> AuthState: + client = globus_sdk.ConfidentialAppAuthClient( + "bogus", "bogus", transport_params={"max_retries": 0} + ) return AuthState( - auth_client=globus_sdk.ConfidentialAppAuthClient("bogus", "bogus"), + auth_client=client, bearer_token="bogus", expected_scopes=frozenset(expected_scopes), ) @@ -99,6 +104,70 @@ def test_invalid_grant_exception(auth_state): assert auth_state.get_authorizer_for_scope("doesn't matter") is None +def test_dependent_token_callout_500_fails_dependent_authorization(auth_state): + """ + On a 5xx response, getting an authorizer fails. + + FIXME: currently this simply emits 'None' -- in the future the error should propagate + """ + RegisteredResponse( + service="auth", path="/v2/oauth2/token", method="POST", status=500 + ).add() + assert ( + auth_state.get_authorizer_for_scope( + "urn:globus:auth:scope:groups.api.globus.org:view_my_groups_and_memberships" + ) + is None + ) + + +def test_dependent_token_callout_success_fixes_bad_cache(auth_state): + """ + Populate the cache "incorrectly" and then "fix it" by asking for an authorizer + and expecting the dependent token logic to appropriately redrive. + """ + # the mock by_scopes value is a dict -- similar enough since it just needs to be + # a mapping type -- which we populate for "foo" + mock_response = mock.Mock() + mock_response.by_scopes = { + "foo_scope": { + "expires_at_seconds": time.time() + 100, + "access_token": "foo_AT", + "refresh_token": "foo_RT", + } + } + auth_state.dependent_tokens_cache[auth_state._dependent_token_cache_key] = ( + mock_response + ) + + # register a response for a different resource server -- 'bar' + RegisteredResponse( + service="auth", + path="/v2/oauth2/token", + method="POST", + json=[ + { + "resource_server": "bar", + "scope": "bar_scope", + "expires_at_seconds": time.time() + 100, + "access_token": "bar_AT", + "refresh_token": "bar_RT", + } + ], + ).add() + # now get the 'bar_scope' authorizer + authorizer = auth_state.get_authorizer_for_scope("bar_scope") + + # it should be a refresh token authorizer and the cache should be updated + assert isinstance(authorizer, globus_sdk.RefreshTokenAuthorizer) + cache_value = auth_state.dependent_tokens_cache[ + auth_state._dependent_token_cache_key + ] + assert isinstance(cache_value, globus_sdk.OAuthDependentTokenResponse) + assert "foo_scope" not in cache_value.by_scopes + assert "bar_scope" in cache_value.by_scopes + + def test_invalid_scopes_error(): auth_state = get_auth_state_instance(["bad-scope"]) load_response("token-introspect", case="success") From 13fb32e7a0105ec0b06e1cfca216402565d9bb94 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 11 Oct 2024 16:48:40 -0500 Subject: [PATCH 21/24] Make mypy stricter (#176) * Make mypy stricter - turn on a variety of mypy strictness flags, but stay short of `disallow_untyped_defs` -- which is why we aren't in strict mode - `tox r -e mypy` does not pass any CLI flags - cleanup numerous annotations - in some cases, make minor implementation ordering adjustments to make things pass - Put a small shim in place over TTLCache to allow its contents to be type checked * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/globus_action_provider_tools/utils.py Co-authored-by: Ada <107940310+ada-globus@users.noreply.github.com> * Fix typing issues on older pythons --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ada <107940310+ada-globus@users.noreply.github.com> --- pyproject.toml | 12 +++++++ .../authentication.py | 21 +++++++---- .../data_types.py | 4 +-- .../flask/api_helpers.py | 4 +-- .../flask/apt_blueprint.py | 14 ++++---- .../flask/helpers.py | 4 +-- src/globus_action_provider_tools/utils.py | 35 +++++++++++++++++++ tox.ini | 2 +- 8 files changed, 75 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4355ce1..856a3de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,3 +90,15 @@ filterwarnings = [ # dateutil, used by freezegun during testing, has a Python 3.12 compatibility issue. "ignore:datetime.datetime.utcfromtimestamp\\(\\) is deprecated:DeprecationWarning", ] + +[tool.mypy] +sqlite_cache = true +ignore_missing_imports = true +disallow_subclassing_any = false +warn_unreachable = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_return_any = true +warn_no_return = true +no_implicit_optional = true +# disallow_untyped_defs = true diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index 5d6a54b..ec3d48a 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -3,11 +3,10 @@ import functools import hashlib import logging -from time import time +import time from typing import Iterable import globus_sdk -from cachetools import TTLCache from globus_sdk import ( AccessTokenAuthorizer, ConfidentialAppAuthClient, @@ -18,6 +17,8 @@ RefreshTokenAuthorizer, ) +from .utils import TypedTTLCache + log = logging.getLogger(__name__) @@ -49,14 +50,20 @@ def __init__( class AuthState: # Cache for introspection operations, max lifetime: 30 seconds - introspect_cache: TTLCache = TTLCache(maxsize=100, ttl=30) + introspect_cache: TypedTTLCache[globus_sdk.GlobusHTTPResponse] = TypedTTLCache( + maxsize=100, ttl=30 + ) # Cache for dependent tokens, max lifetime: 47 hours: a bit less than the 48 hours # until a refresh would be required anyway - dependent_tokens_cache: TTLCache = TTLCache(maxsize=100, ttl=47 * 3600) + dependent_tokens_cache: TypedTTLCache[globus_sdk.OAuthDependentTokenResponse] = ( + TypedTTLCache(maxsize=100, ttl=47 * 3600) + ) # Cache for group lookups, max lifetime: 5 minutes - group_membership_cache: TTLCache = TTLCache(maxsize=100, ttl=60 * 5) + group_membership_cache: TypedTTLCache[frozenset[str]] = TypedTTLCache( + maxsize=100, ttl=60 * 5 + ) def __init__( self, @@ -140,7 +147,7 @@ def identities(self) -> frozenset[str]: def principals(self) -> frozenset[str]: return self.identities.union(self.groups) - @property # type: ignore + @property def groups(self) -> frozenset[str]: try: groups_client = self._get_groups_client() @@ -331,7 +338,7 @@ def _dependent_token_response_satisfies_scope_request( if refresh_token is not None: return True - if token_expiration <= (int(time()) + required_authorizer_expiration_time): + if token_expiration <= (int(time.time()) + required_authorizer_expiration_time): return False return access_token is not None diff --git a/src/globus_action_provider_tools/data_types.py b/src/globus_action_provider_tools/data_types.py index b4de61c..7486b5b 100644 --- a/src/globus_action_provider_tools/data_types.py +++ b/src/globus_action_provider_tools/data_types.py @@ -215,7 +215,7 @@ class ActionStatus(BaseModel): min_length=1, max_length=64, ) - monitor_by: Set[str] = Field( + monitor_by: Optional[Set[str]] = Field( default_factory=set, description=( "A list of principal URNs containing identities which are allowed to " @@ -224,7 +224,7 @@ class ActionStatus(BaseModel): ), regex=principal_urn_regex, ) - manage_by: Set[str] = Field( + manage_by: Optional[Set[str]] = Field( default_factory=set, description=( "A list of principal URNs containing identities which are allowed " diff --git a/src/globus_action_provider_tools/flask/api_helpers.py b/src/globus_action_provider_tools/flask/api_helpers.py index 65af2d4..e35b930 100644 --- a/src/globus_action_provider_tools/flask/api_helpers.py +++ b/src/globus_action_provider_tools/flask/api_helpers.py @@ -68,8 +68,8 @@ def _api_operation_for_request(request: flask.Request) -> str: - method = request.path.rsplit("/", 1) - op_name = method[-1] + method: str = request.path.rsplit("/", 1) + op_name: str = method[-1] return op_name diff --git a/src/globus_action_provider_tools/flask/apt_blueprint.py b/src/globus_action_provider_tools/flask/apt_blueprint.py index b25357b..c778b61 100644 --- a/src/globus_action_provider_tools/flask/apt_blueprint.py +++ b/src/globus_action_provider_tools/flask/apt_blueprint.py @@ -298,7 +298,7 @@ def _action_resume(self, action_id: str): try: if action: - result = self.action_resume_callback(action, g.auth_state) # type: ignore + result = self.action_resume_callback(action, g.auth_state) else: result = self.action_resume_callback(action_id, g.auth_state) # type: ignore except AttributeError: @@ -354,7 +354,7 @@ def _action_status(self, action_id: str): try: if action: - result = self.action_status_callback(action, g.auth_state) # type: ignore + result = self.action_status_callback(action, g.auth_state) else: result = self.action_status_callback(action_id, g.auth_state) # type: ignore except AttributeError: @@ -397,7 +397,7 @@ def _action_cancel(self, action_id: str): try: if action: - result = self.action_cancel_callback(action, g.auth_state) # type: ignore + result = self.action_cancel_callback(action, g.auth_state) else: result = self.action_cancel_callback(action_id, g.auth_state) # type: ignore except AttributeError: @@ -452,7 +452,7 @@ def _action_release(self, action_id: str): try: if action: - result = self.action_release_callback(action, g.auth_state) # type: ignore + result = self.action_release_callback(action, g.auth_state) else: result = self.action_release_callback(action_id, g.auth_state) # type: ignore except ValidationError as ve: @@ -516,10 +516,10 @@ def _save_action( Executes an action_saver to store the ActionStatus in the specified backend. """ - if isinstance(result, ActionStatus): - action = result - elif isinstance(result, tuple): + action = result + if isinstance(result, tuple) and len(result) > 0: action = result[0] + if not isinstance(action, ActionStatus): current_app.logger.warning( f"Attempted to save a non ActionStatus: {action}" diff --git a/src/globus_action_provider_tools/flask/helpers.py b/src/globus_action_provider_tools/flask/helpers.py index 22756bd..c733560 100644 --- a/src/globus_action_provider_tools/flask/helpers.py +++ b/src/globus_action_provider_tools/flask/helpers.py @@ -133,12 +133,12 @@ def blueprint_error_handler(exc: Exception) -> ViewReturn: # ActionProviderToolsException is the base class for HTTP-based exceptions, # return those directly if isinstance(exc, ActionProviderToolsException): - return exc # type: ignore + return exc # If a component in the toolkit throw's an unhandled AuthenticationError, # replace it with a Flask-based response if isinstance(exc, AuthenticationError): - return UnauthorizedRequest() # type: ignore + return UnauthorizedRequest() current_app.logger.exception("Handling unexpected exception", exc_info=True) # Handle unexpected Exceptions in a somewhat predictable way diff --git a/src/globus_action_provider_tools/utils.py b/src/globus_action_provider_tools/utils.py index a5a6f23..637131a 100644 --- a/src/globus_action_provider_tools/utils.py +++ b/src/globus_action_provider_tools/utils.py @@ -1,4 +1,39 @@ +from __future__ import annotations + import datetime +import typing as t + +import cachetools + +T = t.TypeVar("T") + + +class TypedTTLCache(t.Generic[T]): + """ + A tiny wrapper class which provides a type-checked layer on top of TTLCache. + This allows us to know and enforce the types of cached objects. + """ + + def __init__(self, *, maxsize: int, ttl: int) -> None: + self._cache: cachetools.TTLCache = cachetools.TTLCache(maxsize=maxsize, ttl=ttl) + + def get(self, key: str) -> T | None: + return self._cache.get(key) + + def __setitem__(self, key: str, value: T) -> None: + self._cache[key] = value + + def __delitem__(self, key: str) -> None: + del self._cache[key] + + def __getitem__(self, key: str) -> T: + return self._cache[key] # type: ignore[no-any-return] + + def __contains__(self, key: str) -> bool: + return key in self._cache + + def clear(self) -> None: + self._cache.clear() def now_isoformat(): diff --git a/tox.ini b/tox.ini index a6530aa..96f7319 100644 --- a/tox.ini +++ b/tox.ini @@ -47,7 +47,7 @@ skip_install = true deps = -r requirements/mypy/requirements.txt commands = - mypy --ignore-missing-imports src/ tests/ + mypy src/ tests/ [testenv:docs] From 15c24a696fa685fad2a98e3f40c3d5ffa7e561f6 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 14 Oct 2024 11:34:51 -0500 Subject: [PATCH 22/24] Replace 'TokenChecker' with 'AuthStateBuilder' (#177) This change replaces 'TokenChecker' wherever it appears. This fixes some bad naming, but it also moves the confidential client construction *out of* the state builder, so that it is more visible and can examined as a location which needs attention in order for us to tune and control network behaviors. --- ...007_162811_sirosen_rename_tokenchecker.rst | 9 ++++ docs/source/toolkit/authentication.rst | 20 ++++---- examples/whattimeisitrightnow/app/app.py | 13 +++--- src/globus_action_provider_tools/__init__.py | 4 +- .../authentication.py | 10 ++-- .../flask/api_helpers.py | 27 ++++++----- .../flask/apt_blueprint.py | 21 +++++---- .../flask/helpers.py | 46 ++++++++++++------- tests/conftest.py | 29 ++++++------ tests/test_flask_helpers/conftest.py | 18 ++++++-- .../test_flask_auth_state_builder.py | 39 ++++++++++++++++ tests/test_flask_helpers/test_routes.py | 36 --------------- 12 files changed, 159 insertions(+), 113 deletions(-) create mode 100644 changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst create mode 100644 tests/test_flask_helpers/test_flask_auth_state_builder.py diff --git a/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst b/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst new file mode 100644 index 0000000..0b1a544 --- /dev/null +++ b/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst @@ -0,0 +1,9 @@ +Changes +------- + +- The ``TokenChecker`` class has been removed and replaced in all cases with an + ``AuthStateBuilder`` which better matches the purpose of this class. + +- The ``check_token`` flask-specific helper has been replaced with a + ``FlaskAuthStateBuilder`` which subclasses ``AuthStateBuilder`` and + specializes it to handle a ``flask.Request`` object. diff --git a/docs/source/toolkit/authentication.rst b/docs/source/toolkit/authentication.rst index 3fa207b..abcf846 100644 --- a/docs/source/toolkit/authentication.rst +++ b/docs/source/toolkit/authentication.rst @@ -1,28 +1,30 @@ Authentication ============== + The authentication helpers can be used in your action provider as follows: .. code-block:: python - from globus_action_provider_tools.authentication import TokenChecker + from globus_action_provider_tools.authentication import AuthStateBuilder # You will need to register a client and scope(s) in Globus Auth - # Then initialize a TokenChecker instance for your provider: - checker = TokenChecker( - client_id='YOUR_CLIENT_ID', - client_secret='YOUR_CLIENT_SECRET', + # Then initialize an AuthStateBuilder instance for your provider: + state_builder = AuthStateBuilder( + globus_sdk.ConfidentialAppAuthClient( + client_id='YOUR_CLIENT_ID', + client_secret='YOUR_CLIENT_SECRET', + ), expected_scopes=['https://auth.globus.org/scopes/YOUR_SCOPES_HERE'], ) -When a request comes in, use your TokenChecker to validate the access token from -the HTTP Authorization header. +When a request comes in, use your state_builder to construct an ``AuthState``, +thus validating the access token: .. code-block:: python access_token = request.headers['Authorization'].replace('Bearer ', '') - auth_state = checker.check_token(access_token) - + auth_state = state_builder.build(access_token) The AuthState has several properties and methods that will make it easier for you to decide whether or not to allow a request to proceed: diff --git a/examples/whattimeisitrightnow/app/app.py b/examples/whattimeisitrightnow/app/app.py index 8730ef6..83eaeb9 100644 --- a/examples/whattimeisitrightnow/app/app.py +++ b/examples/whattimeisitrightnow/app/app.py @@ -5,12 +5,13 @@ from random import randint from typing import Any, Dict, Tuple +import globus_sdk from flask import Flask, Response, jsonify, request from examples.whattimeisitrightnow.app import config from examples.whattimeisitrightnow.app import error as err from examples.whattimeisitrightnow.app.database import db -from globus_action_provider_tools.authentication import TokenChecker +from globus_action_provider_tools.authentication import AuthStateBuilder from globus_action_provider_tools.authorization import ( authorize_action_access_or_404, authorize_action_management_or_404, @@ -26,7 +27,10 @@ app = Flask(__name__) assign_json_provider(app) -token_checker = TokenChecker(config.client_id, config.client_secret, [config.our_scope]) +state_builder = AuthStateBuilder( + globus_sdk.ConfidentialAppAuthClient(config.client_id, config.client_secret), + [config.our_scope], +) COMPLETE_STATES = (ActionStatusValue.SUCCEEDED, ActionStatusValue.FAILED) INCOMPLETE_STATES = (ActionStatusValue.ACTIVE, ActionStatusValue.INACTIVE) @@ -51,16 +55,13 @@ def before_request() -> None: flask_validate_request ensures that we are receiving a valid request body from the user. - - token_checker.check_token ensure that the requester provided a valid, - Globus recognized token for interacting with the Provider. """ validation_result = flask_validate_request(request) if validation_result.errors: raise err.InvalidRequest(*validation_result.errors) token = request.headers.get("Authorization", "").replace("Bearer ", "") - auth_state = token_checker.check_token(token) + auth_state = state_builder.build(token) if not auth_state.identities: # Returning these authentication errors to the caller will make debugging # easier for this example. Consider whether this is appropriate diff --git a/src/globus_action_provider_tools/__init__.py b/src/globus_action_provider_tools/__init__.py index 080dfc7..50fbab1 100644 --- a/src/globus_action_provider_tools/__init__.py +++ b/src/globus_action_provider_tools/__init__.py @@ -1,4 +1,4 @@ -from globus_action_provider_tools.authentication import AuthState, TokenChecker +from globus_action_provider_tools.authentication import AuthState, AuthStateBuilder from globus_action_provider_tools.data_types import ( ActionProviderDescription, ActionProviderJsonEncoder, @@ -10,7 +10,7 @@ __all__ = [ "AuthState", - "TokenChecker", + "AuthStateBuilder", "ActionProviderDescription", "ActionProviderJsonEncoder", "ActionRequest", diff --git a/src/globus_action_provider_tools/authentication.py b/src/globus_action_provider_tools/authentication.py index ec3d48a..00ab490 100644 --- a/src/globus_action_provider_tools/authentication.py +++ b/src/globus_action_provider_tools/authentication.py @@ -401,14 +401,16 @@ def check_authorization( return bool(allowed_set & all_principals) -class TokenChecker: +class AuthStateBuilder: def __init__( - self, client_id: str, client_secret: str, expected_scopes: Iterable[str] + self, + auth_client: globus_sdk.ConfidentialAppAuthClient, + expected_scopes: Iterable[str], ) -> None: - self.auth_client = ConfidentialAppAuthClient(client_id, client_secret) + self.auth_client = auth_client self.default_expected_scopes = frozenset(expected_scopes) - def check_token( + def build( self, access_token: str, expected_scopes: Iterable[str] | None = None ) -> AuthState: if expected_scopes is None: diff --git a/src/globus_action_provider_tools/flask/api_helpers.py b/src/globus_action_provider_tools/flask/api_helpers.py index e35b930..e9b761e 100644 --- a/src/globus_action_provider_tools/flask/api_helpers.py +++ b/src/globus_action_provider_tools/flask/api_helpers.py @@ -3,10 +3,10 @@ import logging import flask +import globus_sdk from flask import jsonify, request from pydantic import ValidationError -from globus_action_provider_tools.authentication import TokenChecker from globus_action_provider_tools.data_types import ( ActionProviderDescription, ActionStatusValue, @@ -17,10 +17,10 @@ UnauthorizedRequest, ) from globus_action_provider_tools.flask.helpers import ( + FlaskAuthStateBuilder, action_status_return_to_view_return, assign_json_provider, blueprint_error_handler, - check_token, get_input_body_validator, parse_query_args, query_args_to_enum, @@ -178,10 +178,15 @@ def add_action_routes_to_blueprint( else: all_accepted_scopes = [provider_description.globus_auth_scope] - checker = TokenChecker( + # FIXME: + # it needs to be possible to parametrize this client to control its network callout + # behavior, tuning retries and timeouts + auth_client = globus_sdk.ConfidentialAppAuthClient( client_id=client_id, client_secret=client_secret, - expected_scopes=all_accepted_scopes, + ) + state_builder = FlaskAuthStateBuilder( + auth_client=auth_client, expected_scopes=all_accepted_scopes ) assign_json_provider(blueprint) @@ -200,7 +205,7 @@ def action_introspect() -> ViewReturn: # Check tokens if "public" is not in the *visible_to* list. if "public" not in provider_description.visible_to: - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() if not auth_state.check_authorization( provider_description.visible_to, allow_public=True, @@ -215,7 +220,7 @@ def action_introspect() -> ViewReturn: @blueprint.route("/actions", methods=["POST"]) @blueprint.route("/run", methods=["POST"]) def action_run() -> ViewReturn: - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() if not auth_state.check_authorization( provider_description.runnable_by, allow_all_authenticated_users=True ): @@ -248,7 +253,7 @@ def action_run() -> ViewReturn: @blueprint.route("//status", methods=["GET"]) @blueprint.route("/actions/", methods=["GET"]) def action_status(action_id: str) -> ViewReturn: - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() try: status = action_status_callback(action_id, auth_state) # type: ignore except ValidationError as ve: @@ -262,7 +267,7 @@ def action_status(action_id: str) -> ViewReturn: @blueprint.route("//cancel", methods=["POST"]) @blueprint.route("/actions//cancel", methods=["POST"]) def action_cancel(action_id: str) -> ViewReturn: - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() try: status = action_cancel_callback(action_id, auth_state) # type: ignore except ValidationError as ve: @@ -276,7 +281,7 @@ def action_cancel(action_id: str) -> ViewReturn: @blueprint.route("//release", methods=["POST"]) @blueprint.route("/actions/", methods=["DELETE"]) def action_release(action_id: str) -> ViewReturn: - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() try: status = action_release_callback(action_id, auth_state) # type: ignore except ValidationError as ve: @@ -292,14 +297,14 @@ def action_release(action_id: str) -> ViewReturn: @blueprint.route("/actions//log", methods=["GET"]) @blueprint.route("//log", methods=["GET"]) def action_log(action_id: str) -> ViewReturn: - check_token(request, checker) + state_builder.build_from_request() return jsonify({"log": "message"}), 200 if action_enumeration_callback is not None: @blueprint.route("/actions", methods=["GET"]) def action_enumeration(): - auth_state = check_token(request, checker) + auth_state = state_builder.build_from_request() valid_statuses = {e.name.casefold() for e in ActionStatusValue} statuses = parse_query_args( diff --git a/src/globus_action_provider_tools/flask/apt_blueprint.py b/src/globus_action_provider_tools/flask/apt_blueprint.py index c778b61..99f3d34 100644 --- a/src/globus_action_provider_tools/flask/apt_blueprint.py +++ b/src/globus_action_provider_tools/flask/apt_blueprint.py @@ -1,11 +1,11 @@ import typing as t import flask +import globus_sdk from flask import Blueprint, blueprints, current_app, g, jsonify, request from pydantic import ValidationError from werkzeug.exceptions import BadRequest as WerkzeugBadRequest -from globus_action_provider_tools.authentication import TokenChecker from globus_action_provider_tools.authorization import ( authorize_action_access_or_404, authorize_action_management_or_404, @@ -26,10 +26,10 @@ UnauthorizedRequest, ) from globus_action_provider_tools.flask.helpers import ( + FlaskAuthStateBuilder, action_status_return_to_view_return, assign_json_provider, blueprint_error_handler, - check_token, get_input_body_validator, parse_query_args, query_args_to_enum, @@ -91,7 +91,7 @@ def __init__( assign_json_provider(self) self.before_request(self._check_token) self.register_error_handler(Exception, blueprint_error_handler) - self.record_once(self._create_token_checker) + self.record_once(self._create_state_builder) if request_lifecycle_hooks: for hooks in request_lifecycle_hooks: @@ -140,7 +140,7 @@ def __init__( methods=["POST"], ) - def _create_token_checker(self, setup_state: blueprints.BlueprintSetupState): + def _create_state_builder(self, setup_state: blueprints.BlueprintSetupState): app = setup_state.app provider_prefix = self.name.upper() + "_" client_id = app.config.get(provider_prefix + "CLIENT_ID") @@ -154,14 +154,15 @@ def _create_token_checker(self, setup_state: blueprints.BlueprintSetupState): scopes.extend(self.additional_scopes) app.logger.info( - f"Initializing TokenChecker for client {client_id} and secret " + f"Initializing AuthStateBuilder for client {client_id} and secret " f"***{client_secret[-5:]}" ) - self.checker = TokenChecker( - client_id=client_id, - client_secret=client_secret, - expected_scopes=scopes, + # FIXME: it needs to be possible to parametrize this client to control its network + # callout behavior, tuning retries and timeouts + auth_client = globus_sdk.ConfidentialAppAuthClient( + client_id=client_id, client_secret=client_secret ) + self.state_builder = FlaskAuthStateBuilder(auth_client, expected_scopes=scopes) def _action_introspect(self): """ @@ -541,7 +542,7 @@ def _check_token(self) -> None: ): return - g.auth_state = check_token(request, self.checker) + g.auth_state = self.state_builder.build_from_request() if g.auth_state.effective_identity is None: current_app.logger.info( f"Request failed authentication due to: {g.auth_state.errors}" diff --git a/src/globus_action_provider_tools/flask/helpers.py b/src/globus_action_provider_tools/flask/helpers.py index c733560..2ed0a22 100644 --- a/src/globus_action_provider_tools/flask/helpers.py +++ b/src/globus_action_provider_tools/flask/helpers.py @@ -12,7 +12,7 @@ from flask import Request, current_app, jsonify from pydantic import BaseModel, ValidationError -from globus_action_provider_tools.authentication import AuthState, TokenChecker +from globus_action_provider_tools.authentication import AuthState, AuthStateBuilder from globus_action_provider_tools.data_types import ( ActionProviderDescription, ActionProviderJsonEncoder, @@ -48,6 +48,35 @@ ActionInputValidatorType = Callable[[Dict[str, Any]], None] +class FlaskAuthStateBuilder(AuthStateBuilder): + """ + A customized AuthStateBuilder which can handle a flask.Request object + as its input. + """ + + def build_from_request(self, *, request: Request | None = None) -> AuthState: + """ + Build the ``AuthState`` from the ``Authorization`` header provided. + + :param request: The flask request object to process. Defaults to the request + object found in the current app context. + """ + if request is None: + request = flask.request + access_token = request.headers.get("Authorization") + if access_token is None: + raise UnverifiedAuthenticationError("No Authorization header received") + if not access_token.startswith("Bearer "): + raise UnverifiedAuthenticationError( + "No Bearer token in Authorization header" + ) + access_token = access_token[len("Bearer ") :].strip() + if not 10 <= len(access_token) <= 2048: + raise UnverifiedAuthenticationError("Bearer token length is unexpected") + + return super().build(access_token) + + def parse_query_args( request: Request, *, @@ -114,21 +143,6 @@ def action_status_return_to_view_return( return jsonify(status), status_code -def check_token(request: Request, checker: TokenChecker) -> AuthState: - """Extract and validate a bearer token and return an AuthState instance.""" - - access_token = request.headers.get("Authorization") - if access_token is None: - raise UnverifiedAuthenticationError("No Authorization header received") - if not access_token.startswith("Bearer "): - raise UnverifiedAuthenticationError("No Bearer token in Authorization header") - access_token = access_token[len("Bearer ") :].strip() - if not 10 <= len(access_token) <= 2048: - raise UnverifiedAuthenticationError("Bearer token length is unexpected") - auth_state = checker.check_token(access_token) - return auth_state - - def blueprint_error_handler(exc: Exception) -> ViewReturn: # ActionProviderToolsException is the base class for HTTP-based exceptions, # return those directly diff --git a/tests/conftest.py b/tests/conftest.py index 86d3efc..b3af3eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ import yaml from globus_sdk._testing import RegisteredResponse, register_response_set -from globus_action_provider_tools.authentication import AuthState, TokenChecker +from globus_action_provider_tools.authentication import AuthState, AuthStateBuilder from .data import canned_responses @@ -27,10 +27,12 @@ def config(): @pytest.fixture -@mock.patch("globus_action_provider_tools.authentication.ConfidentialAppAuthClient") +@mock.patch("globus_sdk.ConfidentialAppAuthClient") def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: + # FIXME: the comment below is a lie, assess and figure out what is being said + # # Mock the introspection first because that gets called as soon as we create - # a TokenChecker + # an AuthStateBuilder client = MockAuthClient.return_value client.oauth2_token_introspect.return_value = ( canned_responses.introspect_response()() @@ -45,13 +47,9 @@ def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: globus_sdk.GroupsClient, "get_my_groups", canned_responses.groups_response() ) - # Create a TokenChecker to be used to create a mocked auth_state object - checker = TokenChecker( - client_id=config["client_id"], - client_secret=config["client_secret"], - expected_scopes=config["expected_scopes"], - ) - auth_state = checker.check_token("NOT_A_TOKEN") + # Create an AuthStateBuilder to be used to create a mocked auth_state object + builder = AuthStateBuilder(client, expected_scopes=config["expected_scopes"]) + auth_state = builder.build("NOT_A_TOKEN") # Reset the call count because check_token implicitly calls oauth2_token_introspect client.oauth2_token_introspect.call_count = 0 @@ -66,18 +64,19 @@ def auth_state(MockAuthClient, config, monkeypatch) -> AuthState: def apt_blueprint_noauth(auth_state): """ A fixture function which will mock an ActionProviderBlueprint instance's - TokenChecker. + AuthStateBuilder. """ def _apt_blueprint_noauth(aptb): - # Manually remove the function that creates the internal token_checker + # Manually remove the function that creates the internal state_builder for f in aptb.deferred_functions: - if f.__name__ == "_create_token_checker": + if f.__name__ == "_create_state_builder": aptb.deferred_functions.remove(f) # Use a mocked auth state builder internally - aptb.checker = mock.Mock() - aptb.checker.check_token.return_value = auth_state + aptb.state_builder = mock.Mock() + aptb.state_builder.build.return_value = auth_state + aptb.state_builder.build_from_request.return_value = auth_state return _apt_blueprint_noauth diff --git a/tests/test_flask_helpers/conftest.py b/tests/test_flask_helpers/conftest.py index dce4161..cb0c065 100644 --- a/tests/test_flask_helpers/conftest.py +++ b/tests/test_flask_helpers/conftest.py @@ -4,6 +4,8 @@ the only difference being in the helper that is used to create the app. """ +from unittest import mock + import pytest from flask import Blueprint, Flask @@ -25,7 +27,7 @@ @pytest.fixture -def aptb_app(create_app_from_blueprint): +def aptb_app(auth_state, create_app_from_blueprint): """ This fixture creates a Flask app using the ActionProviderBlueprint helper. The function form of the decorators are used to register each @@ -37,7 +39,11 @@ def aptb_app(create_app_from_blueprint): url_prefix="/aptb", provider_description=ap_description, ) - return create_app_from_blueprint(blueprint) + with mock.patch( + "globus_action_provider_tools.authentication.AuthStateBuilder.build", + return_value=auth_state, + ): + yield create_app_from_blueprint(blueprint) @pytest.fixture @@ -60,7 +66,7 @@ def _create_app_from_blueprint(blueprint: ActionProviderBlueprint) -> Flask: @pytest.fixture() -def add_routes_app(flask_helpers_noauth, auth_state): +def add_routes_app(auth_state): """ This fixture creates a Flask app with routes loaded via the add_action_routes_to_blueprint Flask helper. @@ -84,4 +90,8 @@ def add_routes_app(flask_helpers_noauth, auth_state): ], ) app.register_blueprint(bp) - return app + with mock.patch( + "globus_action_provider_tools.authentication.AuthStateBuilder.build", + return_value=auth_state, + ): + yield app diff --git a/tests/test_flask_helpers/test_flask_auth_state_builder.py b/tests/test_flask_helpers/test_flask_auth_state_builder.py new file mode 100644 index 0000000..28a59a2 --- /dev/null +++ b/tests/test_flask_helpers/test_flask_auth_state_builder.py @@ -0,0 +1,39 @@ +from unittest import mock + +import pytest + +from globus_action_provider_tools.errors import UnverifiedAuthenticationError +from globus_action_provider_tools.flask.helpers import FlaskAuthStateBuilder + + +@pytest.mark.parametrize( + "authorization_header", + ( + pytest.param(None, id="missing Authorization header"), + pytest.param("", id="blank header value"), + pytest.param(" ", id="whitespace header value"), + pytest.param("A" * 100, id="no 'Bearer ' prefix"), + pytest.param("Bearer " + "A" * 9, id="short token"), + pytest.param("Bearer " + "A" * 2049, id="long token"), + ), +) +def test_bogus_authorization_headers_are_rejected_without_io(authorization_header): + # stub the Auth Client and scopes + builder = FlaskAuthStateBuilder(mock.Mock(), []) + + headers = {} + if authorization_header is not None: + headers["Authorization"] = authorization_header + + mock_request_object = mock.Mock() + mock_request_object.headers = headers + + # establish a mock which will raise an error if the code even *attempts* + # to create an AuthState object + # we should short-circuit before ever getting this far + with mock.patch( + "globus_action_provider_tools.authentication.AuthState", + side_effect=RuntimeError("ON NO"), + ): + with pytest.raises(UnverifiedAuthenticationError): + builder.build_from_request(request=mock_request_object) diff --git a/tests/test_flask_helpers/test_routes.py b/tests/test_flask_helpers/test_routes.py index f582ef5..766e4e9 100644 --- a/tests/test_flask_helpers/test_routes.py +++ b/tests/test_flask_helpers/test_routes.py @@ -65,39 +65,3 @@ def test_introspect_cors_requests(request, app_fixture): ] assert list(introspection_cors_response.access_control_allow_origin) == ["*"] assert list(introspection_cors_response.access_control_expose_headers) == ["*"] - - -@pytest.mark.parametrize("app_fixture", ["aptb_app", "add_routes_app"]) -@pytest.mark.parametrize("api_version", ["1.0", "1.1"]) -@pytest.mark.parametrize( - "authorization_header", - ( - pytest.param(None, id="missing Authorization header"), - pytest.param("", id="blank header value"), - pytest.param(" ", id="whitespace header value"), - pytest.param("A" * 100, id="no 'Bearer ' prefix"), - pytest.param("Bearer " + "A" * 9, id="short token"), - pytest.param("Bearer " + "A" * 2049, id="long token"), - ), -) -def test_bogus_authorization_headers_are_rejected_without_io( - request, app_fixture, api_version, authorization_header -): - app: flask.Flask = request.getfixturevalue(app_fixture) - _, bp = list(app.blueprints.items())[0] - bp.input_schema = ActionProviderPydanticInputSchema - - client = ActionProviderClient( - app.test_client(), bp.url_prefix, api_version=api_version - ) - - headers = [] - if authorization_header is not None: - headers.append(("Authorization", authorization_header)) - - client.enumerate(assert_status=401, headers=headers) - client.run(assert_status=401, headers=headers) - client.status("any", assert_status=401, headers=headers) - client.log("any", assert_status=401, headers=headers) - client.cancel("any", assert_status=401, headers=headers) - client.release("any", assert_status=401, headers=headers) From be3afd31d444e84ebc3d1383c25688e3adffa8ef Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Wed, 16 Oct 2024 16:24:20 -0500 Subject: [PATCH 23/24] Replace workflow doc releasing sections with releasing doc (#141) * Replace releasing segments of workflow doc Declare the steps which need to be performed, without extraneous documentation about the hows and wherefores. Discard misleading and overly verbose internal documentation. This change only touches the sections of the workflow doc which touch on release process, and only for normal releases. * Refine the new releasing doc * Define merge-back as a PR command --- RELEASING.md | 42 +++++++++++++++++++++++ WORKFLOW.md | 96 ---------------------------------------------------- 2 files changed, 42 insertions(+), 96 deletions(-) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..7ea3ff0 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,42 @@ +# Releasing + +- Determine a new version number, `VERSION=...` + +- Make sure your repo is on `main` and up to date; + `git checkout main; git pull` + +- Checkout a release branch, `git checkout -b release/$VERSION` + +- Update the version number with `poetry version $VERSION` + +- Update the changelog, `scriv collect --edit` + +- Add, commit, and push the release branch + +``` +git add pyproject.toml CHANGELOG.rst changelog.d/ +git commit -m "Bump version for release v$VERSION" +git push -u origin release/$VERSION +``` +_Note: this assumes `origin` is your desired upstream._ + +- Create a PR against the `production` branch; + `gh pr create -B production -t "Release v$VERSION"` + +- After any changes and approval, merge the PR, checkout `production`, and pull; + `git checkout production; git pull` + +- Create a release tag and push; + `git tag -s "v$(poetry version -s)" -m "v$(poetry version -s)"` + `git push --tags` + +- Create a GitHub release, which will auto-publish to pypi + `gh release create "v$(poetry version -s)" --title "v$(poetry version -s)"` + +- Merge `production` back to `main` by opening and merging a PR: + + ``` + gh pr create -B main -H production -t "Merge back production->main ($(date +"%Y-%m-%d"))" -b '' -l no-news-is-good-news + ``` + +- Delete the release branch; `git branch -d release/$VERSION` diff --git a/WORKFLOW.md b/WORKFLOW.md index 87b814c..7720d28 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -13,10 +13,7 @@ Some commands may not be available until the virtual environment is created and * [Version numbering](#version-numbering) * [Priority git branches](#priority-git-branches) * [Everyday development](#everyday-development) -* [Preparing a feature release](#preparing-a-feature-release) * [Preparing a hotfix release](#preparing-a-hotfix-release) -* [Merging release branches](#merging-release-branches) -* [Publishing the new version](#publishing-the-new-version) ## Version numbering @@ -76,28 +73,6 @@ git checkout -b "$BRANCH_NAME" Feature branches are merged back to `main`, and only to `main`. -## Preparing a feature release - -When the code or documentation is ready for release, a new feature release will be created. -Feature releases begin by creating a new branch off of `main` -(or, alternatively, by branching off an agreed-upon merge commit in `main`). - -```shell -read -p "Enter the feature release version: " NEW_VERSION -BRANCH_NAME="release/$NEW_VERSION" - -# If deploying from main: -git checkout main -git pull origin -git checkout -b "$BRANCH_NAME" - -# Alternatively, if deploying from an agreed-upon merge commit: -git checkout -b "$BRANCH_NAME" -``` - -Next, proceed to the [Merging release branches](#merging-release-branches) section. - - ## Preparing a hotfix release If a bug is found in production and must be fixed immediately, this requires a hotfix release. @@ -118,74 +93,3 @@ After creating the hotfix branch, fix that bug, create a changelog fragment and commit the changes in the hotfix branch! Next, proceed to the [Merging release branches](#merging-release-branches) section. - - -## Merging release branches - -**NOTE**: -The steps in this document must be performed in a release or hotfix branch. -See the -[Preparing a feature release](#preparing-a-feature-release) -or -[Preparing a hotfix release](#preparing-a-hotfix-release) -section for steps to create a release or hotfix branch. - -After creating a release or hotfix branch, -you must follow these steps to merge the branch to `production` and `main`: - -1. On the branch that is to be released, prepare the code and documentation for release. - 1. Bump the version. - - If the release is a hotfix, use ``poetry version patch`` - - If the release is a backwards-compatible change use ``poetry version patch`` - - If the release is non-backwards compatible, use ``poetry version minor`` - 2. Bump copyright years as appropriate. - 3. Collect changelog fragments as appropriate. - 4. Run unit/integration/CI/doc tests as appropriate. - 5. Commit all changes to git. - -2. Push the branch to GitHub. - -3. Create a new pull request to merge to `production`. - 1. Select `production` as the "base" merge branch. - 2. Select the release or hotfix branch as the "compare" merge branch. - 3. Wait for CI test results (and approvals, when possible). - - It is the release engineer's discretion to ask for and require PR approvals. - A release branch will usually contain code that has already been reviewed, unless it is a hotfix. - If the release is a hotfix, it is recommended to get approvals. - - > WARNING: **Merge conflicts** - > - > Merge conflicts halt the release process when merging to `production` - > unless it is a trivial conflict (like the "version" in `pyproject.toml`). - - 4. Merge the branch to `production`. Do not delete the branch! - -4. Create a new tag and a new release. - - 1. Click on the "Releases" section. Then click "Draft a new release". - 2. Click the "Choose a tag" dropdown, type the new version, and press Enter. - 3. Select `production` as the target branch. - 4. Type the new version as the release title. - 5. Paste the changelog as the release description. - 6. Click "Publish release" to publish the new tag and release on GitHub. - -5. Create a new pull request to merge to `main`. - - 1. Select `main` as the "base" merge branch. - 2. Select the release or hotfix branch as the "compare" merge branch. - 3. Wait for CI test results (and approvals, if needed). - - > NOTE: **Merge conflicts** - > - > A merge conflict at this stage does NOT halt the release process. - > However, approval is required after resolving the conflict. - - 4. Merge the branch to `main`. - -6. Delete the release branch. - - -## Publishing the new version - -Code updates are automatically published to PyPI when a new release is created on GitHub. From a85c07860f01dc32a1de4a995f14c83f52960967 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 18 Oct 2024 12:05:29 -0500 Subject: [PATCH 24/24] Release v0.19.0 --- CHANGELOG.rst | 63 +++++++++++++++++++ .../20241003_114654_sirosen_infra_cleanup.rst | 4 -- ...007_162811_sirosen_rename_tokenchecker.rst | 9 --- ...25419_sirosen_remove_expected_audience.rst | 14 ----- ...8_141637_sirosen_remove_testing_module.rst | 5 -- ...08_171503_sirosen_fix_introspect_check.rst | 19 ------ ...1011_131741_sirosen_fix_get_authorizer.rst | 7 --- pyproject.toml | 2 +- 8 files changed, 64 insertions(+), 59 deletions(-) delete mode 100644 changelog.d/20241003_114654_sirosen_infra_cleanup.rst delete mode 100644 changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst delete mode 100644 changelog.d/20241008_125419_sirosen_remove_expected_audience.rst delete mode 100644 changelog.d/20241008_141637_sirosen_remove_testing_module.rst delete mode 100644 changelog.d/20241008_171503_sirosen_fix_introspect_check.rst delete mode 100644 changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index facb447..f318239 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,69 @@ Unreleased changes are documented in files in the `changelog.d`_ directory. .. scriv-insert-here +.. _changelog-0.19.0: + +0.19.0 — 2024-10-18 +=================== + +Features +-------- + +- The token introspect checking and caching performed in ``AuthState`` has + been improved. + + - The cache is keyed off of token hashes, rather than raw token strings. + + - The ``exp`` and ``nbf`` values are no longer verified, removing the + possibility of incorrect treatment of valid tokens as invalid due to clock + drift. + + - Introspect response caching caches the raw response even for invalid + tokens, meaning that Action Providers will no longer repeatedly introspect + a token once it is known to be invalid. + + - Scope validation raises a new, dedicated error class, + ``globus_action_provider_tools.authentication.InvalidTokenScopesError``, on + failure. + +Changes +------- + +- The ``TokenChecker`` class has been removed and replaced in all cases with an + ``AuthStateBuilder`` which better matches the purpose of this class. + +- The ``check_token`` flask-specific helper has been replaced with a + ``FlaskAuthStateBuilder`` which subclasses ``AuthStateBuilder`` and + specializes it to handle a ``flask.Request`` object. + +- The ``aud`` field of token introspect responses is no longer validated and + fields associated with it have been removed. This includes changes to + function and class initializer signatures. + + - The ``expected_audience`` field is no longer supported in ``AuthState`` and + ``TokenChecker``. It has been removed from the initializers for these + classes. + + - ``globus_auth_client_name`` has been removed from ``ActionProviderBlueprint``. + + - ``client_name`` has been removed from ``add_action_routes_to_blueprint``. + +Development +----------- + +- Move to `src/` tree layout + +- Refactor ``AuthState.get_authorizer_for_scope`` without changing its + primary outward semantics. The ``bypass_dependent_token_cache`` argument + has been removed from its interface, as it is not necessary to expose + with the improved implementation. + +Removed +------- + +- ``globus_action_provider_tools.testing`` has been removed. Users who were + relying on these components should make use of their own fixtures and mocks. + .. _changelog-0.18.0: 0.18.0 — 2024-06-14 diff --git a/changelog.d/20241003_114654_sirosen_infra_cleanup.rst b/changelog.d/20241003_114654_sirosen_infra_cleanup.rst deleted file mode 100644 index 826027a..0000000 --- a/changelog.d/20241003_114654_sirosen_infra_cleanup.rst +++ /dev/null @@ -1,4 +0,0 @@ -Development ------------ - -- Move to `src/` tree layout diff --git a/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst b/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst deleted file mode 100644 index 0b1a544..0000000 --- a/changelog.d/20241007_162811_sirosen_rename_tokenchecker.rst +++ /dev/null @@ -1,9 +0,0 @@ -Changes -------- - -- The ``TokenChecker`` class has been removed and replaced in all cases with an - ``AuthStateBuilder`` which better matches the purpose of this class. - -- The ``check_token`` flask-specific helper has been replaced with a - ``FlaskAuthStateBuilder`` which subclasses ``AuthStateBuilder`` and - specializes it to handle a ``flask.Request`` object. diff --git a/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst b/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst deleted file mode 100644 index b138abe..0000000 --- a/changelog.d/20241008_125419_sirosen_remove_expected_audience.rst +++ /dev/null @@ -1,14 +0,0 @@ -Changes -------- - -- The ``aud`` field of token introspect responses is no longer validated and - fields associated with it have been removed. This includes changes to - function and class initializer signatures. - - - The ``expected_audience`` field is no longer supported in ``AuthState`` and - ``TokenChecker``. It has been removed from the initializers for these - classes. - - - ``globus_auth_client_name`` has been removed from ``ActionProviderBlueprint``. - - - ``client_name`` has been removed from ``add_action_routes_to_blueprint``. diff --git a/changelog.d/20241008_141637_sirosen_remove_testing_module.rst b/changelog.d/20241008_141637_sirosen_remove_testing_module.rst deleted file mode 100644 index 863056a..0000000 --- a/changelog.d/20241008_141637_sirosen_remove_testing_module.rst +++ /dev/null @@ -1,5 +0,0 @@ -Removed -------- - -- ``globus_action_provider_tools.testing`` has been removed. Users who were - relying on these components should make use of their own fixtures and mocks. diff --git a/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst b/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst deleted file mode 100644 index 3a662a5..0000000 --- a/changelog.d/20241008_171503_sirosen_fix_introspect_check.rst +++ /dev/null @@ -1,19 +0,0 @@ -Features --------- - -- The token introspect checking and caching performed in ``AuthState`` has - been improved. - - - The cache is keyed off of token hashes, rather than raw token strings. - - - The ``exp`` and ``nbf`` values are no longer verified, removing the - possibility of incorrect treatment of valid tokens as invalid due to clock - drift. - - - Introspect response caching caches the raw response even for invalid - tokens, meaning that Action Providers will no longer repeatedly introspect - a token once it is known to be invalid. - - - Scope validation raises a new, dedicated error class, - ``globus_action_provider_tools.authentication.InvalidTokenScopesError``, on - failure. diff --git a/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst b/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst deleted file mode 100644 index 1e56436..0000000 --- a/changelog.d/20241011_131741_sirosen_fix_get_authorizer.rst +++ /dev/null @@ -1,7 +0,0 @@ -Development ------------ - -- Refactor ``AuthState.get_authorizer_for_scope`` without changing its - primary outward semantics. The ``bypass_dependent_token_cache`` argument - has been removed from its interface, as it is not necessary to expose - with the improved implementation. diff --git a/pyproject.toml b/pyproject.toml index 856a3de..47af991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "globus-action-provider-tools" -version = "0.18.0" +version = "0.19.0" description = "Tools to help developers build services that implement the Action Provider specification." authors = [ "Globus Team ",