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. 39
      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:
- python -m pip install poetry
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).
## 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
from keycloak import KeycloakOpenID

6
docs/source/modules/openid_client.rst

@ -116,9 +116,9 @@ Decode token
.. 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

355
poetry.lock

@ -4,7 +4,7 @@
name = "alabaster"
version = "0.7.13"
description = "A configurable sidebar-enabled Sphinx theme"
optional = true
optional = false
python-versions = ">=3.6"
files = [
{file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
@ -15,7 +15,7 @@ files = [
name = "anyascii"
version = "0.3.2"
description = "Unicode to ASCII transliteration"
optional = true
optional = false
python-versions = ">=3.3"
files = [
{file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"},
@ -24,13 +24,13 @@ files = [
[[package]]
name = "argcomplete"
version = "3.2.3"
version = "3.3.0"
description = "Bash tab completion for argparse"
optional = false
python-versions = ">=3.8"
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]
@ -40,7 +40,7 @@ test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
name = "astroid"
version = "3.1.0"
description = "An abstract syntax tree for Python with inference support."
optional = true
optional = false
python-versions = ">=3.8.0"
files = [
{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"
version = "2.14.0"
description = "Internationalization utilities"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{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]]
name = "backports-tarfile"
version = "1.0.0"
version = "1.1.1"
description = "Backport of CPython tarfile module"
optional = false
python-versions = ">=3.8"
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]
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]]
name = "black"
version = "24.3.0"
version = "24.4.2"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
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]
@ -379,17 +379,17 @@ files = [
[[package]]
name = "commitizen"
version = "3.21.3"
version = "3.24.0"
description = "Python commitizen client tool"
optional = false
python-versions = ">=3.8"
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]
argcomplete = ">=1.12.1,<3.3"
argcomplete = ">=1.12.1,<3.4"
charset-normalizer = ">=2.1.0,<4"
colorama = ">=0.4.1,<0.5.0"
decli = ">=0.6.0,<0.7.0"
@ -405,7 +405,7 @@ tomlkit = ">=0.5.3,<1.0.0"
name = "commonmark"
version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec"
optional = true
optional = false
python-versions = "*"
files = [
{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]]
name = "coverage"
version = "7.4.4"
version = "7.5.0"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
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]
@ -588,21 +588,18 @@ name = "docutils"
version = "0.18.1"
description = "Docutils -- Python Documentation Utilities"
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]]
name = "exceptiongroup"
version = "1.2.0"
version = "1.2.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
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]
@ -610,13 +607,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "filelock"
version = "3.13.3"
version = "3.13.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
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]
@ -657,13 +654,13 @@ pydocstyle = ">=2.1"
[[package]]
name = "freezegun"
version = "1.4.0"
version = "1.5.0"
description = "Let your Python tests travel through time"
optional = false
python-versions = ">=3.7"
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]
@ -671,13 +668,13 @@ python-dateutil = ">=2.7"
[[package]]
name = "identify"
version = "2.5.35"
version = "2.5.36"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
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]
@ -685,20 +682,20 @@ license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.6"
version = "3.7"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.5"
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]]
name = "imagesize"
version = "1.4.1"
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.*"
files = [
{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]]
name = "jaraco-functools"
version = "4.0.0"
version = "4.0.1"
description = "Functools like those found in stdlib"
optional = false
python-versions = ">=3.8"
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]
@ -819,7 +816,7 @@ more-itertools = "*"
[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"]
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]]
name = "jeepney"
@ -870,13 +867,13 @@ typing-extensions = ">=4.5.0"
[[package]]
name = "keyring"
version = "25.1.0"
version = "25.2.0"
description = "Store and access your passwords safely."
optional = false
python-versions = ">=3.8"
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]
@ -892,13 +889,13 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras]
completion = ["shtab (>=1.1.0)"]
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]]
name = "m2r2"
version = "0.3.2"
description = "Markdown and reStructuredText in a single file."
optional = true
optional = false
python-versions = "*"
files = [
{file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"},
@ -1028,29 +1025,13 @@ files = [
name = "mistune"
version = "0.8.4"
description = "The fastest markdown parser in pure Python"
optional = true
optional = false
python-versions = "*"
files = [
{file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
{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]]
name = "more-itertools"
version = "10.2.0"
@ -1150,28 +1131,29 @@ testing = ["pytest", "pytest-cov", "wheel"]
[[package]]
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
python-versions = ">=3.8"
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]
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)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pluggy"
version = "1.4.0"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
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]
@ -1296,13 +1278,13 @@ testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytes
[[package]]
name = "pytest"
version = "8.1.1"
version = "8.1.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
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]
@ -1352,7 +1334,7 @@ six = ">=1.5"
name = "pytz"
version = "2024.1"
description = "World timezone definitions, modern and historical"
optional = true
optional = false
python-versions = "*"
files = [
{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"
version = "2.2.5"
description = "Sphinx extension for Read the Docs overrides"
optional = true
optional = false
python-versions = "*"
files = [
{file = "readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27"},
@ -1483,7 +1465,7 @@ requests = "*"
name = "recommonmark"
version = "0.7.1"
description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects."
optional = true
optional = false
python-versions = "*"
files = [
{file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"},
@ -1580,18 +1562,18 @@ jeepney = ">=0.6"
[[package]]
name = "setuptools"
version = "69.2.0"
version = "69.5.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
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]
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"]
[[package]]
@ -1618,20 +1600,20 @@ files = [
[[package]]
name = "sphinx"
version = "6.2.1"
version = "7.1.2"
description = "Python documentation generator"
optional = true
optional = false
python-versions = ">=3.8"
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]
alabaster = ">=0.7,<0.8"
babel = ">=2.9"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.18.1,<0.20"
docutils = ">=0.18.1,<0.21"
imagesize = ">=1.3"
importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
Jinja2 = ">=3.0"
@ -1655,7 +1637,7 @@ test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"]
name = "sphinx-autoapi"
version = "3.0.0"
description = "Sphinx API documentation generator"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "sphinx-autoapi-3.0.0.tar.gz", hash = "sha256:09ebd674a32b44467222b0fb8a917b97c89523f20dbf05b52cb8a3f0e15714de"},
@ -1677,18 +1659,18 @@ docs = ["furo", "sphinx", "sphinx-design"]
[[package]]
name = "sphinx-rtd-theme"
version = "1.3.0"
version = "2.0.0"
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 = [
{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]
docutils = "<0.19"
sphinx = ">=1.6,<8"
docutils = "<0.21"
sphinx = ">=5,<8"
sphinxcontrib-jquery = ">=4,<5"
[package.extras]
@ -1698,7 +1680,7 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
name = "sphinxcontrib-applehelp"
version = "1.0.4"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"},
@ -1713,7 +1695,7 @@ test = ["pytest"]
name = "sphinxcontrib-devhelp"
version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
optional = true
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
@ -1728,7 +1710,7 @@ test = ["pytest"]
name = "sphinxcontrib-htmlhelp"
version = "2.0.1"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"},
@ -1743,7 +1725,7 @@ test = ["html5lib", "pytest"]
name = "sphinxcontrib-jquery"
version = "4.1"
description = "Extension to include jQuery on newer Sphinx releases"
optional = true
optional = false
python-versions = ">=2.7"
files = [
{file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"},
@ -1757,7 +1739,7 @@ Sphinx = ">=1.8"
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
optional = true
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
@ -1771,7 +1753,7 @@ test = ["flake8", "mypy", "pytest"]
name = "sphinxcontrib-qthelp"
version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
optional = true
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
@ -1786,7 +1768,7 @@ test = ["pytest"]
name = "sphinxcontrib-serializinghtml"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
optional = true
optional = false
python-versions = ">=3.5"
files = [
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
@ -1835,13 +1817,13 @@ files = [
[[package]]
name = "tox"
version = "4.14.2"
version = "4.15.0"
description = "tox is a generic virtualenv management and test command line tool"
optional = false
python-versions = ">=3.8"
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]
@ -1912,13 +1894,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "virtualenv"
version = "20.25.1"
version = "20.26.0"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
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]
@ -1927,7 +1909,7 @@ filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[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)"]
[[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"]
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]
lock-version = "2.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]
python = ">=3.8,<4.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"
deprecation = ">=2.1.0"
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]
tox = ">=4.0.0"

475
src/keycloak/keycloak_admin.py

@ -31,11 +31,9 @@ import json
from builtins import isinstance
from typing import Optional
import deprecation
from requests_toolbelt import MultipartEncoder
from . import urls_patterns
from ._version import __version__
from .exceptions import (
KeycloakDeleteError,
KeycloakGetError,
@ -73,9 +71,6 @@ class KeycloakAdmin:
:type custom_headers: dict
:param user_realm_name: The realm name of the user, if different from realm_name
: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
:type timeout: int
:param connection: A KeycloakOpenIDConnection as an alternative to individual params.
@ -84,9 +79,6 @@ class KeycloakAdmin:
PAGE_SIZE = 100
_auto_refresh_token = None
_connection: Optional[KeycloakOpenIDConnection] = None
def __init__(
self,
server_url=None,
@ -100,7 +92,6 @@ class KeycloakAdmin:
client_secret_key=None,
custom_headers=None,
user_realm_name=None,
auto_refresh_token=None,
timeout=60,
connection: Optional[KeycloakOpenIDConnection] = None,
):
@ -130,9 +121,6 @@ class KeycloakAdmin:
:type custom_headers: dict
:param user_realm_name: The realm name of the user, if different from realm_name
: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
:type timeout: int
:param connection: An OpenID Connection as an alternative to individual params.
@ -152,58 +140,6 @@ class KeycloakAdmin:
custom_headers=custom_headers,
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
def connection(self) -> KeycloakOpenIDConnection:
@ -218,256 +154,6 @@ class KeycloakAdmin:
def connection(self, value: KeycloakOpenIDConnection) -> None:
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):
"""Paginate over get requests.
@ -1268,7 +954,7 @@ class KeycloakAdmin:
data_raw = self.connection.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO)
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.
Returns a list of groups belonging to the realm
@ -1276,8 +962,15 @@ class KeycloakAdmin:
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
: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
:rtype: list
"""
@ -1293,11 +986,13 @@ class KeycloakAdmin:
# For version +23.0.0
for group in groups:
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
def get_group(self, group_id):
def get_group(self, group_id, full_hierarchy=False):
"""Get group by id.
Returns full group details
@ -1307,6 +1002,9 @@ class KeycloakAdmin:
:param group_id: The group id
: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)
:rtype: dict
"""
@ -1319,7 +1017,9 @@ class KeycloakAdmin:
# For version +23.0.0
group = response.json()
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
@ -1349,7 +1049,7 @@ class KeycloakAdmin:
# went through the tree without hits
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.
Returns full group children details
@ -1358,15 +1058,32 @@ class KeycloakAdmin:
:type group_id: str
:param query: Additional query options
:type query: dict
:param full_hierarchy: If True, return all of the nested children groups
:type full_hierarchy: bool
:return: Keycloak server response (GroupRepresentation)
:rtype: dict
:raises ValueError: If both query and full_hierarchy parameters are used
"""
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}
url = urls_patterns.URL_ADMIN_GROUP_CHILD.format(**params_path)
if "first" in query or "max" in 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):
"""Get members by group id.
@ -4095,120 +3812,6 @@ class KeycloakAdmin:
)
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):
"""Get sessions associated with the client.
@ -4368,8 +3971,8 @@ class KeycloakAdmin:
:return: Keycloak server response
: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),
data=json.dumps(payload),
)

39
src/keycloak/keycloak_openid.py

@ -212,7 +212,7 @@ class KeycloakOpenID:
:type token: str
:param method_token_info: Token info method to use
:type method_token_info: str
:param kwargs: Additional keyword arguments
:param kwargs: Additional keyword arguments passed to the decode_token method
:type kwargs: dict
:returns: Token info
:rtype: dict
@ -516,7 +516,7 @@ class KeycloakOpenID:
data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload)
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.
A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data
@ -530,25 +530,30 @@ class KeycloakOpenID:
:param token: Keycloak token
: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
:returns: Decoded token
: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)
return jwt.json_decode(full_jwt.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)
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):
"""Load Keycloak settings (authorization).

78
tests/conftest.py

@ -4,7 +4,7 @@ import ipaddress
import os
import uuid
from datetime import datetime, timedelta
from typing import Tuple
from typing import Generator, Tuple
import freezegun
import pytest
@ -337,7 +337,69 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak
@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.
:param admin: Keycloak admin
@ -352,7 +414,7 @@ def realm(admin: KeycloakAdmin) -> str:
@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.
:param admin: Keycloak admin
@ -370,7 +432,7 @@ def user(admin: KeycloakAdmin, realm: str) -> str:
@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.
:param admin: Keycloak admin
@ -388,7 +450,7 @@ def group(admin: KeycloakAdmin, realm: str) -> str:
@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.
:param admin: Keycloak admin
@ -406,7 +468,7 @@ def client(admin: KeycloakAdmin, realm: str) -> str:
@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.
:param admin: Keycloak admin
@ -426,7 +488,9 @@ def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str:
@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.
:param admin: Keycloak admin

142
tests/test_keycloak_admin.py

@ -1,12 +1,14 @@
"""Test the keycloak admin object."""
import copy
import os
import uuid
from typing import Tuple
import freezegun
import pytest
from dateutil import parser as datetime_parser
from packaging.version import Version
import keycloak
from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakOpenIDConnection
@ -46,19 +48,21 @@ def test_keycloak_admin_init(env):
username=env.KEYCLOAK_ADMIN,
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 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(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
@ -67,7 +71,7 @@ def test_keycloak_admin_init(env):
realm_name=None,
user_realm_name="master",
)
assert admin.token
assert admin.connection.token
admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
username=env.KEYCLOAK_ADMIN,
@ -75,19 +79,19 @@ def test_keycloak_admin_init(env):
realm_name=None,
user_realm_name=None,
)
assert admin.token
assert admin.connection.token
token = admin.token
token = admin.connection.token
admin = KeycloakAdmin(
server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
token=token,
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.realm_name = "authz"
admin.connection.realm_name = "authz"
admin.create_client(
payload={
"name": "authz-client",
@ -107,7 +111,7 @@ def test_keycloak_admin_init(env):
user_realm_name="authz",
client_id="authz-client",
client_secret_key=secret["value"],
).token
).connection.token
admin.delete_realm(realm_name="authz")
assert (
@ -117,7 +121,7 @@ def test_keycloak_admin_init(env):
password=None,
client_secret_key=None,
custom_headers={"custom": "header"},
).token
).connection.token
is None
)
@ -130,7 +134,7 @@ def test_keycloak_admin_init(env):
verify=True,
)
keycloak_admin = KeycloakAdmin(connection=keycloak_connection)
assert keycloak_admin.token
assert keycloak_admin.connection.token
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"})
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
users = admin.get_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)
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):
"""Test users roles.
@ -745,6 +796,36 @@ def test_groups(admin: KeycloakAdmin, user: str):
assert res is not None, res
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
res = admin.get_subgroups(group=main_group, path="/none")
assert res is None, res
@ -816,6 +897,8 @@ def test_groups(admin: KeycloakAdmin, user: str):
# Test delete
res = admin.delete_group(group_id=group_id)
assert res == dict(), res
res = admin.delete_group(group_id=main_group_id_2)
assert res == dict(), res
assert len(admin.get_groups()) == 0
# 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")
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
res = admin.delete_realm_role(role_name=composite_role)
assert res == dict(), res
@ -1990,7 +2077,7 @@ def test_get_sessions(admin: KeycloakAdmin):
:param admin: Keycloak Admin client
: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
with pytest.raises(KeycloakGetError) as err:
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"}
with pytest.raises(KeycloakGetError) as err:
admin.realm_name = "doesnotexist"
admin.change_current_realm("doesnotexist")
admin.get_realm_default_roles()
assert err.match('404: b\'{"error":"Realm not found.".*}\'')
admin.change_current_realm(realm)
@ -2964,3 +3051,14 @@ def test_initial_access_token(
new_secret = str(uuid.uuid4())
res = oid.update_client(res["registrationAccessToken"], client, payload={"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
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]):
@ -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")
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
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.add_role(role="account/view-profile")
oid.authorization.policies["test"] = policy
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)"]
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)>"]
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")
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
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.add_role(role="account/view-profile")
policy.add_permission(
@ -408,15 +400,11 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str,
oid.authorization.policies["test"] = policy
assert [
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)"]
assert [
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)>"]
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())"
)
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="
+ "{'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
[testenv:docs]
commands_pre =
poetry install --no-root --sync -E docs
commands =
sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html
allowlist_externals = sphinx-build, poetry

Loading…
Cancel
Save