Browse Source

Merge pull request #687 from marcospereirampj/fix/pyright

fix: strong and consistent typing across the library
pull/689/head v7.0.0
Richard Nemeth 1 week ago
committed by GitHub
parent
commit
15c0565760
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 270
      poetry.lock
  2. 3
      pyproject.toml
  3. 2
      src/keycloak/authorization/permission.py
  4. 7
      src/keycloak/authorization/policy.py
  5. 2
      src/keycloak/authorization/role.py
  6. 150
      src/keycloak/connection.py
  7. 16
      src/keycloak/exceptions.py
  8. 4848
      src/keycloak/keycloak_admin.py
  9. 423
      src/keycloak/keycloak_openid.py
  10. 318
      src/keycloak/keycloak_uma.py
  11. 98
      src/keycloak/openid_connection.py
  12. 27
      src/keycloak/uma_permissions.py
  13. 16
      test_keycloak_init.sh
  14. 7
      tests/conftest.py
  15. 311
      tests/test_keycloak_admin.py
  16. 84
      tests/test_keycloak_openid.py
  17. 16
      tests/test_keycloak_uma.py
  18. 4
      tests/test_pkce_flow.py
  19. 35
      tests/test_uma_permissions.py

270
poetry.lock

@ -110,27 +110,15 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""}
[[package]]
name = "astroid"
version = "4.0.2"
version = "4.0.3"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.10.0"
groups = ["docs"]
markers = "python_version >= \"3.12\""
files = [
{file = "astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b"},
{file = "astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070"},
]
[[package]]
name = "async-property"
version = "0.2.2"
description = "Python decorator for async properties."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7"},
{file = "async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380"},
{file = "astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14"},
{file = "astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3"},
]
[[package]]
@ -215,14 +203,14 @@ files = [
[[package]]
name = "certifi"
version = "2025.11.12"
version = "2026.1.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main", "dev", "docs"]
files = [
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
]
[[package]]
@ -522,6 +510,7 @@ description = "Python commitizen client tool"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["dev"]
markers = "python_version < \"3.12\""
files = [
{file = "commitizen-4.10.1-py3-none-any.whl", hash = "sha256:ed4a377beed63aa4438f7ad5db791f66e117a5f597677a58b27a1c31e9f64fc4"},
{file = "commitizen-4.10.1.tar.gz", hash = "sha256:14d12252970463db2fa7c7e7e4753321190a093e7d5c99efcd1a63be73e3c1f8"},
@ -543,6 +532,33 @@ termcolor = ">=1.1.0,<4.0.0"
tomlkit = ">=0.8.0,<1.0.0"
typing-extensions = {version = ">=4.0.1,<5.0.0", markers = "python_version < \"3.11\""}
[[package]]
name = "commitizen"
version = "4.11.0"
description = "Python commitizen client tool"
optional = false
python-versions = "<4.0,>=3.10"
groups = ["dev"]
markers = "python_version >= \"3.12\""
files = [
{file = "commitizen-4.11.0-py3-none-any.whl", hash = "sha256:a30fdf326bb0913b7b25f8b30530c899159b1757efa9969b0aa1808dad102c02"},
{file = "commitizen-4.11.0.tar.gz", hash = "sha256:d311297a0165ef9f30e0877e04608b786d5fd69760f32245fbf1c21e793e91df"},
]
[package.dependencies]
argcomplete = ">=1.12.1,<3.7"
charset-normalizer = ">=2.1.0,<4"
colorama = ">=0.4.1,<1.0"
decli = ">=0.6.0,<1.0"
deprecated = ">=1.2.13,<2"
jinja2 = ">=2.10.3"
packaging = ">=19"
prompt_toolkit = "!=3.0.52"
pyyaml = ">=3.08"
questionary = ">=2.0,<3.0"
termcolor = ">=1.1.0,<4.0.0"
tomlkit = ">=0.8.0,<1.0.0"
[[package]]
name = "commonmark"
version = "0.9.1"
@ -681,105 +697,105 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "coverage"
version = "7.13.0"
version = "7.13.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
markers = "python_version >= \"3.12\""
files = [
{file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"},
{file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"},
{file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"},
{file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"},
{file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"},
{file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"},
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"},
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"},
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"},
{file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"},
{file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"},
{file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"},
{file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"},
{file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"},
{file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"},
{file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"},
{file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"},
{file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"},
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"},
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"},
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"},
{file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"},
{file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"},
{file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"},
{file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"},
{file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"},
{file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"},
{file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"},
{file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"},
{file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"},
{file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"},
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"},
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"},
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"},
{file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"},
{file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"},
{file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"},
{file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"},
{file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"},
{file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"},
{file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"},
{file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"},
{file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"},
{file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"},
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"},
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"},
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"},
{file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"},
{file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"},
{file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"},
{file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"},
{file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"},
{file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"},
{file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"},
{file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"},
{file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"},
{file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"},
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"},
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"},
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"},
{file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"},
{file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"},
{file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"},
{file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"},
{file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"},
{file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"},
{file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"},
{file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"},
{file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"},
{file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"},
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"},
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"},
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"},
{file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"},
{file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"},
{file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"},
{file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"},
{file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"},
{file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"},
{file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"},
{file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"},
{file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"},
{file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"},
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"},
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"},
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"},
{file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"},
{file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"},
{file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"},
{file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"},
{file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"},
{file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"},
{file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"},
{file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"},
{file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"},
{file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"},
{file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"},
{file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"},
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"},
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"},
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"},
{file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"},
{file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"},
{file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"},
{file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"},
{file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"},
{file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"},
{file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"},
{file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"},
{file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"},
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"},
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"},
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"},
{file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"},
{file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"},
{file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"},
{file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"},
{file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"},
{file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"},
{file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"},
{file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"},
{file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"},
{file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"},
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"},
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"},
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"},
{file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"},
{file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"},
{file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"},
{file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"},
{file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"},
{file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"},
{file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"},
{file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"},
{file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"},
{file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"},
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"},
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"},
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"},
{file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"},
{file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"},
{file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"},
{file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"},
{file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"},
{file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"},
{file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"},
{file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"},
{file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"},
{file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"},
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"},
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"},
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"},
{file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"},
{file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"},
{file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"},
{file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"},
{file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"},
{file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"},
{file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"},
{file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"},
{file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"},
{file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"},
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"},
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"},
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"},
{file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"},
{file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"},
{file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"},
{file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"},
{file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"},
{file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"},
{file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"},
{file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"},
{file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"},
{file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"},
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"},
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"},
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"},
{file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"},
{file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"},
{file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"},
{file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"},
{file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"},
{file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"},
]
[package.extras]
@ -1029,15 +1045,15 @@ files = [
[[package]]
name = "filelock"
version = "3.20.1"
version = "3.20.2"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
markers = "python_version >= \"3.12\""
files = [
{file = "filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a"},
{file = "filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c"},
{file = "filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8"},
{file = "filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64"},
]
[[package]]
@ -2574,15 +2590,15 @@ tests = ["pytest", "pytest-cov"]
[[package]]
name = "termcolor"
version = "3.2.0"
version = "3.3.0"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
markers = "python_version >= \"3.12\""
files = [
{file = "termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6"},
{file = "termcolor-3.2.0.tar.gz", hash = "sha256:610e6456feec42c4bcd28934a8c87a06c3fa28b01561d46aa09a9881b8622c58"},
{file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"},
{file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"},
]
[package.extras]
@ -2681,27 +2697,27 @@ virtualenv = ">=20.31.2"
[[package]]
name = "tox"
version = "4.32.0"
version = "4.33.0"
description = "tox is a generic virtualenv management and test command line tool"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
markers = "python_version >= \"3.12\""
files = [
{file = "tox-4.32.0-py3-none-any.whl", hash = "sha256:451e81dc02ba8d1ed20efd52ee409641ae4b5d5830e008af10fe8823ef1bd551"},
{file = "tox-4.32.0.tar.gz", hash = "sha256:1ad476b5f4d3679455b89a992849ffc3367560bbc7e9495ee8a3963542e7c8ff"},
{file = "tox-4.33.0-py3-none-any.whl", hash = "sha256:8582ac5c3ca97095ce88ae6bcd310d22614350ea9751b0e4ad39acad7874e270"},
{file = "tox-4.33.0.tar.gz", hash = "sha256:a29244bce3f514f94043e173366aa191c8cf0106ec8ddd18ba53f985acd73cc4"},
]
[package.dependencies]
cachetools = ">=6.2"
cachetools = ">=6.2.4"
chardet = ">=5.2"
colorama = ">=0.4.6"
filelock = ">=3.20"
filelock = ">=3.20.2"
packaging = ">=25"
platformdirs = ">=4.5"
platformdirs = ">=4.5.1"
pluggy = ">=1.6"
pyproject-api = ">=1.9.1"
virtualenv = ">=20.34"
pyproject-api = ">=1.10"
virtualenv = ">=20.35.4"
[[package]]
name = "twine"
@ -2953,5 +2969,5 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4.0"
content-hash = "7845cd09ab893d3657e7d87bd9483e046b89f0653a5967ac41c2bb2382b3601b"
python-versions = ">=3.9,<=3.14"
content-hash = "ea21a4329e72bca27f2ac739764bae06f6503c6bae18b31fa2c9282e2f99deff"

3
pyproject.toml

@ -31,13 +31,12 @@ Documentation = "https://python-keycloak.readthedocs.io/en/latest/"
"Issue tracker" = "https://github.com/marcospereirampj/python-keycloak/issues"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
python = ">=3.9,<=3.14"
requests = ">=2.20.0"
requests-toolbelt = ">=0.6.0"
deprecation = ">=2.1.0"
jwcrypto = ">=1.5.4"
httpx = ">=0.23.2"
async-property = ">=0.2.2"
aiofiles = ">=24.1.0"
[tool.poetry.group.docs.dependencies]

2
src/keycloak/authorization/permission.py

@ -133,7 +133,7 @@ class Permission:
return self._logic
@logic.setter
def logic(self, value: str) -> str:
def logic(self, value: str) -> None:
self._logic = value
@property

7
src/keycloak/authorization/policy.py

@ -24,6 +24,9 @@
from keycloak.exceptions import KeycloakAuthorizationConfigError
from .permission import Permission
from .role import Role
class Policy:
"""
@ -172,7 +175,7 @@ class Policy:
def permissions(self, value: list) -> None:
self._permissions = value
def add_role(self, role: dict) -> None:
def add_role(self, role: str | Role) -> None:
"""
Add keycloak role in policy.
@ -185,7 +188,7 @@ class Policy:
raise KeycloakAuthorizationConfigError(error_msg)
self._roles.append(role)
def add_permission(self, permission: dict) -> None:
def add_permission(self, permission: str | Permission) -> None:
"""
Add keycloak permission in policy.

2
src/keycloak/authorization/role.py

@ -61,7 +61,7 @@ class Role:
"""
return self.name
def __eq__(self, other: str | Role) -> bool:
def __eq__(self, other: object) -> bool:
"""
Eq method.

150
src/keycloak/connection.py

@ -27,13 +27,16 @@ from __future__ import annotations
try:
from urllib.parse import urljoin
except ImportError: # pragma: no cover
from urlparse import urljoin
from urlparse import urljoin # pyright: ignore[reportMissingImports]
from typing import Any
import httpx
import requests
from httpx import Response as AsyncResponse
from requests import Response
from requests.adapters import HTTPAdapter
from requests_toolbelt import MultipartEncoder
from .exceptions import KeycloakConnectionError
@ -67,8 +70,8 @@ class ConnectionManager:
self,
base_url: str,
headers: dict | None = None,
timeout: int = 60,
verify: bool = True,
timeout: int | None = 60,
verify: bool | str = True,
proxies: dict | None = None,
cert: str | tuple | None = None,
max_retries: int = 1,
@ -101,7 +104,9 @@ class ConnectionManager:
self.headers = headers
self.timeout = timeout
self.verify = verify
self.proxies = proxies
self.cert = cert
self.max_retries = max_retries
self.pool_maxsize = pool_maxsize
self._s = requests.Session()
self._s.auth = lambda x: x # don't let requests add auth headers
@ -112,9 +117,13 @@ class ConnectionManager:
adapter_kwargs = {"max_retries": max_retries}
if pool_maxsize is not None:
adapter_kwargs["pool_maxsize"] = pool_maxsize
adapter = HTTPAdapter(**adapter_kwargs)
adapter = HTTPAdapter(**adapter_kwargs) # pyright: ignore[reportArgumentType]
# adds POST to retry whitelist
allowed_methods = set(adapter.max_retries.allowed_methods)
allowed_methods = (
set(adapter.max_retries.allowed_methods)
if adapter.max_retries.allowed_methods
else set()
)
allowed_methods.add("POST")
adapter.max_retries.allowed_methods = frozenset(allowed_methods)
@ -132,8 +141,7 @@ class ConnectionManager:
max_keepalive_connections=20,
),
)
self.async_s.auth = None # don't let requests add auth headers
self.async_s.transport = httpx.AsyncHTTPTransport(retries=1)
self.async_s.auth = None # pyright: ignore[reportAttributeAccessIssue]
async def aclose(self) -> None:
"""Close the async connection on delete."""
@ -146,7 +154,7 @@ class ConnectionManager:
self._s.close()
@property
def base_url(self) -> str:
def base_url(self) -> str | None:
"""
Return base url in use for requests to the server.
@ -156,11 +164,11 @@ class ConnectionManager:
return self._base_url
@base_url.setter
def base_url(self, value: str) -> None:
def base_url(self, value: str | None) -> None:
self._base_url = value
@property
def timeout(self) -> int:
def timeout(self) -> int | None:
"""
Return timeout in use for request to the server.
@ -170,11 +178,11 @@ class ConnectionManager:
return self._timeout
@timeout.setter
def timeout(self, value: int) -> None:
def timeout(self, value: int | None) -> None:
self._timeout = value
@property
def verify(self) -> bool:
def verify(self) -> bool | str:
"""
Return verify in use for request to the server.
@ -184,11 +192,25 @@ class ConnectionManager:
return self._verify
@verify.setter
def verify(self, value: bool) -> None:
def verify(self, value: bool | str) -> None:
self._verify = value
@property
def cert(self) -> str | tuple:
def proxies(self) -> dict | None:
"""
Return proxies in use for request to the server.
:returns: Proxies
:rtype: dict | None
"""
return self._proxies
@proxies.setter
def proxies(self, value: dict | None) -> None:
self._proxies = value
@property
def cert(self) -> str | tuple | None:
"""
Return client certificates in use for request to the server.
@ -198,9 +220,23 @@ class ConnectionManager:
return self._cert
@cert.setter
def cert(self, value: str | tuple) -> None:
def cert(self, value: str | tuple | None) -> None:
self._cert = value
@property
def max_retries(self) -> int:
"""
Return maximum number of retries in use for requests to the server.
:returns: Maximum number of retries
:rtype: int
"""
return self._max_retries
@max_retries.setter
def max_retries(self, value: int) -> None:
self._max_retries = value
@property
def pool_maxsize(self) -> int | None:
"""
@ -216,7 +252,7 @@ class ConnectionManager:
self._pool_maxsize = value
@property
def headers(self) -> dict:
def headers(self) -> dict | None:
"""
Return header request to the server.
@ -226,7 +262,7 @@ class ConnectionManager:
return self._headers
@headers.setter
def headers(self, value: dict) -> None:
def headers(self, value: dict | None) -> None:
self._headers = value or {}
def param_headers(self, key: str) -> str | None:
@ -238,7 +274,7 @@ class ConnectionManager:
:returns: If the header parameters exist, return its value.
:rtype: str
"""
return self.headers.get(key)
return (self.headers or {}).get(key)
def clean_headers(self) -> None:
"""Clear header parameters."""
@ -264,6 +300,9 @@ class ConnectionManager:
:param value: Value to be added.
:type value: str
"""
if self.headers is None:
self.headers = {}
self.headers[key] = value
def del_param_headers(self, key: str) -> None:
@ -273,9 +312,12 @@ class ConnectionManager:
:param key: Key of the header parameters.
:type key: str
"""
if self.headers is None:
return
self.headers.pop(key, None)
def raw_get(self, path: str, **kwargs: dict) -> Response:
def raw_get(self, path: str, **kwargs: Any) -> Response: # noqa: ANN401
"""
Submit get request to the path.
@ -287,6 +329,9 @@ class ConnectionManager:
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform GET call with base_url missing."
raise AttributeError(msg)
try:
return self._s.get(
urljoin(self.base_url, path),
@ -300,20 +345,23 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
def raw_post(self, path: str, data: dict, **kwargs: dict) -> Response:
def raw_post(self, path: str, data: dict | str | MultipartEncoder, **kwargs: Any) -> Response: # noqa: ANN401
"""
Submit post request to the path.
:param path: Path for request.
:type path: str
:param data: Payload for request.
:type data: dict
:type data: dict | str | MultipartEncoder
:param kwargs: Additional arguments
:type kwargs: dict
:returns: Response the request.
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform POST call with base_url missing."
raise AttributeError(msg)
try:
return self._s.post(
urljoin(self.base_url, path),
@ -328,20 +376,24 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
def raw_put(self, path: str, data: dict, **kwargs: dict) -> Response:
def raw_put(self, path: str, data: dict | str | MultipartEncoder, **kwargs: Any) -> Response: # noqa: ANN401
"""
Submit put request to the path.
:param path: Path for request.
:type path: str
:param data: Payload for request.
:type data: dict
:type data: dict | str | MultipartEncoder
:param kwargs: Additional arguments
:type kwargs: dict
:returns: Response the request.
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform PUT call with base_url missing."
raise AttributeError(msg)
try:
return self._s.put(
urljoin(self.base_url, path),
@ -356,7 +408,7 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
def raw_delete(self, path: str, data: dict | None = None, **kwargs: dict) -> Response:
def raw_delete(self, path: str, data: dict | None = None, **kwargs: Any) -> Response: # noqa: ANN401
"""
Submit delete request to the path.
@ -370,6 +422,10 @@ class ConnectionManager:
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform DELETE call with base_url missing."
raise AttributeError(msg)
try:
return self._s.delete(
urljoin(self.base_url, path),
@ -384,7 +440,7 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
async def a_raw_get(self, path: str, **kwargs: dict) -> AsyncResponse:
async def a_raw_get(self, path: str, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
"""
Submit get request to the path.
@ -396,6 +452,10 @@ class ConnectionManager:
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform GET call with base_url missing."
raise AttributeError(msg)
try:
return await self.async_s.get(
urljoin(self.base_url, path),
@ -407,20 +467,29 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
async def a_raw_post(self, path: str, data: dict, **kwargs: dict) -> AsyncResponse:
async def a_raw_post(
self,
path: str,
data: dict | str | MultipartEncoder,
**kwargs: Any, # noqa: ANN401
) -> AsyncResponse:
"""
Submit post request to the path.
:param path: Path for request.
:type path: str
:param data: Payload for request.
:type data: dict
:type data: dict | str | MultipartEncoder
:param kwargs: Additional arguments
:type kwargs: dict
:returns: Response the request.
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform POST call with base_url missing."
raise AttributeError(msg)
try:
return await self.async_s.request(
method="POST",
@ -434,20 +503,29 @@ class ConnectionManager:
msg = "Can't connect to server"
raise KeycloakConnectionError(msg) from e
async def a_raw_put(self, path: str, data: dict, **kwargs: dict) -> AsyncResponse:
async def a_raw_put(
self,
path: str,
data: dict | str | MultipartEncoder,
**kwargs: Any, # noqa: ANN401
) -> AsyncResponse:
"""
Submit put request to the path.
:param path: Path for request.
:type path: str
:param data: Payload for request.
:type data: dict
:type data: dict | str | MultipartEncoder
:param kwargs: Additional arguments
:type kwargs: dict
:returns: Response the request.
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform PUT call with base_url missing."
raise AttributeError(msg)
try:
return await self.async_s.put(
urljoin(self.base_url, path),
@ -464,7 +542,7 @@ class ConnectionManager:
self,
path: str,
data: dict | None = None,
**kwargs: dict,
**kwargs: Any, # noqa: ANN401
) -> AsyncResponse:
"""
Submit delete request to the path.
@ -479,6 +557,10 @@ class ConnectionManager:
:rtype: Response
:raises KeycloakConnectionError: HttpError Can't connect to server.
"""
if self.base_url is None:
msg = "Unable to perform DELETE call with base_url missing."
raise AttributeError(msg)
try:
return await self.async_s.request(
method="DELETE",
@ -493,20 +575,24 @@ class ConnectionManager:
raise KeycloakConnectionError(msg) from e
@staticmethod
def _prepare_httpx_request_content(data: dict | str | None) -> dict:
def _prepare_httpx_request_content(data: dict | str | None | MultipartEncoder) -> dict:
"""
Create the correct request content kwarg to `httpx.AsyncClient.request()`.
See https://www.python-httpx.org/compatibility/#request-content
:param data: the request content
:type data: dict | str | None
:type data: dict | str | None | MultipartEncoder
:returns: A dict mapping the correct kwarg to the request content
:rtype: dict
"""
if isinstance(data, MultipartEncoder):
return {"content": data.to_string()}
if isinstance(data, str):
# Note: this could also accept bytes, Iterable[bytes], or AsyncIterable[bytes]
return {"content": data}
return {"data": data}
@staticmethod

16
src/keycloak/exceptions.py

@ -57,7 +57,7 @@ class KeycloakError(Exception):
def __init__(
self,
error_message: str = "",
error_message: str | bytes = "",
response_code: int | None = None,
response_body: bytes | None = None,
) -> None:
@ -147,7 +147,15 @@ class PermissionDefinitionError(Exception):
def raise_error_from_response(
response: Response | AsyncResponse,
error: dict | Exception,
error: type[
KeycloakGetError
| KeycloakPostError
| KeycloakDeprecationError
| KeycloakPutError
| KeycloakDeleteError
]
| dict
| Exception,
expected_codes: list[int] | None = None,
skip_exists: bool = False,
) -> bytes | dict | list:
@ -190,9 +198,9 @@ def raise_error_from_response(
if isinstance(error, dict):
error = error.get(response.status_code, KeycloakOperationError)
elif response.status_code == HTTP_UNAUTHORIZED:
error = KeycloakAuthenticationError
error = KeycloakAuthenticationError # pyright: ignore[reportAssignmentType]
raise error(
raise error( # pyright: ignore[reportCallIssue]
error_message=message,
response_code=response.status_code,
response_body=response.content,

4848
src/keycloak/keycloak_admin.py
File diff suppressed because it is too large
View File

423
src/keycloak/keycloak_openid.py

@ -31,6 +31,7 @@ from __future__ import annotations
import json
import pathlib
from typing import Any
import aiofiles
from jwcrypto import jwk, jwt
@ -100,7 +101,7 @@ class KeycloakOpenID:
verify: bool | str = True,
custom_headers: dict | None = None,
proxies: dict | None = None,
timeout: int = 60,
timeout: int | None = 60,
cert: str | tuple | None = None,
max_retries: int = 1,
pool_maxsize: int | None = None,
@ -166,7 +167,7 @@ class KeycloakOpenID:
self._client_id = value
@property
def client_secret_key(self) -> str:
def client_secret_key(self) -> str | None:
"""
Get the client secret key.
@ -176,7 +177,7 @@ class KeycloakOpenID:
return self._client_secret_key
@client_secret_key.setter
def client_secret_key(self, value: str) -> None:
def client_secret_key(self, value: str | None) -> None:
self._client_secret_key = value
@property
@ -246,7 +247,7 @@ class KeycloakOpenID:
"""
return self.client_id + "/" + role
def _token_info(self, token: str, method_token_info: str, **kwargs: dict) -> dict:
def _token_info(self, token: str, method_token_info: str, **kwargs: Any) -> dict: # noqa: ANN401
"""
Getter for the token data.
@ -279,7 +280,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
f"Unexpected response type on well_known. Expected 'dict', received '{type(res)}'"
f", value: {res}"
)
raise TypeError(msg)
return res
def auth_url(
self,
@ -325,15 +334,15 @@ class KeycloakOpenID:
def token(
self,
username: str = "",
password: str = "",
username: str | None = "",
password: str | None = "",
grant_type: str = "password",
code: str = "",
redirect_uri: str = "",
totp: int | None = None,
scope: str = "openid",
code_verifier: str | None = None,
**extra: dict,
**extra: Any, # noqa: ANN401
) -> dict:
"""
Retrieve user token.
@ -385,7 +394,7 @@ class KeycloakOpenID:
payload["totp"] = totp
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -393,7 +402,15 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
f"Unexpected response type from 'token'. Expected 'dict', received '{type(res)}'"
f", value {res}."
)
raise TypeError(msg)
return res
def refresh_token(self, refresh_token: str, grant_type: str = "refresh_token") -> dict:
"""
@ -420,7 +437,7 @@ class KeycloakOpenID:
"refresh_token": refresh_token,
}
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -428,7 +445,16 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type from refresh_token. "
f"Expected 'dict', received '{type(res)}'"
f", value: {res}."
)
raise TypeError(msg)
return res
def exchange_token(
self,
@ -480,7 +506,7 @@ class KeycloakOpenID:
"scope": scope,
}
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -488,7 +514,15 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type from exchange_token. Expected 'dict', received "
f"'{type(res)}', value '{res}'"
)
raise TypeError(msg)
return res
def userinfo(self, token: str) -> dict:
"""
@ -504,7 +538,7 @@ class KeycloakOpenID:
:returns: Userinfo object
:rtype: dict
"""
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path))
@ -513,26 +547,42 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type from userinfo. Expected 'dict', "
f"received '{type(res)}', value: '{res}'."
)
raise TypeError(msg)
def logout(self, refresh_token: str) -> bytes:
return res
def logout(self, refresh_token: str) -> dict:
"""
Log out the authenticated user.
:param refresh_token: Refresh token from Keycloak
:type refresh_token: str
:returns: Keycloak server response
:rtype: bytes
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
payload = {"client_id": self.client_id, "refresh_token": refresh_token}
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPostError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type from logout. Expected 'dict', "
f"received '{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def certs(self) -> dict:
"""
@ -549,7 +599,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_CERTS.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type from certs. Expected 'dict', "
f"received '{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def public_key(self) -> str:
"""
@ -562,7 +620,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = self.connection.raw_get(URL_REALM.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)["public_key"]
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type from public_key. Expected 'dict', "
f"received '{type(res)}', value '{res}'"
)
raise TypeError(msg)
return res["public_key"]
def entitlement(self, token: str, resource_server_id: str) -> dict:
"""
@ -581,7 +647,7 @@ class KeycloakOpenID:
:returns: Entitlements
:rtype: dict
"""
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id}
data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path))
@ -592,9 +658,24 @@ class KeycloakOpenID:
)
if data_raw.status_code in {HTTP_NOT_FOUND, HTTP_NOT_ALLOWED}:
return raise_error_from_response(data_raw, KeycloakDeprecationError)
res = raise_error_from_response(data_raw, KeycloakDeprecationError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', "
f"received '{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', "
f"received '{type(res)}', value '{res}'."
)
raise TypeError(msg)
return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover
return res
def introspect(
self,
@ -629,7 +710,7 @@ class KeycloakOpenID:
if token_type_hint == "requesting_party_token": # noqa: S105
if rpt:
payload.update({"token": rpt, "token_type_hint": token_type_hint})
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
bearer_changed = True
else:
@ -645,10 +726,19 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
@staticmethod
def _verify_token(token: str, key: jwk.JWK | jwk.JWKSet | None, **kwargs: dict) -> dict:
def _verify_token(token: str, key: jwk.JWK | jwk.JWKSet | None, **kwargs: Any) -> dict: # noqa: ANN401
"""
Decode and optionally validate a token.
@ -669,12 +759,13 @@ class KeycloakOpenID:
full_jwt = jwt.JWT(jwt=token, **kwargs)
full_jwt.leeway = leeway
full_jwt.validate(key)
return jwt.json_decode(full_jwt.claims)
return jwt.json_decode(full_jwt.claims) # pyright: ignore[reportAttributeAccessIssue]
full_jwt = jwt.JWT(jwt=token, **kwargs)
full_jwt.token.objects["valid"] = True
return json.loads(full_jwt.token.payload.decode("utf-8"))
def decode_token(self, token: str, validate: bool = True, **kwargs: dict) -> dict:
def decode_token(self, token: str, validate: bool = True, **kwargs: Any) -> dict: # noqa: ANN401
"""
Decode user token.
@ -727,8 +818,8 @@ class KeycloakOpenID:
self,
token: str,
method_token_info: str = "introspect", # noqa: S107
**kwargs: dict,
) -> list:
**kwargs: Any, # noqa: ANN401
) -> list | None:
"""
Get policies by user token.
@ -771,8 +862,8 @@ class KeycloakOpenID:
self,
token: str,
method_token_info: str = "introspect", # noqa: S107
**kwargs: dict,
) -> list:
**kwargs: Any, # noqa: ANN401
) -> list | None:
"""
Get permission by user token.
@ -811,7 +902,7 @@ class KeycloakOpenID:
return list(set(permissions))
def uma_permissions(self, token: str, permissions: str = "", **extra_payload: dict) -> list:
def uma_permissions(self, token: str, permissions: str = "", **extra_payload: Any) -> list: # noqa: ANN401
"""
Get UMA permissions by user token with requested permissions.
@ -840,9 +931,9 @@ class KeycloakOpenID:
**extra_payload,
}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -855,9 +946,17 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
def has_uma_access(self, token: str, permissions: list) -> AuthStatus:
return res
def has_uma_access(self, token: str, permissions: str) -> AuthStatus:
"""
Determine whether user has uma permissions with specified user token.
@ -918,9 +1017,9 @@ class KeycloakOpenID:
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
orig_content_type = self.connection.headers.get("Content-Type")
orig_content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/json")
data_raw = self.connection.raw_post(
URL_CLIENT_REGISTRATION.format(**params_path),
@ -936,7 +1035,15 @@ class KeycloakOpenID:
if orig_content_type is not None
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def device(self, scope: str = "") -> dict:
"""
@ -964,9 +1071,17 @@ class KeycloakOpenID:
payload = self._add_secret_key(payload)
data_raw = self.connection.raw_post(URL_DEVICE.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def update_client(self, token: str, client_id: str, payload: dict) -> bytes:
def update_client(self, token: str, client_id: str, payload: dict) -> dict:
"""
Update a client.
@ -983,9 +1098,9 @@ class KeycloakOpenID:
:rtype: bytes
"""
params_path = {"realm-name": self.realm_name, "client-id": client_id}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
orig_content_type = self.connection.headers.get("Content-Type")
orig_content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/json")
# Keycloak complains if the clientId is not set in the payload
@ -1006,9 +1121,17 @@ class KeycloakOpenID:
if orig_content_type is not None
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPutError)
res = raise_error_from_response(data_raw, KeycloakPutError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def _a_token_info(self, token: str, method_token_info: str, **kwargs: dict) -> dict:
async def _a_token_info(self, token: str, method_token_info: str, **kwargs: Any) -> dict: # noqa: ANN401
"""
Asynchronous getter for the token data.
@ -1041,7 +1164,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = await self.connection.a_raw_get(URL_WELL_KNOWN.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_auth_url(
self,
@ -1087,15 +1218,15 @@ class KeycloakOpenID:
async def a_token(
self,
username: str = "",
password: str = "",
username: str | None = "",
password: str | None = "",
grant_type: str = "password",
code: str = "",
redirect_uri: str = "",
totp: int | None = None,
scope: str = "openid",
code_verifier: str | None = None,
**extra: dict,
**extra: Any, # noqa: ANN401
) -> dict:
"""
Retrieve user token asynchronously.
@ -1147,7 +1278,7 @@ class KeycloakOpenID:
payload["totp"] = totp
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = await self.connection.a_raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -1155,7 +1286,15 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_refresh_token(self, refresh_token: str, grant_type: str = "refresh_token") -> dict:
"""
@ -1182,7 +1321,7 @@ class KeycloakOpenID:
"refresh_token": refresh_token,
}
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = await self.connection.a_raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -1190,7 +1329,15 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_exchange_token(
self,
@ -1242,7 +1389,7 @@ class KeycloakOpenID:
"scope": scope,
}
payload = self._add_secret_key(payload)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = await self.connection.a_raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -1250,7 +1397,15 @@ class KeycloakOpenID:
if content_type
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_userinfo(self, token: str) -> dict:
"""
@ -1266,7 +1421,7 @@ class KeycloakOpenID:
:returns: Userinfo object
:rtype: dict
"""
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name}
data_raw = await self.connection.a_raw_get(URL_USERINFO.format(**params_path))
@ -1275,26 +1430,42 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
async def a_logout(self, refresh_token: str) -> bytes:
return res
async def a_logout(self, refresh_token: str) -> dict:
"""
Log out the authenticated user asynchronously.
:param refresh_token: Refresh token from Keycloak
:type refresh_token: str
:returns: Keycloak server response
:rtype: bytes
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
payload = {"client_id": self.client_id, "refresh_token": refresh_token}
payload = self._add_secret_key(payload)
data_raw = await self.connection.a_raw_post(URL_LOGOUT.format(**params_path), data=payload)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPostError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_certs(self) -> dict:
"""
@ -1311,7 +1482,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = await self.connection.a_raw_get(URL_CERTS.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_public_key(self) -> str:
"""
@ -1324,7 +1503,15 @@ class KeycloakOpenID:
"""
params_path = {"realm-name": self.realm_name}
data_raw = await self.connection.a_raw_get(URL_REALM.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)["public_key"]
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res["public_key"]
async def a_entitlement(self, token: str, resource_server_id: str) -> dict:
"""
@ -1343,7 +1530,7 @@ class KeycloakOpenID:
:returns: Entitlements
:rtype: dict
"""
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id}
data_raw = await self.connection.a_raw_get(URL_ENTITLEMENT.format(**params_path))
@ -1354,9 +1541,25 @@ class KeycloakOpenID:
)
if data_raw.status_code in [HTTP_NOT_FOUND, HTTP_NOT_ALLOWED]:
return raise_error_from_response(data_raw, KeycloakDeprecationError)
res = raise_error_from_response(data_raw, KeycloakDeprecationError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_introspect(
self,
@ -1391,7 +1594,7 @@ class KeycloakOpenID:
if token_type_hint == "requesting_party_token": # noqa: S105
if rpt:
payload.update({"token": rpt, "token_type_hint": token_type_hint})
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
bearer_changed = True
else:
@ -1410,9 +1613,17 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
async def a_decode_token(self, token: str, validate: bool = True, **kwargs: dict) -> dict:
return res
async def a_decode_token(self, token: str, validate: bool = True, **kwargs: Any) -> dict: # noqa: ANN401
"""
Decode user token asynchronously.
@ -1465,8 +1676,8 @@ class KeycloakOpenID:
self,
token: str,
method_token_info: str = "introspect", # noqa: S107
**kwargs: dict,
) -> list:
**kwargs: Any, # noqa: ANN401
) -> list | None:
"""
Get policies by user token asynchronously.
@ -1477,7 +1688,7 @@ class KeycloakOpenID:
:param kwargs: Additional keyword arguments
:type kwargs: dict
:return: Policies
:rtype: list
:rtype: list | None
:raises KeycloakAuthorizationConfigError: In case of bad authorization configuration
:raises KeycloakInvalidTokenError: In case of bad token
"""
@ -1509,8 +1720,8 @@ class KeycloakOpenID:
self,
token: str,
method_token_info: str = "introspect", # noqa: S107
**kwargs: dict,
) -> list:
**kwargs: Any, # noqa: ANN401
) -> list | None:
"""
Get permission by user token asynchronously.
@ -1521,7 +1732,7 @@ class KeycloakOpenID:
:param kwargs: parameters for decode
:type kwargs: dict
:returns: permissions list
:rtype: list
:rtype: list | None
:raises KeycloakAuthorizationConfigError: In case of bad authorization configuration
:raises KeycloakInvalidTokenError: In case of bad token
"""
@ -1552,7 +1763,7 @@ class KeycloakOpenID:
self,
token: str,
permissions: str = "",
**extra_payload: dict,
**extra_payload: Any, # noqa: ANN401
) -> list:
"""
Get UMA permissions by user token with requested permissions asynchronously.
@ -1582,9 +1793,9 @@ class KeycloakOpenID:
**extra_payload,
}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
content_type = self.connection.headers.get("Content-Type")
content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = await self.connection.a_raw_post(URL_TOKEN.format(**params_path), data=payload)
(
@ -1597,9 +1808,17 @@ class KeycloakOpenID:
if orig_bearer is not None
else self.connection.del_param_headers("Authorization")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
async def a_has_uma_access(self, token: str, permissions: list) -> AuthStatus:
return res
async def a_has_uma_access(self, token: str, permissions: str) -> AuthStatus:
"""
Determine whether user has uma permissions with specified user token asynchronously.
@ -1660,9 +1879,9 @@ class KeycloakOpenID:
:rtype: dict
"""
params_path = {"realm-name": self.realm_name}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
orig_content_type = self.connection.headers.get("Content-Type")
orig_content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/json")
data_raw = await self.connection.a_raw_post(
URL_CLIENT_REGISTRATION.format(**params_path),
@ -1678,7 +1897,15 @@ class KeycloakOpenID:
if orig_content_type is not None
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_device(self, scope: str = "") -> dict:
"""
@ -1706,9 +1933,17 @@ class KeycloakOpenID:
payload = self._add_secret_key(payload)
data_raw = await self.connection.a_raw_post(URL_DEVICE.format(**params_path), data=payload)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_update_client(self, token: str, client_id: str, payload: dict) -> bytes:
async def a_update_client(self, token: str, client_id: str, payload: dict) -> dict:
"""
Update a client asynchronously.
@ -1722,12 +1957,12 @@ class KeycloakOpenID:
:param payload: ClientRepresentation
:type payload: dict
:return: Client Representation
:rtype: bytes
:rtype: dict
"""
params_path = {"realm-name": self.realm_name, "client-id": client_id}
orig_bearer = self.connection.headers.get("Authorization")
orig_bearer = (self.connection.headers or {}).get("Authorization")
self.connection.add_param_headers("Authorization", "Bearer " + token)
orig_content_type = self.connection.headers.get("Content-Type")
orig_content_type = (self.connection.headers or {}).get("Content-Type")
self.connection.add_param_headers("Content-Type", "application/json")
# Keycloak complains if the clientId is not set in the payload
@ -1748,4 +1983,12 @@ class KeycloakOpenID:
if orig_content_type is not None
else self.connection.del_param_headers("Content-Type")
)
return raise_error_from_response(data_raw, KeycloakPutError)
res = raise_error_from_response(data_raw, KeycloakPutError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res

318
src/keycloak/keycloak_uma.py

@ -30,11 +30,9 @@ https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from urllib.parse import quote_plus
from async_property import async_property
from .connection import ConnectionManager
from .exceptions import (
HTTP_CREATED,
@ -49,7 +47,7 @@ from .exceptions import (
from .urls_patterns import URL_UMA_WELL_KNOWN
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import AsyncGenerator, Generator, Iterable
from .openid_connection import KeycloakOpenIDConnection
from .uma_permissions import UMAPermission
@ -75,10 +73,18 @@ class KeycloakUMA:
def _fetch_well_known(self) -> dict:
params_path = {"realm-name": self.connection.realm_name}
data_raw = self.connection.raw_get(URL_UMA_WELL_KNOWN.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
@staticmethod
def format_url(url: str, **kwargs: dict) -> str:
def format_url(url: str, **kwargs: Any) -> str: # noqa: ANN401
"""
Substitute url path parameters.
@ -97,7 +103,7 @@ class KeycloakUMA:
return url.format(**{k: quote_plus(v) for k, v in kwargs.items()})
@staticmethod
async def a_format_url(url: str, **kwargs: dict) -> str:
async def a_format_url(url: str, **kwargs: Any) -> str: # noqa: ANN401
"""
Substitute url path parameters.
@ -129,7 +135,7 @@ class KeycloakUMA:
return self._well_known
@async_property
@property
async def a_uma_well_known(self) -> dict:
"""
Get the well_known UMA2 config async.
@ -142,7 +148,7 @@ class KeycloakUMA:
return self._well_known
def resource_set_create(self, payload: dict) -> dict | bytes:
def resource_set_create(self, payload: dict) -> dict:
"""
Create a resource set.
@ -161,13 +167,21 @@ class KeycloakUMA:
self.uma_well_known["resource_registration_endpoint"],
data=json.dumps(payload),
)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPostError,
expected_codes=[HTTP_CREATED],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
def resource_set_update(self, resource_id: str, payload: dict) -> bytes:
return res
def resource_set_update(self, resource_id: str, payload: dict) -> dict:
"""
Update a resource set.
@ -189,11 +203,19 @@ class KeycloakUMA:
id=resource_id,
)
data_raw = self.connection.raw_put(url, data=json.dumps(payload))
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPutError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def resource_set_read(self, resource_id: str) -> dict:
"""
@ -215,9 +237,17 @@ class KeycloakUMA:
id=resource_id,
)
data_raw = self.connection.raw_get(url)
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
res = raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
def resource_set_delete(self, resource_id: str) -> bytes:
return res
def resource_set_delete(self, resource_id: str) -> dict:
"""
Delete a resource set.
@ -234,11 +264,19 @@ class KeycloakUMA:
id=resource_id,
)
data_raw = self.connection.raw_delete(url)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakDeleteError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def resource_set_list_ids(
self,
@ -303,9 +341,17 @@ class KeycloakUMA:
self.uma_well_known["resource_registration_endpoint"],
**query,
)
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
res = raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def resource_set_list(self) -> list:
def resource_set_list(self) -> Generator[dict, Any, Any]:
"""
List all resource sets.
@ -363,13 +409,21 @@ class KeycloakUMA:
self.uma_well_known["permission_endpoint"],
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def permissions_check(
self,
token: str,
permissions: Iterable[UMAPermission],
**extra_payload: dict,
**extra_payload: Any, # noqa: ANN401
) -> bool:
"""
Check UMA permissions by user token with requested permissions.
@ -401,7 +455,21 @@ class KeycloakUMA:
if len(payload["permission"]) == 0:
return True
connection = ConnectionManager(self.connection.base_url)
if self.connection.base_url is None:
msg = (
"Unable to perform permission check without base_url set on the connection object."
)
raise AttributeError(msg)
connection = ConnectionManager(
base_url=self.connection.base_url,
timeout=self.connection.timeout,
verify=self.connection.verify,
proxies=self.connection.proxies,
cert=self.connection.cert,
max_retries=self.connection.max_retries,
pool_maxsize=self.connection.pool_maxsize,
)
connection.add_param_headers("Authorization", "Bearer " + token)
connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = connection.raw_post(self.uma_well_known["token_endpoint"], data=payload)
@ -409,6 +477,14 @@ class KeycloakUMA:
data = raise_error_from_response(data_raw, KeycloakPostError)
except KeycloakPostError:
return False
if not isinstance(data, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(data)}', value '{data}'."
)
raise TypeError(msg)
return data.get("result", False)
def policy_resource_create(self, resource_id: str, payload: dict) -> dict:
@ -430,9 +506,17 @@ class KeycloakUMA:
self.uma_well_known["policy_endpoint"] + f"/{resource_id}",
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def policy_update(self, policy_id: str, payload: dict) -> dict:
def policy_update(self, policy_id: str, payload: dict) -> bytes:
"""
Update permission policy.
@ -444,13 +528,21 @@ class KeycloakUMA:
:param payload: policy permission configuration
:type payload: dict
:return: PermissionRepresentation
:rtype: dict
:rtype: bytes
"""
data_raw = self.connection.raw_put(
self.uma_well_known["policy_endpoint"] + f"/{policy_id}",
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPutError)
res = raise_error_from_response(data_raw, KeycloakPutError)
if not isinstance(res, bytes):
msg = (
"Unexpected response type. Expected 'bytes', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def policy_delete(self, policy_id: str) -> dict:
"""
@ -467,7 +559,15 @@ class KeycloakUMA:
data_raw = self.connection.raw_delete(
self.uma_well_known["policy_endpoint"] + f"/{policy_id}",
)
return raise_error_from_response(data_raw, KeycloakDeleteError)
res = raise_error_from_response(data_raw, KeycloakDeleteError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
def policy_query(
self,
@ -509,7 +609,18 @@ class KeycloakUMA:
query["max"] = maximum
data_raw = self.connection.raw_get(self.uma_well_known["policy_endpoint"], **query)
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if isinstance(res, dict) and res == {}:
return []
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a__fetch_well_known(self) -> dict:
"""
@ -520,7 +631,15 @@ class KeycloakUMA:
"""
params_path = {"realm-name": self.connection.realm_name}
data_raw = await self.connection.a_raw_get(URL_UMA_WELL_KNOWN.format(**params_path))
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_resource_set_create(self, payload: dict) -> dict:
"""
@ -541,13 +660,21 @@ class KeycloakUMA:
(await self.a_uma_well_known)["resource_registration_endpoint"],
data=json.dumps(payload),
)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPostError,
expected_codes=[HTTP_CREATED],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_resource_set_update(self, resource_id: str, payload: dict) -> bytes:
async def a_resource_set_update(self, resource_id: str, payload: dict) -> dict:
"""
Update a resource set asynchronously.
@ -562,18 +689,26 @@ class KeycloakUMA:
:param payload: ResourceRepresentation
:type payload: dict
:return: Response dict (empty)
:rtype: bytes
:rtype: dict
"""
url = self.format_url(
(await self.a_uma_well_known)["resource_registration_endpoint"] + "/{id}",
id=resource_id,
)
data_raw = await self.connection.a_raw_put(url, data=json.dumps(payload))
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakPutError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_resource_set_read(self, resource_id: str) -> dict:
"""
@ -595,9 +730,17 @@ class KeycloakUMA:
id=resource_id,
)
data_raw = await self.connection.a_raw_get(url)
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
res = raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
async def a_resource_set_delete(self, resource_id: str) -> bytes:
return res
async def a_resource_set_delete(self, resource_id: str) -> dict:
"""
Delete a resource set asynchronously.
@ -607,18 +750,26 @@ class KeycloakUMA:
:param resource_id: id of the resource
:type resource_id: str
:return: Response dict (empty)
:rtype: bytes
:rtype: dict
"""
url = self.format_url(
(await self.a_uma_well_known)["resource_registration_endpoint"] + "/{id}",
id=resource_id,
)
data_raw = await self.connection.a_raw_delete(url)
return raise_error_from_response(
res = raise_error_from_response(
data_raw,
KeycloakDeleteError,
expected_codes=[HTTP_NO_CONTENT],
)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_resource_set_list_ids(
self,
@ -683,9 +834,17 @@ class KeycloakUMA:
(await self.a_uma_well_known)["resource_registration_endpoint"],
**query,
)
return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
res = raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[HTTP_OK])
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_resource_set_list(self) -> list:
async def a_resource_set_list(self) -> AsyncGenerator[dict, Any]:
"""
List all resource sets asynchronously.
@ -702,7 +861,7 @@ class KeycloakUMA:
resource = await self.a_resource_set_read(resource_id)
yield resource
async def a_permission_ticket_create(self, permissions: Iterable[UMAPermission]) -> bool:
async def a_permission_ticket_create(self, permissions: Iterable[UMAPermission]) -> dict:
"""
Create a permission ticket asynchronously.
@ -743,13 +902,21 @@ class KeycloakUMA:
(await self.a_uma_well_known)["permission_endpoint"],
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_permissions_check(
self,
token: str,
permissions: Iterable[UMAPermission],
**extra_payload: dict,
**extra_payload: Any, # noqa: ANN401
) -> bool:
"""
Check UMA permissions by user token with requested permissions asynchronously.
@ -781,7 +948,21 @@ class KeycloakUMA:
if len(payload["permission"]) == 0:
return True
connection = ConnectionManager(self.connection.base_url)
if self.connection.base_url is None:
msg = (
"Unable to perform permission check without base_url set on the connection object."
)
raise AttributeError(msg)
connection = ConnectionManager(
base_url=self.connection.base_url,
timeout=self.connection.timeout,
verify=self.connection.verify,
proxies=self.connection.proxies,
cert=self.connection.cert,
max_retries=self.connection.max_retries,
pool_maxsize=self.connection.pool_maxsize,
)
connection.add_param_headers("Authorization", "Bearer " + token)
connection.add_param_headers("Content-Type", "application/x-www-form-urlencoded")
data_raw = await connection.a_raw_post(
@ -792,6 +973,14 @@ class KeycloakUMA:
data = raise_error_from_response(data_raw, KeycloakPostError)
except KeycloakPostError:
return False
if not isinstance(data, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(data)}', value '{data}'."
)
raise TypeError(msg)
return data.get("result", False)
async def a_policy_resource_create(self, resource_id: str, payload: dict) -> dict:
@ -813,9 +1002,17 @@ class KeycloakUMA:
(await self.a_uma_well_known)["policy_endpoint"] + f"/{resource_id}",
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPostError)
res = raise_error_from_response(data_raw, KeycloakPostError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_policy_update(self, policy_id: str, payload: dict) -> dict:
async def a_policy_update(self, policy_id: str, payload: dict) -> bytes:
"""
Update permission policy asynchronously.
@ -827,13 +1024,21 @@ class KeycloakUMA:
:param payload: policy permission configuration
:type payload: dict
:return: PermissionRepresentation
:rtype: dict
:rtype: bytes
"""
data_raw = await self.connection.a_raw_put(
(await self.a_uma_well_known)["policy_endpoint"] + f"/{policy_id}",
data=json.dumps(payload),
)
return raise_error_from_response(data_raw, KeycloakPutError)
res = raise_error_from_response(data_raw, KeycloakPutError)
if not isinstance(res, bytes):
msg = (
"Unexpected response type. Expected 'bytes', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_policy_delete(self, policy_id: str) -> dict:
"""
@ -850,7 +1055,15 @@ class KeycloakUMA:
data_raw = await self.connection.a_raw_delete(
(await self.a_uma_well_known)["policy_endpoint"] + f"/{policy_id}",
)
return raise_error_from_response(data_raw, KeycloakDeleteError)
res = raise_error_from_response(data_raw, KeycloakDeleteError)
if not isinstance(res, dict):
msg = (
"Unexpected response type. Expected 'dict', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res
async def a_policy_query(
self,
@ -895,4 +1108,15 @@ class KeycloakUMA:
(await self.a_uma_well_known)["policy_endpoint"],
**query,
)
return raise_error_from_response(data_raw, KeycloakGetError)
res = raise_error_from_response(data_raw, KeycloakGetError)
if isinstance(res, dict) and res == {}:
return []
if not isinstance(res, list):
msg = (
"Unexpected response type. Expected 'list', received "
f"'{type(res)}', value '{res}'."
)
raise TypeError(msg)
return res

98
src/keycloak/openid_connection.py

@ -31,7 +31,7 @@ of openid tokens when required.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from httpx import Response as AsyncResponse
@ -67,13 +67,13 @@ class KeycloakOpenIDConnection(ConnectionManager):
def __init__(
self,
server_url: str,
server_url: str | None = None,
grant_type: str | None = None,
username: str | None = None,
password: str | None = None,
token: str | None = None,
totp: str | None = None,
realm_name: str = "master",
token: dict | None = None,
totp: int | None = None,
realm_name: str | None = "master",
client_id: str = "admin-cli",
verify: str | bool = True,
client_secret_key: str | None = None,
@ -150,6 +150,10 @@ class KeycloakOpenIDConnection(ConnectionManager):
elif client_secret_key:
self.grant_type = "client_credentials"
if self.server_url is None:
msg = "Unable to initialize KeycloakOpenIDConnection without server_url."
raise ValueError(msg)
super().__init__(
base_url=self.server_url,
headers=self.headers,
@ -161,7 +165,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
)
@property
def server_url(self) -> str:
def server_url(self) -> str | None:
"""
Get server url.
@ -171,11 +175,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self.base_url
@server_url.setter
def server_url(self, value: str) -> None:
def server_url(self, value: str | None) -> None:
self.base_url = value
@property
def grant_type(self) -> str:
def grant_type(self) -> str | None:
"""
Get grant type.
@ -185,11 +189,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._grant_type
@grant_type.setter
def grant_type(self, value: str) -> None:
def grant_type(self, value: str | None) -> None:
self._grant_type = value
@property
def realm_name(self) -> str:
def realm_name(self) -> str | None:
"""
Get realm name.
@ -199,11 +203,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._realm_name
@realm_name.setter
def realm_name(self, value: str) -> None:
def realm_name(self, value: str | None) -> None:
self._realm_name = value
@property
def client_id(self) -> str:
def client_id(self) -> str | None:
"""
Get client id.
@ -213,11 +217,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._client_id
@client_id.setter
def client_id(self, value: str) -> None:
def client_id(self, value: str | None) -> None:
self._client_id = value
@property
def client_secret_key(self) -> str:
def client_secret_key(self) -> str | None:
"""
Get client secret key.
@ -227,11 +231,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._client_secret_key
@client_secret_key.setter
def client_secret_key(self, value: str) -> None:
def client_secret_key(self, value: str | None) -> None:
self._client_secret_key = value
@property
def username(self) -> str:
def username(self) -> str | None:
"""
Get username.
@ -241,11 +245,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._username
@username.setter
def username(self, value: str) -> None:
def username(self, value: str | None) -> None:
self._username = value
@property
def password(self) -> str:
def password(self) -> str | None:
"""
Get password.
@ -255,11 +259,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._password
@password.setter
def password(self, value: str) -> None:
def password(self, value: str | None) -> None:
self._password = value
@property
def totp(self) -> str:
def totp(self) -> int | None:
"""
Get totp.
@ -269,11 +273,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._totp
@totp.setter
def totp(self, value: str) -> None:
def totp(self, value: int | None) -> None:
self._totp = value
@property
def token(self) -> dict:
def token(self) -> dict | None:
"""
Get token.
@ -283,16 +287,16 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._token
@token.setter
def token(self, value: dict) -> None:
def token(self, value: dict | None) -> None:
self._token = value
self._expires_at = datetime.now(tz=timezone.utc) + timedelta(
seconds=int(self.token_lifetime_fraction * self.token["expires_in"] if value else 0),
seconds=int(self.token_lifetime_fraction * value["expires_in"] if value else 0),
)
if value is not None:
self.add_param_headers("Authorization", "Bearer " + value.get("access_token"))
self.add_param_headers("Authorization", "Bearer " + value["access_token"])
@property
def expires_at(self) -> datetime:
def expires_at(self) -> datetime | None:
"""
Get token expiry time.
@ -302,7 +306,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._expires_at
@property
def user_realm_name(self) -> str:
def user_realm_name(self) -> str | None:
"""
Get user realm name.
@ -312,11 +316,11 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._user_realm_name
@user_realm_name.setter
def user_realm_name(self, value: str) -> None:
def user_realm_name(self, value: str | None) -> None:
self._user_realm_name = value
@property
def custom_headers(self) -> dict:
def custom_headers(self) -> dict | None:
"""
Get custom headers.
@ -326,9 +330,9 @@ class KeycloakOpenIDConnection(ConnectionManager):
return self._custom_headers
@custom_headers.setter
def custom_headers(self, value: dict) -> None:
def custom_headers(self, value: dict | None) -> None:
self._custom_headers = value
if self.custom_headers is not None:
if self.custom_headers is not None and self.headers is not None:
# merge custom headers to main headers
self.headers.update(self.custom_headers)
@ -350,6 +354,14 @@ class KeycloakOpenIDConnection(ConnectionManager):
else:
token_realm_name = "master" # noqa: S105
if self.client_id is None:
msg = "Unable to get KeycloakOpenID client without client_id set."
raise AttributeError(msg)
if self.server_url is None:
msg = "Unable to get KeycloakOpenID without server_url set."
raise AttributeError(msg)
self._keycloak_openid = KeycloakOpenID(
server_url=self.server_url,
client_id=self.client_id,
@ -398,17 +410,17 @@ class KeycloakOpenIDConnection(ConnectionManager):
b"Session not active",
]
if e.response_code == HTTP_BAD_REQUEST and any(
err in e.response_body for err in list_errors
err in (e.response_body or b"") for err in list_errors
):
self.get_token()
else:
raise
def _refresh_if_required(self) -> None:
if datetime.now(tz=timezone.utc) >= self.expires_at:
if self.expires_at is not None and datetime.now(tz=timezone.utc) >= self.expires_at:
self.refresh_token()
def raw_get(self, *args: list, **kwargs: dict) -> Response:
def raw_get(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401
"""
Call connection.raw_get.
@ -430,7 +442,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
def raw_post(self, *args: list, **kwargs: dict) -> Response:
def raw_post(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401
"""
Call connection.raw_post.
@ -452,7 +464,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
def raw_put(self, *args: list, **kwargs: dict) -> Response:
def raw_put(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401
"""
Call connection.raw_put.
@ -474,7 +486,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
def raw_delete(self, *args: list, **kwargs: dict) -> Response:
def raw_delete(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401
"""
Call connection.raw_delete.
@ -531,7 +543,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
b"Session not active",
]
if e.response_code == HTTP_BAD_REQUEST and any(
err in e.response_body for err in list_errors
err in (e.response_body or b"") for err in list_errors
):
await self.a_get_token()
else:
@ -539,10 +551,10 @@ class KeycloakOpenIDConnection(ConnectionManager):
async def a__refresh_if_required(self) -> None:
"""Refresh the token if it is expired."""
if datetime.now(tz=timezone.utc) >= self.expires_at:
if self.expires_at is not None and datetime.now(tz=timezone.utc) >= self.expires_at:
await self.a_refresh_token()
async def a_raw_get(self, *args: list, **kwargs: dict) -> AsyncResponse:
async def a_raw_get(self, *args: Any, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
"""
Call connection.raw_get.
@ -564,7 +576,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
async def a_raw_post(self, *args: list, **kwargs: dict) -> AsyncResponse:
async def a_raw_post(self, *args: Any, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
"""
Call connection.raw_post.
@ -586,7 +598,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
async def a_raw_put(self, *args: list, **kwargs: dict) -> AsyncResponse:
async def a_raw_put(self, *args: Any, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
"""
Call connection.raw_put.
@ -608,7 +620,7 @@ class KeycloakOpenIDConnection(ConnectionManager):
return r
async def a_raw_delete(self, *args: list, **kwargs: dict) -> AsyncResponse:
async def a_raw_delete(self, *args: Any, **kwargs: Any) -> AsyncResponse: # noqa: ANN401
"""
Call connection.raw_delete.

27
src/keycloak/uma_permissions.py

@ -24,7 +24,7 @@
from __future__ import annotations
from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError
from keycloak.exceptions import KeycloakPermissionFormatError
class UMAPermission:
@ -68,11 +68,9 @@ class UMAPermission:
"""
self.resource = resource
self.scope = scope
self.resource_id = None
if permission:
if not isinstance(permission, UMAPermission):
msg = f"can't determine if '{permission}' is a resource or scope"
raise PermissionDefinitionError(msg)
if permission is not None:
if permission.resource:
self.resource = str(permission.resource)
if permission.scope:
@ -146,10 +144,7 @@ class UMAPermission:
if scope:
result_scope = str(scope)
if permission:
if not isinstance(permission, UMAPermission):
msg = f"can't determine if '{permission}' is a resource or scope"
raise PermissionDefinitionError(msg)
if permission is not None:
if permission.resource:
result_resource = str(permission.resource)
if permission.scope:
@ -168,7 +163,7 @@ class Resource(UMAPermission):
:type resource: str
"""
def __init__(self, resource: Resource) -> None:
def __init__(self, resource: str) -> None:
"""
Init method.
@ -188,7 +183,7 @@ class Scope(UMAPermission):
:type scope: str
"""
def __init__(self, scope: Scope) -> None:
def __init__(self, scope: str) -> None:
"""
Init method.
@ -213,7 +208,9 @@ class AuthStatus:
:type missing_permissions: set
"""
def __init__(self, is_logged_in: bool, is_authorized: bool, missing_permissions: set) -> None:
def __init__(
self, is_logged_in: bool, is_authorized: bool, missing_permissions: set | str
) -> None:
"""
Init method.
@ -252,7 +249,9 @@ class AuthStatus:
)
def build_permission_param(permissions: str | list | dict) -> set:
def build_permission_param(
permissions: str | list | dict | UMAPermission | None | tuple | set,
) -> set:
"""
Transform permissions to a set, so they are usable for requests.
@ -268,6 +267,8 @@ def build_permission_param(permissions: str | list | dict) -> set:
return {permissions}
if isinstance(permissions, UMAPermission):
return {str(permissions)}
if isinstance(permissions, (list, tuple, set)):
return set(permissions)
try: # treat as dictionary of permissions
result = set()

16
test_keycloak_init.sh

@ -6,9 +6,9 @@ KEYCLOAK_DOCKER_IMAGE="quay.io/keycloak/keycloak:$KEYCLOAK_DOCKER_IMAGE_TAG"
function keycloak_stop() {
if [ "$(docker ps -q -f name=unittest_keycloak)" ]; then
docker logs unittest_keycloak > keycloak_test_logs.txt
docker stop unittest_keycloak &> /dev/null
docker rm unittest_keycloak &> /dev/null
docker logs unittest_keycloak >keycloak_test_logs.txt
docker stop unittest_keycloak &>/dev/null
docker rm unittest_keycloak &>/dev/null
fi
}
@ -23,11 +23,11 @@ function keycloak_start() {
docker run --rm -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -p "${KEYCLOAK_PORT}:8080" -v $PWD/tests/providers:/opt/keycloak/providers "${KEYCLOAK_DOCKER_IMAGE}" start-dev --features="${KEYCLOAK_FEATURES}"
SECONDS=0
until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do
sleep 5;
if [ ${SECONDS} -gt 180 ]; then
echo "Timeout exceeded";
exit 1;
fi
sleep 5
if [ ${SECONDS} -gt 180 ]; then
echo "Timeout exceeded"
exit 1
fi
done
}

7
tests/conftest.py

@ -481,6 +481,7 @@ def group(admin: KeycloakAdmin, realm: str) -> Generator[str, None, None]:
admin.change_current_realm(realm)
group_name = str(uuid.uuid4())
group_id = admin.create_group(payload={"name": group_name})
assert group_id is not None
yield group_id
admin.delete_group(group_id=group_id)
@ -556,7 +557,7 @@ def composite_client_role(
@pytest.fixture
def selfsigned_cert() -> tuple[str, str]:
def selfsigned_cert() -> tuple[bytes, bytes]:
"""
Generate self signed certificate for a hostname, and optional IP addresses.
@ -564,7 +565,7 @@ def selfsigned_cert() -> tuple[str, str]:
:rtype: Tuple[str, str]
"""
hostname = "testcert"
ip_addresses = None
ip_addresses: None | list = []
key = None
# Generate our key
if key is None:
@ -575,7 +576,7 @@ def selfsigned_cert() -> tuple[str, str]:
)
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)])
alt_names = [x509.DNSName(hostname)]
alt_names: list = [x509.DNSName(hostname)]
# allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
if ip_addresses:

311
tests/test_keycloak_admin.py
File diff suppressed because it is too large
View File

84
tests/test_keycloak_openid.py

@ -160,7 +160,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]) -> None:
}
# Test with dummy totp
token = oid.token(username=username, password=password, totp="123456")
token = oid.token(username=username, password=password, totp=123456)
assert token == {
"access_token": mock.ANY,
"expires_in": mock.ANY,
@ -205,15 +205,14 @@ def test_exchange_token(
# Allow impersonation
admin.change_current_realm(oid.realm_name)
user_id = admin.get_user_id(username=username)
assert user_id is not None
client_id = admin.get_client_id(client_id="realm-management")
assert client_id is not None
admin.assign_client_role(
user_id=admin.get_user_id(username=username),
client_id=admin.get_client_id(client_id="realm-management"),
roles=[
admin.get_client_role(
client_id=admin.get_client_id(client_id="realm-management"),
role_name="impersonation",
),
],
user_id=user_id,
client_id=client_id,
roles=[admin.get_client_role(client_id=client_id, role_name="impersonation")],
)
token = oid.token(username=username, password=password)
@ -297,9 +296,9 @@ def test_entitlement(
"""
oid, username, password = oid_with_credentials_authz
token = oid.token(username=username, password=password)
resource_server_id = admin.get_client_authz_resources(
client_id=admin.get_client_id(oid.client_id),
)[0]["_id"]
client_id = admin.get_client_id(oid.client_id)
assert client_id is not None
resource_server_id = admin.get_client_authz_resources(client_id=client_id)[0]["_id"]
with pytest.raises(KeycloakDeprecationError):
oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id)
@ -431,11 +430,11 @@ def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str
oid.authorization.policies["test"] = policy
assert [
str(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode") # noqa: S106
for x in (oid.get_policies(token=token["access_token"], method_token_info="decode") or []) # noqa: S106
] == ["Policy: test (role)"]
assert [
repr(x)
for x in oid.get_policies(token=token["access_token"], method_token_info="decode") # noqa: S106
for x in (oid.get_policies(token=token["access_token"], method_token_info="decode") or []) # noqa: S106
] == ["<Policy: test (role)>"]
oid.client_id = orig_client_id
@ -477,11 +476,15 @@ 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") # noqa: S106
for x in (
oid.get_permissions(token=token["access_token"], method_token_info="decode") or [] # noqa: S106
)
] == ["Permission: test-perm (resource)"]
assert [
repr(x)
for x in oid.get_permissions(token=token["access_token"], method_token_info="decode") # noqa: S106
for x in (
oid.get_permissions(token=token["access_token"], method_token_info="decode") or [] # noqa: S106
)
] == ["<Permission: test-perm (resource)>"]
oid.client_id = orig_client_id
@ -538,6 +541,7 @@ def test_has_uma_access(
str(oid.has_uma_access(token=token["access_token"], permissions=""))
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
)
assert admin.connection.token is not None
assert (
str(
oid.has_uma_access(
@ -684,7 +688,7 @@ async def test_a_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]) ->
}
# Test with dummy totp
token = await oid.a_token(username=username, password=password, totp="123456")
token = await oid.a_token(username=username, password=password, totp=123456)
assert token == {
"access_token": mock.ANY,
"expires_in": mock.ANY,
@ -730,14 +734,15 @@ async def test_a_exchange_token(
# Allow impersonation
await admin.a_change_current_realm(oid.realm_name)
user_id = await admin.a_get_user_id(username=username)
assert user_id is not None
client_id = await admin.a_get_client_id(client_id="realm-management")
assert client_id is not None
await admin.a_assign_client_role(
user_id=await admin.a_get_user_id(username=username),
client_id=await admin.a_get_client_id(client_id="realm-management"),
user_id=user_id,
client_id=client_id,
roles=[
await admin.a_get_client_role(
client_id=admin.get_client_id(client_id="realm-management"),
role_name="impersonation",
),
await admin.a_get_client_role(client_id=client_id, role_name="impersonation"),
],
)
@ -826,9 +831,9 @@ async def test_a_entitlement(
"""
oid, username, password = oid_with_credentials_authz
token = await oid.a_token(username=username, password=password)
resource_server_id = admin.get_client_authz_resources(
client_id=admin.get_client_id(oid.client_id),
)[0]["_id"]
client_id = await admin.a_get_client_id(oid.client_id)
assert client_id is not None
resource_server_id = admin.get_client_authz_resources(client_id=client_id)[0]["_id"]
with pytest.raises(KeycloakDeprecationError):
await oid.a_entitlement(token=token["access_token"], resource_server_id=resource_server_id)
@ -989,6 +994,7 @@ async def test_a_has_uma_access(
str(await oid.a_has_uma_access(token=token["access_token"], permissions=""))
== "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
)
assert admin.connection.token is not None
assert (
str(
await oid.a_has_uma_access(
@ -1027,11 +1033,15 @@ async def test_a_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID,
oid.authorization.policies["test"] = policy
assert [
str(x)
for x in await oid.a_get_policies(token=token["access_token"], method_token_info="decode") # noqa: S106
for x in (
await oid.a_get_policies(token=token["access_token"], method_token_info="decode") or [] # noqa: S106
)
] == ["Policy: test (role)"]
assert [
repr(x)
for x in await oid.a_get_policies(token=token["access_token"], method_token_info="decode") # noqa: S106
for x in (
await oid.a_get_policies(token=token["access_token"], method_token_info="decode") or [] # noqa: S106
)
] == ["<Policy: test (role)>"]
oid.client_id = orig_client_id
@ -1078,16 +1088,22 @@ async def test_a_get_permissions(
oid.authorization.policies["test"] = policy
assert [
str(x)
for x in await oid.a_get_permissions(
token=token["access_token"],
method_token_info="decode", # noqa: S106
for x in (
await oid.a_get_permissions(
token=token["access_token"],
method_token_info="decode", # noqa: S106
)
or []
)
] == ["Permission: test-perm (resource)"]
assert [
repr(x)
for x in await oid.a_get_permissions(
token=token["access_token"],
method_token_info="decode", # noqa: S106
for x in (
await oid.a_get_permissions(
token=token["access_token"],
method_token_info="decode", # noqa: S106
)
or []
)
] == ["<Permission: test-perm (resource)>"]
oid.client_id = orig_client_id

16
tests/test_keycloak_uma.py

@ -216,7 +216,8 @@ def test_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
assert len(policies) == 1
policy_id = policy["id"]
uma.policy_delete(policy_id)
policy_delete_res = uma.policy_delete(policy_id)
assert policy_delete_res == {}
with pytest.raises(KeycloakDeleteError) as err:
uma.policy_delete(policy_id)
assert err.match(
@ -228,7 +229,8 @@ def test_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
assert len(policies) == 2
policy = policies[0]
uma.policy_update(policy_id=policy["id"], payload=policy)
policy_update_res = uma.policy_update(policy_id=policy["id"], payload=policy)
assert policy_update_res == b""
policies = uma.policy_query()
assert len(policies) == 2
@ -254,6 +256,7 @@ def test_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
uma.resource_set_delete(resource_id)
admin.delete_client(other_client_id)
admin.delete_realm_role(role_id)
assert group_id is not None
admin.delete_group(group_id)
@ -282,6 +285,7 @@ def test_uma_access(uma: KeycloakUMA) -> None:
token = uma.connection.token
permissions = []
assert token is not None
assert uma.permissions_check(token["access_token"], permissions)
permissions.append(UMAPermission(resource=resource_to_create["name"]))
@ -522,7 +526,8 @@ async def test_a_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
assert len(policies) == 1
policy_id = policy["id"]
await uma.a_policy_delete(policy_id)
policy_delete_res = await uma.a_policy_delete(policy_id)
assert policy_delete_res == {}
with pytest.raises(KeycloakDeleteError) as err:
await uma.a_policy_delete(policy_id)
assert err.match(
@ -534,7 +539,8 @@ async def test_a_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
assert len(policies) == 2
policy = policies[0]
await uma.a_policy_update(policy_id=policy["id"], payload=policy)
policy_update_res = await uma.a_policy_update(policy_id=policy["id"], payload=policy)
assert policy_update_res == b""
policies = await uma.a_policy_query()
assert len(policies) == 2
@ -560,6 +566,7 @@ async def test_a_uma_policy(uma: KeycloakUMA, admin: KeycloakAdmin) -> None:
await uma.a_resource_set_delete(resource_id)
await admin.a_delete_client(other_client_id)
await admin.a_delete_realm_role(role_id)
assert group_id is not None
await admin.a_delete_group(group_id)
@ -589,6 +596,7 @@ async def test_a_uma_access(uma: KeycloakUMA) -> None:
token = uma.connection.token
permissions = []
assert token is not None
assert await uma.a_permissions_check(token["access_token"], permissions)
permissions.append(UMAPermission(resource=resource_to_create["name"]))

4
tests/test_pkce_flow.py

@ -9,9 +9,10 @@ from packaging.version import Version
from keycloak import KeycloakAdmin, KeycloakOpenID
from keycloak.pkce_utils import generate_code_challenge, generate_code_verifier
from tests.conftest import KeycloakTestEnv
def test_pkce_auth_url_and_token(env: object, admin: KeycloakAdmin) -> None:
def test_pkce_auth_url_and_token(env: KeycloakTestEnv, admin: KeycloakAdmin) -> None:
"""Test PKCE flow: auth_url includes code_challenge, token includes code_verifier."""
if os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"] != "latest" and Version(
os.environ["KEYCLOAK_DOCKER_IMAGE_TAG"],
@ -74,4 +75,5 @@ def test_pkce_auth_url_and_token(env: object, admin: KeycloakAdmin) -> None:
# Cleanup
client_id = admin.get_client_id("pkce-test")
assert client_id is not None
admin.delete_client(client_id)

35
tests/test_uma_permissions.py

@ -20,7 +20,7 @@ import re
import pytest
from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError
from keycloak.exceptions import KeycloakPermissionFormatError
from keycloak.uma_permissions import (
AuthStatus,
Resource,
@ -32,9 +32,6 @@ from keycloak.uma_permissions import (
def test_uma_permission_obj() -> None:
"""Test generic UMA permission."""
with pytest.raises(PermissionDefinitionError):
UMAPermission(permission="bad")
p1 = UMAPermission(permission=Resource("Resource"))
assert p1.resource == "Resource"
assert p1.scope == ""
@ -77,15 +74,6 @@ def test_scope_resource_str() -> None:
assert s(resource=r) == "Resource1#Scope1"
def test_resource_scope_list() -> None:
"""Test resource scope as list."""
r = Resource("Resource1")
s = ["Scope1"]
with pytest.raises(PermissionDefinitionError) as err:
r(s)
assert err.match(re.escape("can't determine if '['Scope1']' is a resource or scope"))
def test_build_permission_none() -> None:
"""Test build permission param with None."""
assert build_permission_param(None) == set()
@ -172,27 +160,6 @@ def test_build_permission_misbuilt_dict_str_list_list_str() -> None:
assert err.match(re.escape("misbuilt permission {'res1': [['scope1', 'scope2']]}"))
def test_build_permission_misbuilt_list_list_str() -> None:
"""Test bad build of permission param from list."""
with pytest.raises(KeycloakPermissionFormatError) as err:
build_permission_param([["scope1", "scope2"]])
assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]"))
def test_build_permission_misbuilt_list_set_str() -> None:
"""Test bad build of permission param from set."""
with pytest.raises(KeycloakPermissionFormatError) as err:
build_permission_param([{"scope1", "scope2"}])
assert err.match("misbuilt permission.*")
def test_build_permission_misbuilt_set_set_str() -> None:
"""Test bad build of permission param from list of set."""
with pytest.raises(KeycloakPermissionFormatError) as err:
build_permission_param([{"scope1"}])
assert err.match(re.escape("misbuilt permission [{'scope1'}]"))
def test_build_permission_misbuilt_dict_non_iterable() -> None:
"""Test bad build of permission param from non-iterable."""
with pytest.raises(KeycloakPermissionFormatError) as err:

Loading…
Cancel
Save