Browse Source

feat: Merge pull request #556 from marcospereirampj/release/4.0.0

Release/4.0.0
pull/558/head v4.0.0
Richard Nemeth 8 months ago
committed by GitHub
parent
commit
6ec4998cff
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .readthedocs.yaml
  2. 17
      README.md
  3. 6
      docs/source/modules/openid_client.rst
  4. 355
      poetry.lock
  5. 30
      pyproject.toml
  6. 475
      src/keycloak/keycloak_admin.py
  7. 37
      src/keycloak/keycloak_openid.py
  8. 78
      tests/conftest.py
  9. 142
      tests/test_keycloak_admin.py
  10. 62
      tests/test_keycloak_openid.py
  11. 2
      tox.ini

2
.readthedocs.yaml

@ -8,4 +8,4 @@ build:
post_create_environment: post_create_environment:
- python -m pip install poetry - python -m pip install poetry
post_install: post_install:
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install -E docs
- VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install

17
README.md

@ -20,9 +20,24 @@ https://github.com/marcospereirampj/python-keycloak/issues
The documentation for python-keycloak is available on [readthedocs](http://python-keycloak.readthedocs.io). The documentation for python-keycloak is available on [readthedocs](http://python-keycloak.readthedocs.io).
## Example of Using Keycloak OpenID
## Keycloak version support
The library strives to always support Keycloak's latest version. Additionally to that, we also support 5 latest major versions of Keycloak,
in order to give our user base more time for smoother upgrades.
Current list of supported Keycloak versions:
- 24.X
- 23.X
- 22.X
- 21.X
- 20.X
## Python version support
We only support Python versions which have active or security support by the Python Software Foundation. You find the list of active python versions [here](https://endoflife.date/python).
## Example of Using Keycloak OpenID
```python ```python
from keycloak import KeycloakOpenID from keycloak import KeycloakOpenID

6
docs/source/modules/openid_client.rst

@ -116,9 +116,9 @@ Decode token
.. code-block:: python .. code-block:: python
KEYCLOAK_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + keycloak_openid.public_key() + "\n-----END PUBLIC KEY-----"
options = {"verify_signature": True, "verify_aud": True, "verify_exp": True}
token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options)
token_info = keycloak_openid.decode_token(token['access_token'])
# Without validation
token_info = keycloak_openid.decode_token(token['access_token'], validate=False)
Get UMA-permissions by token Get UMA-permissions by token

355
poetry.lock

@ -4,7 +4,7 @@
name = "alabaster" name = "alabaster"
version = "0.7.13" version = "0.7.13"
description = "A configurable sidebar-enabled Sphinx theme" description = "A configurable sidebar-enabled Sphinx theme"
optional = true
optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
@ -15,7 +15,7 @@ files = [
name = "anyascii" name = "anyascii"
version = "0.3.2" version = "0.3.2"
description = "Unicode to ASCII transliteration" description = "Unicode to ASCII transliteration"
optional = true
optional = false
python-versions = ">=3.3" python-versions = ">=3.3"
files = [ files = [
{file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"}, {file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"},
@ -24,13 +24,13 @@ files = [
[[package]] [[package]]
name = "argcomplete" name = "argcomplete"
version = "3.2.3"
version = "3.3.0"
description = "Bash tab completion for argparse" description = "Bash tab completion for argparse"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "argcomplete-3.2.3-py3-none-any.whl", hash = "sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c"},
{file = "argcomplete-3.2.3.tar.gz", hash = "sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23"},
{file = "argcomplete-3.3.0-py3-none-any.whl", hash = "sha256:c168c3723482c031df3c207d4ba8fa702717ccb9fc0bfe4117166c1f537b4a54"},
{file = "argcomplete-3.3.0.tar.gz", hash = "sha256:fd03ff4a5b9e6580569d34b273f741e85cd9e072f3feeeee3eba4891c70eda62"},
] ]
[package.extras] [package.extras]
@ -40,7 +40,7 @@ test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
name = "astroid" name = "astroid"
version = "3.1.0" version = "3.1.0"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
optional = true
optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"},
@ -54,7 +54,7 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
name = "babel" name = "babel"
version = "2.14.0" version = "2.14.0"
description = "Internationalization utilities" description = "Internationalization utilities"
optional = true
optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"},
@ -69,48 +69,48 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]] [[package]]
name = "backports-tarfile" name = "backports-tarfile"
version = "1.0.0"
version = "1.1.1"
description = "Backport of CPython tarfile module" description = "Backport of CPython tarfile module"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "backports.tarfile-1.0.0-py3-none-any.whl", hash = "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4"},
{file = "backports.tarfile-1.0.0.tar.gz", hash = "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75"},
{file = "backports.tarfile-1.1.1-py3-none-any.whl", hash = "sha256:73e0179647803d3726d82e76089d01d8549ceca9bace469953fcb4d97cf2d417"},
{file = "backports_tarfile-1.1.1.tar.gz", hash = "sha256:9c2ef9696cb73374f7164e17fc761389393ca76777036f5aad42e8b93fcd8009"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
testing = ["jaraco.test", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
[[package]] [[package]]
name = "black" name = "black"
version = "24.3.0"
version = "24.4.2"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
] ]
[package.dependencies] [package.dependencies]
@ -379,17 +379,17 @@ files = [
[[package]] [[package]]
name = "commitizen" name = "commitizen"
version = "3.21.3"
version = "3.24.0"
description = "Python commitizen client tool" description = "Python commitizen client tool"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "commitizen-3.21.3-py3-none-any.whl", hash = "sha256:1c23a9b0e02fadfb1f586649ed7f57745679af24be2f8158d2e746472f115246"},
{file = "commitizen-3.21.3.tar.gz", hash = "sha256:dc23147e77376cced87f2aedd3693afa05832da88bf9e08c0baaa9d242d9549f"},
{file = "commitizen-3.24.0-py3-none-any.whl", hash = "sha256:d9e28b1dcd97cea64dcb50be25292ceb730470d933f1da37131f9540f762df36"},
{file = "commitizen-3.24.0.tar.gz", hash = "sha256:088e01ae8265f1d6fa5a4d11a05e4fd7092d958c881837c35f6c65aad27331a9"},
] ]
[package.dependencies] [package.dependencies]
argcomplete = ">=1.12.1,<3.3"
argcomplete = ">=1.12.1,<3.4"
charset-normalizer = ">=2.1.0,<4" charset-normalizer = ">=2.1.0,<4"
colorama = ">=0.4.1,<0.5.0" colorama = ">=0.4.1,<0.5.0"
decli = ">=0.6.0,<0.7.0" decli = ">=0.6.0,<0.7.0"
@ -405,7 +405,7 @@ tomlkit = ">=0.5.3,<1.0.0"
name = "commonmark" name = "commonmark"
version = "0.9.1" version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec" description = "Python parser for the CommonMark Markdown spec"
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
@ -417,63 +417,63 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.4.4"
version = "7.5.0"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"},
{file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"},
{file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"},
{file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"},
{file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"},
{file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"},
{file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"},
{file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"},
{file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"},
{file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"},
{file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"},
{file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"},
{file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"},
{file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"},
{file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"},
{file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"},
{file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"},
{file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"},
{file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"},
{file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"},
{file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"},
{file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"},
{file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"},
{file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"},
{file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"},
{file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"},
{file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"},
{file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"},
{file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"},
{file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"},
{file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"},
{file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"},
{file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"},
{file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"},
{file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"},
{file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"},
{file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"},
{file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"},
{file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"},
{file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"},
{file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"},
{file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"},
{file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"},
{file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"},
{file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"},
{file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"},
{file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"},
{file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"},
{file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"},
{file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"},
{file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"},
{file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"},
{file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"},
{file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"},
{file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"},
{file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"},
{file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"},
{file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"},
{file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"},
{file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"},
{file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"},
{file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"},
{file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"},
{file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"},
{file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"},
{file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"},
{file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"},
{file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"},
{file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"},
{file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"},
{file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"},
{file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"},
{file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"},
{file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"},
{file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"},
{file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"},
{file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"},
{file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"},
{file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"},
{file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"},
{file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"},
{file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"},
{file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"},
{file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"},
] ]
[package.dependencies] [package.dependencies]
@ -588,21 +588,18 @@ name = "docutils"
version = "0.18.1" version = "0.18.1"
description = "Docutils -- Python Documentation Utilities" description = "Docutils -- Python Documentation Utilities"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"},
{file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
]
python-versions = "*"
files = []
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.0"
version = "1.2.1"
description = "Backport of PEP 654 (exception groups)" description = "Backport of PEP 654 (exception groups)"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
{file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
] ]
[package.extras] [package.extras]
@ -610,13 +607,13 @@ test = ["pytest (>=6)"]
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.13.3"
version = "3.13.4"
description = "A platform independent file lock." description = "A platform independent file lock."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
{file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
{file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"},
{file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"},
] ]
[package.extras] [package.extras]
@ -657,13 +654,13 @@ pydocstyle = ">=2.1"
[[package]] [[package]]
name = "freezegun" name = "freezegun"
version = "1.4.0"
version = "1.5.0"
description = "Let your Python tests travel through time" description = "Let your Python tests travel through time"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"},
{file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"},
{file = "freezegun-1.5.0-py3-none-any.whl", hash = "sha256:ec3f4ba030e34eb6cf7e1e257308aee2c60c3d038ff35996d7475760c9ff3719"},
{file = "freezegun-1.5.0.tar.gz", hash = "sha256:200a64359b363aa3653d8aac289584078386c7c3da77339d257e46a01fb5c77c"},
] ]
[package.dependencies] [package.dependencies]
@ -671,13 +668,13 @@ python-dateutil = ">=2.7"
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.35"
version = "2.5.36"
description = "File identification library for Python" description = "File identification library for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
{file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
{file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
{file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
] ]
[package.extras] [package.extras]
@ -685,20 +682,20 @@ license = ["ukkonen"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.6"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
{file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
{file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
] ]
[[package]] [[package]]
name = "imagesize" name = "imagesize"
version = "1.4.1" version = "1.4.1"
description = "Getting image size from png/jpeg/jpeg2000/gif file" description = "Getting image size from png/jpeg/jpeg2000/gif file"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [ files = [
{file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
@ -805,13 +802,13 @@ testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytes
[[package]] [[package]]
name = "jaraco-functools" name = "jaraco-functools"
version = "4.0.0"
version = "4.0.1"
description = "Functools like those found in stdlib" description = "Functools like those found in stdlib"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "jaraco.functools-4.0.0-py3-none-any.whl", hash = "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d"},
{file = "jaraco.functools-4.0.0.tar.gz", hash = "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925"},
{file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"},
{file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"},
] ]
[package.dependencies] [package.dependencies]
@ -819,7 +816,7 @@ more-itertools = "*"
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["jaraco.classes", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]] [[package]]
name = "jeepney" name = "jeepney"
@ -870,13 +867,13 @@ typing-extensions = ">=4.5.0"
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "25.1.0"
version = "25.2.0"
description = "Store and access your passwords safely." description = "Store and access your passwords safely."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "keyring-25.1.0-py3-none-any.whl", hash = "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427"},
{file = "keyring-25.1.0.tar.gz", hash = "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893"},
{file = "keyring-25.2.0-py3-none-any.whl", hash = "sha256:19f17d40335444aab84b19a0d16a77ec0758a9c384e3446ae2ed8bd6d53b67a5"},
{file = "keyring-25.2.0.tar.gz", hash = "sha256:7045f367268ce42dba44745050164b431e46f6e92f99ef2937dfadaef368d8cf"},
] ]
[package.dependencies] [package.dependencies]
@ -892,13 +889,13 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras] [package.extras]
completion = ["shtab (>=1.1.0)"] completion = ["shtab (>=1.1.0)"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]] [[package]]
name = "m2r2" name = "m2r2"
version = "0.3.2" version = "0.3.2"
description = "Markdown and reStructuredText in a single file." description = "Markdown and reStructuredText in a single file."
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"},
@ -1028,29 +1025,13 @@ files = [
name = "mistune" name = "mistune"
version = "0.8.4" version = "0.8.4"
description = "The fastest markdown parser in pure Python" description = "The fastest markdown parser in pure Python"
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
] ]
[[package]]
name = "mock"
version = "4.0.3"
description = "Rolling backport of unittest.mock for all Pythons"
optional = true
python-versions = ">=3.6"
files = [
{file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"},
{file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"},
]
[package.extras]
build = ["blurb", "twine", "wheel"]
docs = ["sphinx"]
test = ["pytest (<5.4)", "pytest-cov"]
[[package]] [[package]]
name = "more-itertools" name = "more-itertools"
version = "10.2.0" version = "10.2.0"
@ -1150,28 +1131,29 @@ testing = ["pytest", "pytest-cov", "wheel"]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
version = "4.2.1"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
{file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"},
{file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"},
] ]
[package.extras] [package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.4.0"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
] ]
[package.extras] [package.extras]
@ -1296,13 +1278,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytes
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.1.1"
version = "8.1.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
{file = "pytest-8.1.2-py3-none-any.whl", hash = "sha256:6c06dc309ff46a05721e6fd48e492a775ed8165d2ecdf57f156a80c7e95bb142"},
{file = "pytest-8.1.2.tar.gz", hash = "sha256:f3c45d1d5eed96b01a2aea70dee6a4a366d51d38f9957768083e4fecfc77f3ef"},
] ]
[package.dependencies] [package.dependencies]
@ -1352,7 +1334,7 @@ six = ">=1.5"
name = "pytz" name = "pytz"
version = "2024.1" version = "2024.1"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
@ -1467,7 +1449,7 @@ md = ["cmarkgfm (>=0.8.0)"]
name = "readthedocs-sphinx-ext" name = "readthedocs-sphinx-ext"
version = "2.2.5" version = "2.2.5"
description = "Sphinx extension for Read the Docs overrides" description = "Sphinx extension for Read the Docs overrides"
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27"}, {file = "readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27"},
@ -1483,7 +1465,7 @@ requests = "*"
name = "recommonmark" name = "recommonmark"
version = "0.7.1" version = "0.7.1"
description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects."
optional = true
optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"},
@ -1580,18 +1562,18 @@ jeepney = ">=0.6"
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.2.0"
version = "69.5.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
{file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
] ]
[package.extras] [package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]] [[package]]
@ -1618,20 +1600,20 @@ files = [
[[package]] [[package]]
name = "sphinx" name = "sphinx"
version = "6.2.1"
version = "7.1.2"
description = "Python documentation generator" description = "Python documentation generator"
optional = true
optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b"},
{file = "sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912"},
{file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"},
{file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"},
] ]
[package.dependencies] [package.dependencies]
alabaster = ">=0.7,<0.8" alabaster = ">=0.7,<0.8"
babel = ">=2.9" babel = ">=2.9"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.18.1,<0.20"
docutils = ">=0.18.1,<0.21"
imagesize = ">=1.3" imagesize = ">=1.3"
importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
Jinja2 = ">=3.0" Jinja2 = ">=3.0"
@ -1655,7 +1637,7 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
name = "sphinx-autoapi" name = "sphinx-autoapi"
version = "3.0.0" version = "3.0.0"
description = "Sphinx API documentation generator" description = "Sphinx API documentation generator"
optional = true
optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "sphinx-autoapi-3.0.0.tar.gz", hash = "sha256:09ebd674a32b44467222b0fb8a917b97c89523f20dbf05b52cb8a3f0e15714de"}, {file = "sphinx-autoapi-3.0.0.tar.gz", hash = "sha256:09ebd674a32b44467222b0fb8a917b97c89523f20dbf05b52cb8a3f0e15714de"},
@ -1677,18 +1659,18 @@ docs = ["furo", "sphinx", "sphinx-design"]
[[package]] [[package]]
name = "sphinx-rtd-theme" name = "sphinx-rtd-theme"
version = "1.3.0"
version = "2.0.0"
description = "Read the Docs theme for Sphinx" description = "Read the Docs theme for Sphinx"
optional = true
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
optional = false
python-versions = ">=3.6"
files = [ files = [
{file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"},
{file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"},
{file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"},
{file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"},
] ]
[package.dependencies] [package.dependencies]
docutils = "<0.19"
sphinx = ">=1.6,<8"
docutils = "<0.21"
sphinx = ">=5,<8"
sphinxcontrib-jquery = ">=4,<5" sphinxcontrib-jquery = ">=4,<5"
[package.extras] [package.extras]
@ -1698,7 +1680,7 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
name = "sphinxcontrib-applehelp" name = "sphinxcontrib-applehelp"
version = "1.0.4" version = "1.0.4"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
optional = true
optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"},
@ -1713,7 +1695,7 @@ test = ["pytest"]
name = "sphinxcontrib-devhelp" name = "sphinxcontrib-devhelp"
version = "1.0.2" version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
optional = true
optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
@ -1728,7 +1710,7 @@ test = ["pytest"]
name = "sphinxcontrib-htmlhelp" name = "sphinxcontrib-htmlhelp"
version = "2.0.1" version = "2.0.1"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
optional = true
optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"},
@ -1743,7 +1725,7 @@ test = ["html5lib", "pytest"]
name = "sphinxcontrib-jquery" name = "sphinxcontrib-jquery"
version = "4.1" version = "4.1"
description = "Extension to include jQuery on newer Sphinx releases" description = "Extension to include jQuery on newer Sphinx releases"
optional = true
optional = false
python-versions = ">=2.7" python-versions = ">=2.7"
files = [ files = [
{file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"},
@ -1757,7 +1739,7 @@ Sphinx = ">=1.8"
name = "sphinxcontrib-jsmath" name = "sphinxcontrib-jsmath"
version = "1.0.1" version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript" description = "A sphinx extension which renders display math in HTML via JavaScript"
optional = true
optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
@ -1771,7 +1753,7 @@ test = ["flake8", "mypy", "pytest"]
name = "sphinxcontrib-qthelp" name = "sphinxcontrib-qthelp"
version = "1.0.3" version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
optional = true
optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
@ -1786,7 +1768,7 @@ test = ["pytest"]
name = "sphinxcontrib-serializinghtml" name = "sphinxcontrib-serializinghtml"
version = "1.1.5" version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
optional = true
optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
files = [ files = [
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
@ -1835,13 +1817,13 @@ files = [
[[package]] [[package]]
name = "tox" name = "tox"
version = "4.14.2"
version = "4.15.0"
description = "tox is a generic virtualenv management and test command line tool" description = "tox is a generic virtualenv management and test command line tool"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "tox-4.14.2-py3-none-any.whl", hash = "sha256:2900c4eb7b716af4a928a7fdc2ed248ad6575294ed7cfae2ea41203937422847"},
{file = "tox-4.14.2.tar.gz", hash = "sha256:0defb44f6dafd911b61788325741cc6b2e12ea71f987ac025ad4d649f1f1a104"},
{file = "tox-4.15.0-py3-none-any.whl", hash = "sha256:300055f335d855b2ab1b12c5802de7f62a36d4fd53f30bd2835f6a201dda46ea"},
{file = "tox-4.15.0.tar.gz", hash = "sha256:7a0beeef166fbe566f54f795b4906c31b428eddafc0102ac00d20998dd1933f6"},
] ]
[package.dependencies] [package.dependencies]
@ -1912,13 +1894,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.25.1"
version = "20.26.0"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
{file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
{file = "virtualenv-20.26.0-py3-none-any.whl", hash = "sha256:0846377ea76e818daaa3e00a4365c018bc3ac9760cbb3544de542885aad61fb3"},
{file = "virtualenv-20.26.0.tar.gz", hash = "sha256:ec25a9671a5102c8d2657f62792a27b48f016664c6873f6beed3800008577210"},
] ]
[package.dependencies] [package.dependencies]
@ -1927,7 +1909,7 @@ filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5" platformdirs = ">=3.9.1,<5"
[package.extras] [package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[[package]] [[package]]
@ -1970,10 +1952,7 @@ files = [
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[extras]
docs = ["Sphinx", "alabaster", "commonmark", "m2r2", "mock", "readthedocs-sphinx-ext", "recommonmark", "sphinx-autoapi", "sphinx-rtd-theme"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.8,<4.0" python-versions = ">=3.8,<4.0"
content-hash = "fe45fda91997f6001207b828d4c17c2364b7c4b403ec3d82ef8154252e2a8f4c"
content-hash = "7594e5a7a562c246e5c6ab3df0282095bb70ee23e9ce748adf57ce67cb0f9413"

30
pyproject.toml

@ -31,31 +31,19 @@ Documentation = "https://python-keycloak.readthedocs.io/en/latest/"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.8,<4.0" python = ">=3.8,<4.0"
requests = ">=2.20.0" requests = ">=2.20.0"
mock = {version = "^4.0.3", optional = true}
alabaster = {version = "^0.7.12", optional = true}
commonmark = {version = "^0.9.1", optional = true}
recommonmark = {version = "^0.7.1", optional = true}
Sphinx = {version = "^6.1.0", optional = true}
sphinx-rtd-theme = {version = "^1.0.0", optional = true}
readthedocs-sphinx-ext = {version = "^2.1.9", optional = true}
m2r2 = {version = "^0.3.2", optional = true}
sphinx-autoapi = {version = "^3.0.0", optional = true}
requests-toolbelt = ">=0.6.0" requests-toolbelt = ">=0.6.0"
deprecation = ">=2.1.0" deprecation = ">=2.1.0"
jwcrypto = "^1.5.4" jwcrypto = "^1.5.4"
[tool.poetry.extras]
docs = [
"mock",
"alabaster",
"commonmark",
"recommonmark",
"sphinx",
"sphinx-rtd-theme",
"readthedocs-sphinx-ext",
"m2r2",
"sphinx-autoapi",
]
[tool.poetry.group.docs.dependencies]
alabaster = ">=0.7.12"
commonmark = ">=0.9.1"
recommonmark = ">=0.7.1"
Sphinx = ">=6.1.0"
sphinx-rtd-theme = ">=1.0.0"
readthedocs-sphinx-ext = ">=2.1.9"
m2r2 = ">=0.3.2"
sphinx-autoapi = ">=3.0.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
tox = ">=4.0.0" tox = ">=4.0.0"

475
src/keycloak/keycloak_admin.py

@ -31,11 +31,9 @@ import json
from builtins import isinstance from builtins import isinstance
from typing import Optional from typing import Optional
import deprecation
from requests_toolbelt import MultipartEncoder from requests_toolbelt import MultipartEncoder
from . import urls_patterns from . import urls_patterns
from ._version import __version__
from .exceptions import ( from .exceptions import (
KeycloakDeleteError, KeycloakDeleteError,
KeycloakGetError, KeycloakGetError,
@ -73,9 +71,6 @@ class KeycloakAdmin:
:type custom_headers: dict :type custom_headers: dict
:param user_realm_name: The realm name of the user, if different from realm_name :param user_realm_name: The realm name of the user, if different from realm_name
:type user_realm_name: str :type user_realm_name: str
:param auto_refresh_token: list of methods that allows automatic token refresh.
Ex: ['get', 'put', 'post', 'delete']
:type auto_refresh_token: list
:param timeout: connection timeout in seconds :param timeout: connection timeout in seconds
:type timeout: int :type timeout: int
:param connection: A KeycloakOpenIDConnection as an alternative to individual params. :param connection: A KeycloakOpenIDConnection as an alternative to individual params.
@ -84,9 +79,6 @@ class KeycloakAdmin:
PAGE_SIZE = 100 PAGE_SIZE = 100
_auto_refresh_token = None
_connection: Optional[KeycloakOpenIDConnection] = None
def __init__( def __init__(
self, self,
server_url=None, server_url=None,
@ -100,7 +92,6 @@ class KeycloakAdmin:
client_secret_key=None, client_secret_key=None,
custom_headers=None, custom_headers=None,
user_realm_name=None, user_realm_name=None,
auto_refresh_token=None,
timeout=60, timeout=60,
connection: Optional[KeycloakOpenIDConnection] = None, connection: Optional[KeycloakOpenIDConnection] = None,
): ):
@ -130,9 +121,6 @@ class KeycloakAdmin:
:type custom_headers: dict :type custom_headers: dict
:param user_realm_name: The realm name of the user, if different from realm_name :param user_realm_name: The realm name of the user, if different from realm_name
:type user_realm_name: str :type user_realm_name: str
:param auto_refresh_token: list of methods that allows automatic token refresh.
Ex: ['get', 'put', 'post', 'delete']
:type auto_refresh_token: list
:param timeout: connection timeout in seconds :param timeout: connection timeout in seconds
:type timeout: int :type timeout: int
:param connection: An OpenID Connection as an alternative to individual params. :param connection: An OpenID Connection as an alternative to individual params.
@ -152,58 +140,6 @@ class KeycloakAdmin:
custom_headers=custom_headers, custom_headers=custom_headers,
timeout=timeout, timeout=timeout,
) )
if auto_refresh_token is not None:
self.auto_refresh_token = auto_refresh_token
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.server_url property instead",
)
def server_url(self):
"""Get server url.
:returns: Keycloak server url
:rtype: str
"""
return self.connection.server_url
@server_url.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.server_url property instead",
)
def server_url(self, value):
self.connection.server_url = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.realm_name property instead",
)
def realm_name(self):
"""Get realm name.
:returns: Realm name
:rtype: str
"""
return self.connection.realm_name
@realm_name.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.realm_name property instead",
)
def realm_name(self, value):
self.connection.realm_name = value
@property @property
def connection(self) -> KeycloakOpenIDConnection: def connection(self) -> KeycloakOpenIDConnection:
@ -218,256 +154,6 @@ class KeycloakAdmin:
def connection(self, value: KeycloakOpenIDConnection) -> None: def connection(self, value: KeycloakOpenIDConnection) -> None:
self._connection = value self._connection = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.client_id property instead",
)
def client_id(self):
"""Get client id.
:returns: Client id
:rtype: str
"""
return self.connection.client_id
@client_id.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.client_id property instead",
)
def client_id(self, value):
self.connection.client_id = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.client_secret_key property instead",
)
def client_secret_key(self):
"""Get client secret key.
:returns: Client secret key
:rtype: str
"""
return self.connection.client_secret_key
@client_secret_key.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.client_secret_key property instead",
)
def client_secret_key(self, value):
self.connection.client_secret_key = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.verify property instead",
)
def verify(self):
"""Get verify.
:returns: Verify indicator
:rtype: bool
"""
return self.connection.verify
@verify.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.verify property instead",
)
def verify(self, value):
self.connection.verify = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.username property instead",
)
def username(self):
"""Get username.
:returns: Admin username
:rtype: str
"""
return self.connection.username
@username.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.username property instead",
)
def username(self, value):
self.connection.username = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.password property instead",
)
def password(self):
"""Get password.
:returns: Admin password
:rtype: str
"""
return self.connection.password
@password.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.password property instead",
)
def password(self, value):
self.connection.password = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.totp property instead",
)
def totp(self):
"""Get totp.
:returns: TOTP
:rtype: str
"""
return self.connection.totp
@totp.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.totp property instead",
)
def totp(self, value):
self.connection.totp = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.token property instead",
)
def token(self):
"""Get token.
:returns: Access and refresh token
:rtype: dict
"""
return self.connection.token
@token.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.token property instead",
)
def token(self, value):
self.connection.token = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.user_realm_name property instead",
)
def user_realm_name(self):
"""Get user realm name.
:returns: User realm name
:rtype: str
"""
return self.connection.user_realm_name
@user_realm_name.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.user_realm_name property instead",
)
def user_realm_name(self, value):
self.connection.user_realm_name = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.custom_headers property instead",
)
def custom_headers(self):
"""Get custom headers.
:returns: Custom headers
:rtype: dict
"""
return self.connection.custom_headers
@custom_headers.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.custom_headers property instead",
)
def custom_headers(self, value):
self.connection.custom_headers = value
@property
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Auto-refresh will be implicitly set for all requests",
)
def auto_refresh_token(self):
"""Get auto refresh token.
:returns: List of methods for automatic token refresh
:rtype: list
"""
return self._auto_refresh_token
@auto_refresh_token.setter
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Auto-refresh will be implicitly set for all requests",
)
def auto_refresh_token(self, value):
self._auto_refresh_token = value or []
def __fetch_all(self, url, query=None): def __fetch_all(self, url, query=None):
"""Paginate over get requests. """Paginate over get requests.
@ -1268,7 +954,7 @@ class KeycloakAdmin:
data_raw = self.connection.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO) data_raw = self.connection.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO)
return raise_error_from_response(data_raw, KeycloakGetError) return raise_error_from_response(data_raw, KeycloakGetError)
def get_groups(self, query=None):
def get_groups(self, query=None, full_hierarchy=False):
"""Get groups. """Get groups.
Returns a list of groups belonging to the realm Returns a list of groups belonging to the realm
@ -1276,8 +962,15 @@ class KeycloakAdmin:
GroupRepresentation GroupRepresentation
https://www.keycloak.org/docs-api/24.0.2/rest-api/#_grouprepresentation https://www.keycloak.org/docs-api/24.0.2/rest-api/#_grouprepresentation
Notice that when using full_hierarchy=True, the response will be a nested structure
containing all the children groups. If used with query parameters, the full_hierarchy
will be applied to the received groups only.
:param query: Additional query options :param query: Additional query options
:type query: dict :type query: dict
:param full_hierarchy: If True, return all of the nested children groups, otherwise only
the first level children are returned
:type full_hierarchy: bool
:return: array GroupRepresentation :return: array GroupRepresentation
:rtype: list :rtype: list
""" """
@ -1293,11 +986,13 @@ class KeycloakAdmin:
# For version +23.0.0 # For version +23.0.0
for group in groups: for group in groups:
if group.get("subGroupCount"): if group.get("subGroupCount"):
group["subGroups"] = self.get_group_children(group.get("id"))
group["subGroups"] = self.get_group_children(
group_id=group.get("id"), full_hierarchy=full_hierarchy
)
return groups return groups
def get_group(self, group_id):
def get_group(self, group_id, full_hierarchy=False):
"""Get group by id. """Get group by id.
Returns full group details Returns full group details
@ -1307,6 +1002,9 @@ class KeycloakAdmin:
:param group_id: The group id :param group_id: The group id
:type group_id: str :type group_id: str
:param full_hierarchy: If True, return all of the nested children groups, otherwise only
the first level children are returned
:type full_hierarchy: bool
:return: Keycloak server response (GroupRepresentation) :return: Keycloak server response (GroupRepresentation)
:rtype: dict :rtype: dict
""" """
@ -1319,7 +1017,9 @@ class KeycloakAdmin:
# For version +23.0.0 # For version +23.0.0
group = response.json() group = response.json()
if group.get("subGroupCount"): if group.get("subGroupCount"):
group["subGroups"] = self.get_group_children(group.get("id"))
group["subGroups"] = self.get_group_children(
group.get("id"), full_hierarchy=full_hierarchy
)
return group return group
@ -1349,7 +1049,7 @@ class KeycloakAdmin:
# went through the tree without hits # went through the tree without hits
return None return None
def get_group_children(self, group_id, query=None):
def get_group_children(self, group_id, query=None, full_hierarchy=False):
"""Get group children by parent id. """Get group children by parent id.
Returns full group children details Returns full group children details
@ -1358,15 +1058,32 @@ class KeycloakAdmin:
:type group_id: str :type group_id: str
:param query: Additional query options :param query: Additional query options
:type query: dict :type query: dict
:param full_hierarchy: If True, return all of the nested children groups
:type full_hierarchy: bool
:return: Keycloak server response (GroupRepresentation) :return: Keycloak server response (GroupRepresentation)
:rtype: dict :rtype: dict
:raises ValueError: If both query and full_hierarchy parameters are used
""" """
query = query or {} query = query or {}
if query and full_hierarchy:
raise ValueError("Cannot use both query and full_hierarchy parameters")
params_path = {"realm-name": self.connection.realm_name, "id": group_id} params_path = {"realm-name": self.connection.realm_name, "id": group_id}
url = urls_patterns.URL_ADMIN_GROUP_CHILD.format(**params_path) url = urls_patterns.URL_ADMIN_GROUP_CHILD.format(**params_path)
if "first" in query or "max" in query: if "first" in query or "max" in query:
return self.__fetch_paginated(url, query) return self.__fetch_paginated(url, query)
return self.__fetch_all(url, query)
res = self.__fetch_all(url, query)
if not full_hierarchy:
return res
for group in res:
if group.get("subGroupCount"):
group["subGroups"] = self.get_group_children(
group_id=group.get("id"), full_hierarchy=full_hierarchy
)
return res
def get_group_members(self, group_id, query=None): def get_group_members(self, group_id, query=None):
"""Get members by group id. """Get members by group id.
@ -4095,120 +3812,6 @@ class KeycloakAdmin:
) )
return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204])
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.raw_get function instead",
)
def raw_get(self, *args, **kwargs):
"""Call connection.raw_get.
If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token
and try *get* once more.
:param args: Additional arguments
:type args: tuple
:param kwargs: Additional keyword arguments
:type kwargs: dict
:returns: Response
:rtype: Response
"""
return self.connection.raw_get(*args, **kwargs)
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.raw_post function instead",
)
def raw_post(self, *args, **kwargs):
"""Call connection.raw_post.
If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token
and try *post* once more.
:param args: Additional arguments
:type args: tuple
:param kwargs: Additional keyword arguments
:type kwargs: dict
:returns: Response
:rtype: Response
"""
return self.connection.raw_post(*args, **kwargs)
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.raw_put function instead",
)
def raw_put(self, *args, **kwargs):
"""Call connection.raw_put.
If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token
and try *put* once more.
:param args: Additional arguments
:type args: tuple
:param kwargs: Additional keyword arguments
:type kwargs: dict
:returns: Response
:rtype: Response
"""
return self.connection.raw_put(*args, **kwargs)
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.raw_delete function instead",
)
def raw_delete(self, *args, **kwargs):
"""Call connection.raw_delete.
If auto_refresh is set for *delete* and *access_token* is expired,
it will refresh the token and try *delete* once more.
:param args: Additional arguments
:type args: tuple
:param kwargs: Additional keyword arguments
:type kwargs: dict
:returns: Response
:rtype: Response
"""
return self.connection.raw_delete(*args, **kwargs)
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.get_token function instead",
)
def get_token(self):
"""Get admin token.
The admin token is then set in the `token` attribute.
:returns: token
:rtype: dict
"""
return self.connection.get_token()
@deprecation.deprecated(
deprecated_in="2.13.0",
removed_in="4.0.0",
current_version=__version__,
details="Use the connection.refresh_token function instead",
)
def refresh_token(self):
"""Refresh the token.
:returns: token
:rtype: dict
"""
return self.connection.refresh_token()
def get_client_all_sessions(self, client_id): def get_client_all_sessions(self, client_id):
"""Get sessions associated with the client. """Get sessions associated with the client.
@ -4368,8 +3971,8 @@ class KeycloakAdmin:
:return: Keycloak server response :return: Keycloak server response
:rtype: bytes :rtype: bytes
""" """
params_path = {"realm-name": self.realm_name, "id": client_id}
data_raw = self.raw_post(
params_path = {"realm-name": self.connection.realm_name, "id": client_id}
data_raw = self.connection.raw_post(
urls_patterns.URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path), urls_patterns.URL_ADMIN_ADD_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path),
data=json.dumps(payload), data=json.dumps(payload),
) )

37
src/keycloak/keycloak_openid.py

@ -212,7 +212,7 @@ class KeycloakOpenID:
:type token: str :type token: str
:param method_token_info: Token info method to use :param method_token_info: Token info method to use
:type method_token_info: str :type method_token_info: str
:param kwargs: Additional keyword arguments
:param kwargs: Additional keyword arguments passed to the decode_token method
:type kwargs: dict :type kwargs: dict
:returns: Token info :returns: Token info
:rtype: dict :rtype: dict
@ -516,7 +516,7 @@ class KeycloakOpenID:
data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError) return raise_error_from_response(data_raw, KeycloakPostError)
def decode_token(self, token, key, algorithms=["RS256"], **kwargs):
def decode_token(self, token, validate: bool = True, **kwargs):
"""Decode user token. """Decode user token.
A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data
@ -530,25 +530,30 @@ class KeycloakOpenID:
:param token: Keycloak token :param token: Keycloak token
:type token: str :type token: str
:param key: Decode key
:type key: str
:param algorithms: Algorithms to use for decoding
:type algorithms: list[str]
:param kwargs: Keyword arguments
:param validate: Determines whether the token should be validated with the public key.
Defaults to True.
:type validate: bool
:param kwargs: Additional keyword arguments for jwcrypto's JWT object
:type kwargs: dict :type kwargs: dict
:returns: Decoded token :returns: Decoded token
:rtype: dict :rtype: dict
""" """
# To keep the same API, we map the python-jose options to our claims for jwcrypto
# Per the jwcrypto dev, `exp` and `nbf` are always checked
options = kwargs.get("options", {})
check_claims = {}
if options.get("verify_aud") is True:
check_claims["aud"] = self.client_id
k = jwk.JWK.from_pem(key.encode("utf-8"))
full_jwt = jwt.JWT(jwt=token, key=k, algs=algorithms, check_claims=check_claims)
if validate:
if "key" not in kwargs:
key = (
"-----BEGIN PUBLIC KEY-----\n"
+ self.public_key()
+ "\n-----END PUBLIC KEY-----"
)
key = jwk.JWK.from_pem(key.encode("utf-8"))
kwargs["key"] = key
full_jwt = jwt.JWT(jwt=token, **kwargs)
return jwt.json_decode(full_jwt.claims) return jwt.json_decode(full_jwt.claims)
else:
full_jwt = jwt.JWT(jwt=token, **kwargs)
full_jwt.token.objects["valid"] = True
return json.loads(full_jwt.token.payload.decode("utf-8"))
def load_authorization_config(self, path): def load_authorization_config(self, path):
"""Load Keycloak settings (authorization). """Load Keycloak settings (authorization).

78
tests/conftest.py

@ -4,7 +4,7 @@ import ipaddress
import os import os
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Tuple
from typing import Generator, Tuple
import freezegun import freezegun
import pytest import pytest
@ -337,7 +337,69 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak
@pytest.fixture @pytest.fixture
def realm(admin: KeycloakAdmin) -> str:
def oid_with_credentials_device(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin):
"""Fixture for an initialized KeycloakOpenID class and a random user credentials.
:param env: Keycloak test environment
:type env: KeycloakTestEnv
:param realm: Keycloak realm
:type realm: str
:param admin: Keycloak admin
:type admin: KeycloakAdmin
:yields: Keycloak OpenID client with user credentials
:rtype: Tuple[KeycloakOpenID, str, str]
"""
# Set the realm
admin.change_current_realm(realm)
# Create client
client = str(uuid.uuid4())
secret = str(uuid.uuid4())
client_id = admin.create_client(
payload={
"name": client,
"clientId": client,
"enabled": True,
"publicClient": False,
"protocol": "openid-connect",
"secret": secret,
"clientAuthenticatorType": "client-secret",
"attributes": {"oauth2.device.authorization.grant.enabled": True},
}
)
# Create user
username = str(uuid.uuid4())
password = str(uuid.uuid4())
user_id = admin.create_user(
payload={
"username": username,
"email": f"{username}@test.test",
"enabled": True,
"firstName": "first",
"lastName": "last",
"emailVerified": True,
"requiredActions": [],
"credentials": [{"type": "password", "value": password, "temporary": False}],
}
)
yield (
KeycloakOpenID(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
realm_name=realm,
client_id=client,
client_secret_key=secret,
),
username,
password,
)
# Cleanup
admin.delete_client(client_id=client_id)
admin.delete_user(user_id=user_id)
@pytest.fixture
def realm(admin: KeycloakAdmin) -> Generator[str, None, None]:
"""Fixture for a new random realm. """Fixture for a new random realm.
:param admin: Keycloak admin :param admin: Keycloak admin
@ -352,7 +414,7 @@ def realm(admin: KeycloakAdmin) -> str:
@pytest.fixture @pytest.fixture
def user(admin: KeycloakAdmin, realm: str) -> str:
def user(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
"""Fixture for a new random user. """Fixture for a new random user.
:param admin: Keycloak admin :param admin: Keycloak admin
@ -370,7 +432,7 @@ def user(admin: KeycloakAdmin, realm: str) -> str:
@pytest.fixture @pytest.fixture
def group(admin: KeycloakAdmin, realm: str) -> str:
def group(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
"""Fixture for a new random group. """Fixture for a new random group.
:param admin: Keycloak admin :param admin: Keycloak admin
@ -388,7 +450,7 @@ def group(admin: KeycloakAdmin, realm: str) -> str:
@pytest.fixture @pytest.fixture
def client(admin: KeycloakAdmin, realm: str) -> str:
def client(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
"""Fixture for a new random client. """Fixture for a new random client.
:param admin: Keycloak admin :param admin: Keycloak admin
@ -406,7 +468,7 @@ def client(admin: KeycloakAdmin, realm: str) -> str:
@pytest.fixture @pytest.fixture
def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
def client_role(admin: KeycloakAdmin, realm: str, client: str) -> Generator[str, None, None]:
"""Fixture for a new random client role. """Fixture for a new random client role.
:param admin: Keycloak admin :param admin: Keycloak admin
@ -426,7 +488,9 @@ def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
@pytest.fixture @pytest.fixture
def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str:
def composite_client_role(
admin: KeycloakAdmin, realm: str, client: str, client_role: str
) -> Generator[str, None, None]:
"""Fixture for a new random composite client role. """Fixture for a new random composite client role.
:param admin: Keycloak admin :param admin: Keycloak admin

142
tests/test_keycloak_admin.py

@ -1,12 +1,14 @@
"""Test the keycloak admin object.""" """Test the keycloak admin object."""
import copy import copy
import os
import uuid import uuid
from typing import Tuple from typing import Tuple
import freezegun import freezegun
import pytest import pytest
from dateutil import parser as datetime_parser from dateutil import parser as datetime_parser
from packaging.version import Version
import keycloak import keycloak
from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakOpenIDConnection from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakOpenIDConnection
@ -46,19 +48,21 @@ def test_keycloak_admin_init(env):
username=env.KEYCLOAK_ADMIN, username=env.KEYCLOAK_ADMIN,
password=env.KEYCLOAK_ADMIN_PASSWORD, password=env.KEYCLOAK_ADMIN_PASSWORD,
) )
assert admin.server_url == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", admin.server_url
assert admin.realm_name == "master", admin.realm_name
assert (
admin.connection.server_url == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}"
), admin.connection.server_url
assert admin.connection.realm_name == "master", admin.connection.realm_name
assert isinstance(admin.connection, ConnectionManager), type(admin.connection) assert isinstance(admin.connection, ConnectionManager), type(admin.connection)
assert admin.client_id == "admin-cli", admin.client_id
assert admin.client_secret_key is None, admin.client_secret_key
assert admin.verify, admin.verify
assert admin.username == env.KEYCLOAK_ADMIN, admin.username
assert admin.password == env.KEYCLOAK_ADMIN_PASSWORD, admin.password
assert admin.totp is None, admin.totp
assert admin.token is not None, admin.token
assert admin.user_realm_name is None, admin.user_realm_name
assert admin.custom_headers is None, admin.custom_headers
assert admin.token
assert admin.connection.client_id == "admin-cli", admin.connection.client_id
assert admin.connection.client_secret_key is None, admin.connection.client_secret_key
assert admin.connection.verify, admin.connection.verify
assert admin.connection.username == env.KEYCLOAK_ADMIN, admin.connection.username
assert admin.connection.password == env.KEYCLOAK_ADMIN_PASSWORD, admin.connection.password
assert admin.connection.totp is None, admin.connection.totp
assert admin.connection.token is not None, admin.connection.token
assert admin.connection.user_realm_name is None, admin.connection.user_realm_name
assert admin.connection.custom_headers is None, admin.connection.custom_headers
assert admin.connection.token
admin = KeycloakAdmin( admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
@ -67,7 +71,7 @@ def test_keycloak_admin_init(env):
realm_name=None, realm_name=None,
user_realm_name="master", user_realm_name="master",
) )
assert admin.token
assert admin.connection.token
admin = KeycloakAdmin( admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
username=env.KEYCLOAK_ADMIN, username=env.KEYCLOAK_ADMIN,
@ -75,19 +79,19 @@ def test_keycloak_admin_init(env):
realm_name=None, realm_name=None,
user_realm_name=None, user_realm_name=None,
) )
assert admin.token
assert admin.connection.token
token = admin.token
token = admin.connection.token
admin = KeycloakAdmin( admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
token=token, token=token,
realm_name=None, realm_name=None,
user_realm_name=None, user_realm_name=None,
) )
assert admin.token == token
assert admin.connection.token == token
admin.create_realm(payload={"realm": "authz", "enabled": True}) admin.create_realm(payload={"realm": "authz", "enabled": True})
admin.realm_name = "authz"
admin.connection.realm_name = "authz"
admin.create_client( admin.create_client(
payload={ payload={
"name": "authz-client", "name": "authz-client",
@ -107,7 +111,7 @@ def test_keycloak_admin_init(env):
user_realm_name="authz", user_realm_name="authz",
client_id="authz-client", client_id="authz-client",
client_secret_key=secret["value"], client_secret_key=secret["value"],
).token
).connection.token
admin.delete_realm(realm_name="authz") admin.delete_realm(realm_name="authz")
assert ( assert (
@ -117,7 +121,7 @@ def test_keycloak_admin_init(env):
password=None, password=None,
client_secret_key=None, client_secret_key=None,
custom_headers={"custom": "header"}, custom_headers={"custom": "header"},
).token
).connection.token
is None is None
) )
@ -130,7 +134,7 @@ def test_keycloak_admin_init(env):
verify=True, verify=True,
) )
keycloak_admin = KeycloakAdmin(connection=keycloak_connection) keycloak_admin = KeycloakAdmin(connection=keycloak_connection)
assert keycloak_admin.token
assert keycloak_admin.connection.token
def test_realms(admin: KeycloakAdmin): def test_realms(admin: KeycloakAdmin):
@ -333,6 +337,16 @@ def test_users(admin: KeycloakAdmin, realm: str):
admin.update_user(user_id=user_id, payload={"wrong": "payload"}) admin.update_user(user_id=user_id, payload={"wrong": "payload"})
assert err.match('400: b\'{"error":"Unrecognized field') assert err.match('400: b\'{"error":"Unrecognized field')
# Test disable user
res = admin.disable_user(user_id=user_id)
assert res == {}, res
assert not admin.get_user(user_id=user_id)["enabled"]
# Test enable user
res = admin.enable_user(user_id=user_id)
assert res == {}, res
assert admin.get_user(user_id=user_id)["enabled"]
# Test get users again # Test get users again
users = admin.get_users() users = admin.get_users()
usernames = [x["username"] for x in users] usernames = [x["username"] for x in users]
@ -386,6 +400,43 @@ def test_users(admin: KeycloakAdmin, realm: str):
assert err.match(USER_NOT_FOUND_REGEX) assert err.match(USER_NOT_FOUND_REGEX)
def test_enable_disable_all_users(admin: KeycloakAdmin, realm: str):
"""Test enable and disable all users.
:param admin: Keycloak Admin client
:type admin: KeycloakAdmin
:param realm: Keycloak realm
:type realm: str
"""
admin.change_current_realm(realm)
user_id_1 = admin.create_user(
payload={"username": "test", "email": "test@test.test", "enabled": True}
)
user_id_2 = admin.create_user(
payload={"username": "test2", "email": "test2@test.test", "enabled": True}
)
user_id_3 = admin.create_user(
payload={"username": "test3", "email": "test3@test.test", "enabled": True}
)
assert admin.get_user(user_id_1)["enabled"]
assert admin.get_user(user_id_2)["enabled"]
assert admin.get_user(user_id_3)["enabled"]
admin.disable_all_users()
assert not admin.get_user(user_id_1)["enabled"]
assert not admin.get_user(user_id_2)["enabled"]
assert not admin.get_user(user_id_3)["enabled"]
admin.enable_all_users()
assert admin.get_user(user_id_1)["enabled"]
assert admin.get_user(user_id_2)["enabled"]
assert admin.get_user(user_id_3)["enabled"]
def test_users_roles(admin: KeycloakAdmin, realm: str): def test_users_roles(admin: KeycloakAdmin, realm: str):
"""Test users roles. """Test users roles.
@ -745,6 +796,36 @@ def test_groups(admin: KeycloakAdmin, user: str):
assert res is not None, res assert res is not None, res
assert res["id"] == subsubgroup_id_1 assert res["id"] == subsubgroup_id_1
# Test nested search from main group
res = admin.get_subgroups(
group=admin.get_group(group_id=group_id, full_hierarchy=True),
path="/main-group/subgroup-2/subsubgroup-1",
)
assert res["id"] == subsubgroup_id_1
# Test nested search from all groups
res = admin.get_groups(full_hierarchy=True)
assert len(res) == 1
assert len(res[0]["subGroups"]) == 2
assert len([x for x in res[0]["subGroups"] if x["id"] == subgroup_id_1][0]["subGroups"]) == 0
assert len([x for x in res[0]["subGroups"] if x["id"] == subgroup_id_2][0]["subGroups"]) == 1
# Test that query params are not allowed for full hierarchy
with pytest.raises(ValueError) as err:
admin.get_group_children(group_id=group_id, full_hierarchy=True, query={"max": 10})
# Test that query params are passed
if os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"] == "latest" or Version(
os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"]
) >= Version("23"):
res = admin.get_group_children(group_id=group_id, query={"max": 1})
assert len(res) == 1
assert err.match("Cannot use both query and full_hierarchy parameters")
main_group_id_2 = admin.create_group(payload={"name": "main-group-2"})
assert len(admin.get_groups(full_hierarchy=True)) == 2
# Test empty search # Test empty search
res = admin.get_subgroups(group=main_group, path="/none") res = admin.get_subgroups(group=main_group, path="/none")
assert res is None, res assert res is None, res
@ -816,6 +897,8 @@ def test_groups(admin: KeycloakAdmin, user: str):
# Test delete # Test delete
res = admin.delete_group(group_id=group_id) res = admin.delete_group(group_id=group_id)
assert res == dict(), res assert res == dict(), res
res = admin.delete_group(group_id=main_group_id_2)
assert res == dict(), res
assert len(admin.get_groups()) == 0 assert len(admin.get_groups()) == 0
# Test delete fail # Test delete fail
@ -1352,6 +1435,10 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str):
admin.get_realm_role_groups(role_name="non-existent-role") admin.get_realm_role_groups(role_name="non-existent-role")
assert err.match(COULD_NOT_FIND_ROLE_REGEX) assert err.match(COULD_NOT_FIND_ROLE_REGEX)
# Test with query params
res = admin.get_realm_role_groups(role_name="test-realm-role-update", query={"max": 1})
assert len(res) == 1
# Test delete realm role # Test delete realm role
res = admin.delete_realm_role(role_name=composite_role) res = admin.delete_realm_role(role_name=composite_role)
assert res == dict(), res assert res == dict(), res
@ -1990,7 +2077,7 @@ def test_get_sessions(admin: KeycloakAdmin):
:param admin: Keycloak Admin client :param admin: Keycloak Admin client
:type admin: KeycloakAdmin :type admin: KeycloakAdmin
""" """
sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username))
sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.connection.username))
assert len(sessions) >= 1 assert len(sessions) >= 1
with pytest.raises(KeycloakGetError) as err: with pytest.raises(KeycloakGetError) as err:
admin.get_sessions(user_id="bad") admin.get_sessions(user_id="bad")
@ -2862,7 +2949,7 @@ def test_realm_default_roles(admin: KeycloakAdmin, realm: str) -> None:
assert {x["name"] for x in roles} == {"offline_access", "uma_authorization"} assert {x["name"] for x in roles} == {"offline_access", "uma_authorization"}
with pytest.raises(KeycloakGetError) as err: with pytest.raises(KeycloakGetError) as err:
admin.realm_name = "doesnotexist"
admin.change_current_realm("doesnotexist")
admin.get_realm_default_roles() admin.get_realm_default_roles()
assert err.match('404: b\'{"error":"Realm not found.".*}\'') assert err.match('404: b\'{"error":"Realm not found.".*}\'')
admin.change_current_realm(realm) admin.change_current_realm(realm)
@ -2964,3 +3051,14 @@ def test_initial_access_token(
new_secret = str(uuid.uuid4()) new_secret = str(uuid.uuid4())
res = oid.update_client(res["registrationAccessToken"], client, payload={"secret": new_secret}) res = oid.update_client(res["registrationAccessToken"], client, payload={"secret": new_secret})
assert res["secret"] == new_secret assert res["secret"] == new_secret
def test_refresh_token(admin: KeycloakAdmin):
"""Test refresh token on connection even if it is expired.
:param admin: Keycloak admin
:type admin: KeycloakAdmin
"""
assert admin.connection.token is not None
admin.user_logout(admin.get_user_id(admin.connection.username))
admin.connection.refresh_token()

62
tests/test_keycloak_openid.py

@ -307,15 +307,13 @@ def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
""" """
oid, username, password = oid_with_credentials oid, username, password = oid_with_credentials
token = oid.token(username=username, password=password) token = oid.token(username=username, password=password)
decoded_access_token = oid.decode_token(token=token["access_token"])
decoded_access_token_2 = oid.decode_token(token=token["access_token"], validate=False)
decoded_refresh_token = oid.decode_token(token=token["refresh_token"], validate=False)
assert (
oid.decode_token(
token=token["access_token"],
key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
options={"verify_aud": False},
)["preferred_username"]
== username
)
assert decoded_access_token == decoded_access_token_2
assert decoded_access_token["preferred_username"] == username, decoded_access_token
assert decoded_refresh_token["typ"] == "Refresh", decoded_refresh_token
def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
@ -354,20 +352,17 @@ def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str
oid.load_authorization_config(path="tests/data/authz_settings.json") oid.load_authorization_config(path="tests/data/authz_settings.json")
assert oid.get_policies(token=token["access_token"]) is None assert oid.get_policies(token=token["access_token"]) is None
key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id orig_client_id = oid.client_id
oid.client_id = "account" oid.client_id = "account"
assert oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == []
assert oid.get_policies(token=token["access_token"], method_token_info="decode") == []
policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS") policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile") policy.add_role(role="account/view-profile")
oid.authorization.policies["test"] = policy oid.authorization.policies["test"] = policy
assert [ assert [
str(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
str(x) for x in oid.get_policies(token=token["access_token"], method_token_info="decode")
] == ["Policy: test (role)"] ] == ["Policy: test (role)"]
assert [ assert [
repr(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
repr(x) for x in oid.get_policies(token=token["access_token"], method_token_info="decode")
] == ["<Policy: test (role)>"] ] == ["<Policy: test (role)>"]
oid.client_id = orig_client_id oid.client_id = orig_client_id
@ -392,12 +387,9 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
oid.load_authorization_config(path="tests/data/authz_settings.json") oid.load_authorization_config(path="tests/data/authz_settings.json")
assert oid.get_permissions(token=token["access_token"]) is None assert oid.get_permissions(token=token["access_token"]) is None
key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
orig_client_id = oid.client_id orig_client_id = oid.client_id
oid.client_id = "account" oid.client_id = "account"
assert (
oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == []
)
assert oid.get_permissions(token=token["access_token"], method_token_info="decode") == []
policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS") policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
policy.add_role(role="account/view-profile") policy.add_role(role="account/view-profile")
policy.add_permission( policy.add_permission(
@ -408,15 +400,11 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
oid.authorization.policies["test"] = policy oid.authorization.policies["test"] = policy
assert [ assert [
str(x) str(x)
for x in oid.get_permissions(
token=token["access_token"], method_token_info="decode", key=key
)
for x in oid.get_permissions(token=token["access_token"], method_token_info="decode")
] == ["Permission: test-perm (resource)"] ] == ["Permission: test-perm (resource)"]
assert [ assert [
repr(x) repr(x)
for x in oid.get_permissions(
token=token["access_token"], method_token_info="decode", key=key
)
for x in oid.get_permissions(token=token["access_token"], method_token_info="decode")
] == ["<Permission: test-perm (resource)>"] ] == ["<Permission: test-perm (resource)>"]
oid.client_id = orig_client_id oid.client_id = orig_client_id
@ -471,7 +459,31 @@ def test_has_uma_access(
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())" == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
) )
assert ( assert (
str(oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource"))
str(
oid.has_uma_access(
token=admin.connection.token["access_token"], permissions="Default Resource"
)
)
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=" == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions="
+ "{'Default Resource'})" + "{'Default Resource'})"
) )
def test_device(oid_with_credentials_device: Tuple[KeycloakOpenID, str, str]):
"""Test device authorization flow.
:param oid_with_credentials_device: Keycloak OpenID client with pre-configured user
credentials and device authorization flow enabled
:type oid_with_credentials_device: Tuple[KeycloakOpenID, str, str]
"""
oid, _, _ = oid_with_credentials_device
res = oid.device()
assert res == {
"device_code": mock.ANY,
"user_code": mock.ANY,
"verification_uri": f"http://localhost:8081/realms/{oid.realm_name}/device",
"verification_uri_complete": f"http://localhost:8081/realms/{oid.realm_name}/"
+ f"device?user_code={res['user_code']}",
"expires_in": 600,
"interval": 5,
}

2
tox.ini

@ -24,8 +24,6 @@ commands =
allowlist_externals = black, poetry, isort allowlist_externals = black, poetry, isort
[testenv:docs] [testenv:docs]
commands_pre =
poetry install --no-root --sync -E docs
commands = commands =
sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html
allowlist_externals = sphinx-build, poetry allowlist_externals = sphinx-build, poetry

Loading…
Cancel
Save