├── .github └── workflows │ ├── python-lint.properties.json │ ├── python-lint.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── advanced_topics.rst ├── api.rst ├── authorization.rst ├── conf.py ├── index.rst ├── intro.rst ├── make.bat ├── requirements.txt ├── testing.rst └── usage.rst ├── fastapi_keycloak_middleware ├── __init__.py ├── decorators │ ├── __init__.py │ ├── require_permission.py │ └── strip_request.py ├── dependencies │ ├── __init__.py │ ├── check_permission.py │ ├── get_auth.py │ ├── get_authorization_result.py │ └── get_user.py ├── exceptions.py ├── fast_api_user.py ├── keycloak_backend.py ├── middleware.py ├── schemas │ ├── __init__.py │ ├── authorization_methods.py │ ├── authorization_result.py │ ├── exception_response.py │ ├── keycloak_configuration.py │ └── match_strategy.py └── setup.py ├── poetry.lock └── pyproject.toml /.github/workflows/python-lint.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python Linting", 3 | "description": "Workflow that triggers basic linting tasks for commits in the development branch", 4 | "iconName": "demo-icon", 5 | "categories": [ 6 | "Python" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/python-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-22.04 11 | timeout-minutes: 10 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | - name: Run pre-commit 20 | uses: pre-commit/action@v3.0.0 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Python Test package 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | jobs: 7 | build-test: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build and publish to pypi 12 | uses: JRubics/poetry-publish@v1.16 13 | with: 14 | pypi_token: ${{ secrets.TEST_PYPI_TOKEN }} 15 | repository_name: "fastapi-keycloak-middleware" 16 | repository_url: "https://test.pypi.org/legacy/" 17 | python_version: "3.10.3" 18 | poetry_version: "==1.3.1" 19 | release: 20 | runs-on: ubuntu-22.04 21 | environment: production 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Build and publish to pypi 25 | uses: JRubics/poetry-publish@v1.16 26 | with: 27 | pypi_token: ${{ secrets.PYPI_TOKEN }} 28 | repository_name: "fastapi-keycloak-middleware" 29 | python_version: "3.10.3" 30 | poetry_version: "==1.3.1" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | mksp_backend/config.py 4 | dev/misc/ 5 | files/ 6 | __pycache__ 7 | .pytest_cache 8 | .coverage 9 | mksp_backend/test.py 10 | .venv 11 | .DS_Store 12 | docs/_build 13 | .ruff_cache 14 | .mypy_cache 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "docs/|ext/" 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - repo: https://github.com/asottile/pyupgrade 12 | rev: v3.20.0 13 | hooks: 14 | - id: pyupgrade 15 | args: [--py37-plus, --keep-percent-format] 16 | - repo: https://github.com/astral-sh/ruff-pre-commit 17 | rev: "v0.11.12" 18 | hooks: 19 | # Run the linter. 20 | - id: ruff 21 | # Run the formatter. 22 | - id: ruff-format 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-24.04 11 | tools: 12 | python: "3.13" 13 | jobs: 14 | post_create_environment: 15 | - pip install poetry==1.8.5 16 | post_install: 17 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with dev 18 | 19 | # Build documentation in the docs/ directory with Sphinx 20 | sphinx: 21 | configuration: docs/conf.py 22 | fail_on_warning: true 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Daniel Herrmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/fastapi-keycloak-middleware/badge/?version=latest)](https://fastapi-keycloak-middleware.readthedocs.io/en/latest/?badge=latest) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) 3 | ![GitHub issues](https://img.shields.io/github/issues/waza-ari/fastapi-keycloak-middleware) 4 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/waza-ari/fastapi-keycloak-middleware) 5 | ![GitHub top language](https://img.shields.io/github/languages/top/waza-ari/fastapi-keycloak-middleware) 6 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/waza-ari/fastapi-keycloak-middleware/development.svg)](https://results.pre-commit.ci/latest/github/waza-ari/fastapi-keycloak-middleware/development) 7 | 8 | 9 | # FastAPI Keycloak Middleware 10 | 11 | **Full documentation** is [available at Read The Docs](https://fastapi-keycloak-middleware.readthedocs.io/en/latest/) 12 | 13 | This package provides a middleware for [FastAPI](http://fastapi.tiangolo.com) that 14 | simplifies integrating with [Keycloak](http://keycloak.org) for 15 | authentication and authorization. It supports OIDC and supports validating access 16 | tokens, reading roles and basic authentication. In addition it provides several 17 | decorators and dependencies to easily integrate into your FastAPI application. 18 | 19 | It relies on the [python-keycloak](http://python-keycloak.readthedocs.io) package, 20 | which is the only dependency outside of the FastAPI ecosystem which would be installed 21 | anyway. Shoutout to the author of [fastapi-auth-middleware](https://github.com/code-specialist/fastapi-auth-middleware) 22 | which served as inspiration for this package and some of its code. 23 | 24 | In the future, I plan to add support for fine grained authorization using Keycloak 25 | Authorization services. 26 | 27 | ## Motivation 28 | 29 | Using FastAPI and Keycloak quite a lot, and keeping to repeat myself quite a lot when 30 | it comes to authentiating users, I decided to create this library to help with this. 31 | 32 | There is a clear separation between the authentication and authorization: 33 | 34 | - **Authentication** is about verifying the identity of the user 35 | (who they are). This is done by an authentication backend 36 | that verifies the users access token obtained from the 37 | identity provider (Keycloak in this case). 38 | - **Authorization** is about deciding which resources can be 39 | accessed. This package providers convenience decoraters to 40 | enforce certain roles or permissions on FastAPI endpoints. 41 | 42 | ## Installation 43 | 44 | Install the package using poetry: 45 | 46 | ```bash 47 | poetry add fastapi-keycloak-middleware 48 | ``` 49 | 50 | or `pip`: 51 | 52 | ```bash 53 | pip install fastapi-keycloak-middleware 54 | ``` 55 | 56 | ## Features 57 | 58 | The package helps with: 59 | 60 | * An easy to use middleware that validates the request for an access token 61 | * Validation can done in one of two ways: 62 | * Validate locally using the public key obtained from Keycloak 63 | * Validate using the Keycloak token introspection endpoint 64 | * Using Starlette authentication mechanisms to store both the user object as well as the authorization scopes in the Request object 65 | * Ability to provide custom callback functions to retrieve the user object (e.g. from your database) and to provide an arbitrary mapping to authentication scopes (e.g. roles to permissions) 66 | * A decorator to use previously stored information to enforce certain roles or permissions on FastAPI endpoints 67 | * Convenience dependencies to retrieve the user object or the authorization result after evaluation within the FastAPI endpoint 68 | 69 | ## Acknowledgements 70 | 71 | This package is heavily inspired by [fastapi-auth-middleware](https://github.com/code-specialist/fastapi-auth-middleware) 72 | which provides some of the same functionality but without the direct integration 73 | into Keycloak. Thanks for writing and providing this great piece of software! 74 | 75 | ## Contributing 76 | 77 | The client is written in pure Python. 78 | Any changes or pull requests are more than welcome, but please adhere to the code style. 79 | 80 | Ruff is used both for code formatting and linting. Before committing, please run the following command to ensure 81 | that your code is properly formatted: 82 | 83 | ```bash 84 | ruff check . 85 | ruff format . 86 | ``` 87 | 88 | A pre-commit hook configuration is supplied as part of the project. 89 | 90 | ## Development 91 | 92 | This project is using [Act](https://github.com/nektos/act) to handle local development tasks. It is used 93 | to work locally and also to test Github actions before deploying them. 94 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waza-ari/fastapi-keycloak-middleware/b2c0c483d565303c9875fd8912db92061be5d833/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/advanced_topics.rst: -------------------------------------------------------------------------------- 1 | .. _advanced_topics: 2 | 3 | Advanced Topics 4 | =============== 5 | 6 | The following sections describe advanced usage of the library. 7 | 8 | Request Modification Details 9 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 | 11 | If all checks outlined above pass successfully, the actual endpoint (or next middleware) will be called. To persist the authentication result, 12 | we're using Starlettes :code:`Request` object. The following attributes are added to the request: 13 | 14 | **User Object** 15 | 16 | The user object is stored in :code:`scope.user` attribute. As the request is passed to further middlewares, dependencies or potentially decorators, the endpoint can access the user information. 17 | 18 | **Authorization Scopes** 19 | 20 | This example does not contain any scopes, so the :code:`scope.auth` attribute will be an empty list. Refer to the advanced documentation for how to leverate authorization scopes. 21 | 22 | Logging 23 | ^^^^^^^ 24 | 25 | Note straight from the `Python Docs `_: 26 | 27 | .. note:: 28 | It is strongly advised that you do not log to the root logger in your library. Instead, use a logger with a unique and easily identifiable name, such as the :code:`__name__` for your library’s top-level package or module. Logging to the root logger will make it difficult or impossible for the application developer to configure the logging verbosity or handlers of your library as they wish. 29 | 30 | Also, it is recommended to use module level logging: 31 | 32 | .. note:: 33 | A good convention to use when naming loggers is to use a module-level logger, in each module which uses logging, named as follows 34 | 35 | This module implements these best practices. 36 | 37 | .. warning:: 38 | Especially during the authorization phase, the user object is included in the log message. Make sure your user object is serializable to a string, otherwise the log message will contain non-helpful strings. 39 | 40 | Token Introspection vs Opaque Tokens 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | 43 | By default, this library will attempt to validate the JWT signature locally using the public key obtained from Keycloak. This is the recommended way to validate the token, as it does not require any additional requests to Keycloak. Also, Keycloak does not support opaque tokens yet. 44 | 45 | If you want to still use the token endpoint to validate the token, you can opt to do so: 46 | 47 | .. code-block:: python 48 | :emphasize-lines: 7 49 | 50 | # Set up Keycloak 51 | keycloak_config = KeycloakConfiguration( 52 | url="https://sso.your-keycloak.com/auth/", 53 | realm="", 54 | client_id="", 55 | client_secret="", 56 | use_introspection_endpoint=True 57 | ) 58 | 59 | Please make sure to understand the consequences before applying this configuration. 60 | 61 | Disabling Token Validation 62 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 63 | 64 | It is possible to disable token validation altogether. Be aware that this is a security risk pretty much disables 65 | all security features of the library. 66 | 67 | .. code-block:: python 68 | :emphasize-lines: 7 69 | 70 | # Set up Keycloak 71 | keycloak_config = KeycloakConfiguration( 72 | url="https://sso.your-keycloak.com/auth/", 73 | realm="", 74 | client_id="", 75 | client_secret="", 76 | validate_token=False 77 | ) 78 | 79 | JWT Decoding Options 80 | ^^^^^^^^^^^^^^^^^^^^ 81 | 82 | The upstream project `python-keycloak` uses `JWCrypto` under the hood to verify 83 | JWT tokens. This library uses sensible defaults for the JWT verification, but it is 84 | possible to modify the decode options if needed. 85 | 86 | .. code-block:: python 87 | :emphasize-lines: 6,7,8 88 | 89 | # Set up Keycloak 90 | keycloak_config = KeycloakConfiguration( 91 | # ... 92 | validation_options={ 93 | check_claims = { 94 | "jti": None, 95 | "exp": None, 96 | "iat": None, 97 | } 98 | } 99 | ) 100 | 101 | Please refer to the `JWCrypto` documentation for the available options: 102 | https://jwcrypto.readthedocs.io/en/latest/jwt.html#jwcrypto.jwt.JWT 103 | 104 | Excluding Endpoints 105 | ^^^^^^^^^^^^^^^^^^^ 106 | 107 | You may not want to enforce authentication for all endpoints. For example, you may want to allow anonymous access to the health check endpoint or allow accessing the autogenerated docs and OpenAPI schema without authentication. 108 | 109 | There are two ways of doing this. 110 | 111 | .. note:: 112 | Both examples can also be combined to achieve more complex setups. 113 | 114 | Exclude certain paths 115 | --------------------- 116 | 117 | The middleware provides a configuration option to exclude certain paths from authentication. Those are compiled as regex and then matched against the request path. 118 | 119 | **Example:** 120 | 121 | .. code-block:: python 122 | :emphasize-lines: 12 123 | 124 | excluded_routes = [ 125 | "/status", 126 | "/docs", 127 | "/openapi.json", 128 | "/redoc", 129 | ] 130 | 131 | app = FastAPI() 132 | setup_keycloak_middleware( 133 | app, 134 | # ... 135 | exclude_patterns=excluded_routes, 136 | ) 137 | 138 | This would make sure you can access the docs, alternate docs, OpenAPI schema and health check endpoint without authentication. 139 | 140 | .. warning:: 141 | At the moment only the paths are checked, not the request method or other criteria. See issue `#3 `_ for more details. 142 | 143 | **Technical Details:** 144 | 145 | Under the hood these paths are compiled to regex and then matched against the request path. Each string is passed as-is to :code:`re.compile` and stored, such that it can be used later to patch against the request path. 146 | 147 | Use Multiple Applications 148 | ------------------------- 149 | 150 | Alternatively you can use multiple :code:`FastAPI` applications and mount them to the main application. This way you can have different authentication requirements for different endpoints. 151 | 152 | **Example:** 153 | 154 | .. code-block:: python 155 | 156 | # This first app is secured 157 | secured_app = FastAPI() 158 | 159 | setup_keycloak_middleware( 160 | secured_app, 161 | # ... 162 | exclude_patterns=excluded_routes, 163 | ) 164 | 165 | # This second app has no middleware to it and is not protected 166 | public_app = FastAPI() 167 | 168 | # This is your main app, mounting the other two applications 169 | app = FastAPI() 170 | app.mount(path="/secured", app=secured_app) 171 | app.mount(path="/public", app=public_app) 172 | 173 | Device Authentication 174 | ^^^^^^^^^^^^^^^^^^^^^ 175 | 176 | If you need to authenticate devices, you can do so in various different ways. We need to distinguish between two different scenarios: 177 | 178 | User Devices 179 | ------------ 180 | 181 | These are devices that belong to a certain user. You can use Keycloak `device authorization grant `_. The device can start the process by using the Keycloak REST API and show a code to the user. The user then enters this code in Keycloak and authenticates with the user credentials. The device can poll another endpoint and receives a token when the authentication is completed. 182 | 183 | You only need to make sure that the same claims are mapped to tokens created by this client compared to the claims normal users would get. For this library there is no difference between those tokens then, so authentication and authorization work as previously described. 184 | 185 | Standalone Devices 186 | ------------------ 187 | 188 | **Overview** 189 | 190 | It gets a little more complicated if a device is not directly mapped to a user, for example IoT decices you maintain that need to access your API. 191 | 192 | While the way how you obtain the token doesn't really matter (could be device code flow as described above or could be Keycloak offline tokens), the user that is used for this matters. 193 | 194 | **Keycloak configuration** 195 | 196 | One example on how to configure the Keycloak side of things: 197 | 198 | 1. Create a user in Keycloak that represents the device 199 | 2. Create a client for device authentication 200 | 3. Create client roles for the devices you need to support and map them to the same claim you use for user roles on your user client 201 | 4. Map the device user to client roles of the device client 202 | 203 | You can now obtain a refresh token on either using the device flow or my leveraging offline sessions and the device can use them to obtain an access token if it needs to perform requests against the API. 204 | 205 | .. note:: 206 | This by no means is the only way to do this. Keycloak is very flexible, you'll need to find the configuration that fits your needs. 207 | 208 | **Library configuration** 209 | 210 | Depending on your user handling within the API, you may need to take additional steps. If you also create the device users within your API environment and the user mapper can map them as normal, you don't need to take additonal steps. If you don't want to create these users within the API, this library has options to configure how to behave in case the user does not exist. 211 | 212 | The default behavior is to fail authentication if the built-in or user-defined user mapper cannot return a user. For device authentication, it is possible to add a specific claim to the access token which tells the library that this is a device requesting access. 213 | 214 | The following example shows the configurtion on the library side: 215 | 216 | .. code-block:: python 217 | :emphasize-lines: 7,8 218 | 219 | # Set up Keycloak 220 | keycloak_config = KeycloakConfiguration( 221 | url="https://sso.your-keycloak.com/auth/", 222 | realm="", 223 | client_id="", 224 | client_secret="", 225 | enable_device_authentication=True, 226 | device_authentication_claim="is_device", 227 | ) 228 | 229 | This tells the library to enable the aforementioned behavior. It will now: 230 | 231 | 1. The access token signature and validity will be checked as usual 232 | 2. Check if the claim :code:`is_device` is present in the access token 233 | 3. If it is present, it will evaluate the value of the claim. If it is a truthy value (``bool(value) === True``), continue, otherwise fail authentication 234 | 4. The remaining steps (claim extraction, user mapping, authorization scope mapping) will be skipped 235 | 236 | If the claim is not present in the access token, the library will behave as usual and try normal user authentication. 237 | 238 | .. note:: 239 | To add the claim to your token, you can either use a ``Hardcoded claim`` mapper or any other method you prefer. 240 | 241 | Websocket Support 242 | ^^^^^^^^^^^^^^^^^ 243 | 244 | We can also inspect websocket connection requests and validate the token using the same mechanisms. Websockets do not support the same headers, therefore we have to use cookies instead. 245 | Websocket support is enabled by default, but can be disabled by setting the :code:`enable_websocket_support` parameter to :code:`False`. 246 | 247 | If enabled, the initial websocket connection request will be validated, subsequent messages within the same socket won't be affected. 248 | The cookie is expected to be named :code:`access_token` and contain the JWT token prefixed with :code:`Bearer`. 249 | 250 | Request Injection 251 | ^^^^^^^^^^^^^^^^^ 252 | 253 | .. warning:: 254 | This section documents details about deprecated, decorator based permission checking and will be removed in the future. 255 | 256 | .. note:: 257 | This section contains technical details about the implementation within the library and is not required to use the library. Feel free to skip it. 258 | 259 | The decorator used to enforce permissions requires to have access to the Request object, as the middleware stores the user information and compiled permissions there. 260 | 261 | FastAPI injects the request to the path function, if the path function declares the request parameter. If its not provided by the user, the request would normally not be passed and would therefore not be available to the decorator. 262 | 263 | This would end up in some code like this: 264 | 265 | .. code-block:: python 266 | 267 | @app.get("/users/me") 268 | @require_permission("user:read") 269 | def read_users_me(request: Request): # pylint: disable=unused-argument 270 | return {"user": "Hello World"} 271 | 272 | Not only would this require unneccessary imports and blow up the path function, it would also raise a warning for an unused variable which then would need to be suppressed. 273 | 274 | To avoid this, the decorater uses a somewhat "hacky" way to modify the function signature and include the request parameter. This way, the user does not need to declare the request parameter and the decorator can still access it. 275 | 276 | Lateron, before actually calling the path function, the request is removed from :code:`kwargs` again, to avoid an exception being raised for an unexpected argument. 277 | 278 | Details can be found in `PEP 362 - Function Signature Object `_. Consider the following code: 279 | 280 | .. code-block:: python 281 | 282 | # Get function signature 283 | sig = signature(func) 284 | 285 | # Get parameters 286 | parameters: OrderedDict = sig.parameters 287 | if "request" in parameters.keys(): 288 | # Request is already present, no need to modify signature 289 | return wrapper 290 | 291 | # Add request parameter by creating a new parameter list based on the old one 292 | parameters = [ 293 | Parameter( 294 | name="request", 295 | kind=Parameter.POSITIONAL_OR_KEYWORD, 296 | default=Parameter.empty, 297 | annotation=starlette.requests.Request, 298 | ), 299 | *parameters.values(), 300 | ] 301 | 302 | # Create a new signature, as the signature is immutable 303 | new_sig = sig.replace(parameters=parameters, return_annotation=sig.return_annotation) 304 | 305 | # Update the wrapper function signature 306 | wrapper.__signature__ = new_sig 307 | return wrapper 308 | 309 | The request is still passed to the path function if defined by the user, otherwise its removed before calling the path function. 310 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. module:: fastapi_keycloak_middleware 5 | 6 | This part of the documentation covers all the interfaces of Requests. For 7 | parts where Requests depends on external libraries, we document the most 8 | important right here and provide links to the canonical documentation. 9 | 10 | Middleware 11 | ---------- 12 | 13 | .. autofunction:: setup_keycloak_middleware 14 | .. autoclass:: KeycloakMiddleware 15 | :members: 16 | 17 | Support Classes 18 | --------------- 19 | 20 | .. _keycloak_configuration: 21 | .. autoclass:: KeycloakConfiguration 22 | :members: 23 | .. autoclass:: AuthorizationResult 24 | :members: 25 | .. autoclass:: AuthorizationMethod 26 | :members: 27 | .. autoclass:: MatchStrategy 28 | :members: 29 | 30 | Decorators 31 | ---------- 32 | 33 | These decorators can be used to wrap certain paths in your FastAPI application. 34 | 35 | .. autofunction:: require_permission 36 | .. autofunction:: strip_request 37 | 38 | FastAPI Depdencies 39 | ------------------ 40 | 41 | These are the dependencies that you can use in your FastAPI application. 42 | 43 | .. autofunction:: get_user 44 | .. autofunction:: get_authorization_result 45 | -------------------------------------------------------------------------------- /docs/authorization.rst: -------------------------------------------------------------------------------- 1 | Authorization 2 | ============= 3 | 4 | Authorization is about enforcing certain permissions to certain resources. In this 5 | case a resource is a FastAPI endpoint, but there's also a concept to enforce more 6 | fine grained controls, also that needs to be done within your code. 7 | 8 | Used Nomenclature 9 | ^^^^^^^^^^^^^^^^^ 10 | 11 | In this document, the following nomenclature is used. Note that especially the Scope and Permission definition does not neccessarily match the definition in the OAuth2 specification. 12 | 13 | * **Resource**: A resource is a FastAPI endpoint. 14 | * **Resource Server (RS)**: The resource server is the FastAPI application. 15 | * **Access Token (AT)**: Token send by the user to the resource server. 16 | * **Claim**: Part of the AT that contains information about the user. 17 | * **Scope**: Information obtained from the chosen authorization method (e.g. based on a :code:`roles` claim within the AT). Generally, this is just a list of strings 18 | * **Permissions**: Your application has the option to map the scopes to permissions. This is done by providing a mapper function. If you don't provide one, the scopes will be passed as is and used for permissions. 19 | 20 | Overview 21 | ^^^^^^^^ 22 | 23 | In general, the folowing process is followed: 24 | 25 | #. A user is authenticated. 26 | #. Based on the chosen authorization method, the Scope is compiled. Currently, only claim based scopes are supported (that is, the information is extracted from a claim), but there are plans to add Keycloaks fine grained permissions system at a later time. 27 | #. A mapper is applied to map the obtained Scope to Permissions. For example, if your AT containes the :code:`roles` claim, you can map these roles to permissions. 28 | #. A decorator is used on the FastAPI endpoint to enforce a certain permission. 29 | #. Optionally, you can get the result of the permission evaluation as dependency and can work with it to ensure a more fine grained control. 30 | 31 | Basic Usage 32 | ^^^^^^^^^^^ 33 | 34 | This is a basic example of how to use the authorization system. It is based on the :ref:`usage` example. 35 | 36 | Enable Authorization 37 | """""""""""""""""""" 38 | 39 | To enable authorization, simply pass the chosen method to the middleware initialization: 40 | 41 | .. code-block:: python 42 | 43 | from fastapi import FastAPI 44 | from fastapi_keycloak_middleware import KeycloakConfiguration, AuthorizationMethod, setup_keycloak_middleware 45 | 46 | # Set up Keycloak 47 | keycloak_config = KeycloakConfiguration( 48 | url="https://sso.your-keycloak.com/auth/", 49 | realm="", 50 | client_id="", 51 | client_secret="", 52 | authorization_method=AuthorizationMethod.CLAIM, 53 | ) 54 | 55 | app = FastAPI() 56 | 57 | setup_keycloak_middleware( 58 | app, 59 | keycloak_configuration=keycloak_config, 60 | get_user=auth_get_user, 61 | ) 62 | 63 | Protect Endpoint 64 | """""""""""""""" 65 | 66 | Then, on the endpoint you want to protect, add a dependency specifying which permission is required to access the resource: 67 | 68 | .. code-block:: python 69 | 70 | from fastapi import Depends 71 | from fastapi_keycloak_middleware import CheckPermissions 72 | 73 | @app.get("/protected", dependencies=[Depends(CheckPermissions("protected"))]) 74 | def protected(): 75 | return {"message": "Hello World"} 76 | 77 | .. note:: 78 | Previous versions of the library used the :code:`@require_permission` decorator. This has been deprecated in favor of the :code:`CheckPermissions` dependency, please update your code accordingly. 79 | 80 | Claim Authorization 81 | ^^^^^^^^^^^^^^^^^^^ 82 | 83 | As of today, the authorization based on a claim is the only supported method. This means that the scopes are extracted from a claim within the AT. 84 | 85 | By default, the :code:`roles` claim will be checked to build the scope. You can configure this behavior: 86 | 87 | .. code-block:: python 88 | :emphasize-lines: 4 89 | 90 | keycloak_config = KeycloakConfiguration( 91 | # ... 92 | authorization_method=AuthorizationMethod.CLAIM, 93 | authorization_claim="permissions" 94 | ) 95 | 96 | setup_keycloak_middleware( 97 | app, 98 | keycloak_configuration=keycloak_config, 99 | get_user=auth_get_user, 100 | ) 101 | 102 | In this example, the library would extract the scopes from the :code:`permissions` claim. 103 | 104 | Permission Mapping 105 | ^^^^^^^^^^^^^^^^^^ 106 | 107 | In the examples above, the content of the claims is used unmodified. You can add a custom mapper to map the scopes to permissions. A common example for this is mapping **roles** to **permissions**. This is done by providing a mapper function: 108 | 109 | .. code-block:: python 110 | :emphasize-lines: 29 111 | 112 | from fastapi import FastAPI 113 | from fastapi_keycloak_middleware import KeycloakConfiguration, AuthorizationMethod, setup_keycloak_middleware 114 | 115 | async def scope_mapper(claim_auth: typing.List[str]) -> typing.List[str]: 116 | """ 117 | Map token roles to internal permissions. 118 | 119 | This could be whatever code you like it to be, you could also fetch this 120 | from database. Keep in mind this is done for every incoming request though. 121 | """ 122 | permissions = [] 123 | for role in claim_auth: 124 | try: 125 | permissions += rules[role] 126 | except KeyError: 127 | log.warning("Unknown role %s" % role) 128 | 129 | return permissions 130 | 131 | keycloak_config = KeycloakConfiguration( 132 | # ... 133 | authorization_method=AuthorizationMethod.CLAIM, 134 | ) 135 | 136 | setup_keycloak_middleware( 137 | app, 138 | keycloak_configuration=keycloak_config, 139 | get_user=auth_get_user, 140 | scope_mapper=scope_mapper, 141 | ) 142 | 143 | The result of this mapping function is then used to enforce the permissions. 144 | 145 | Composite Authorization 146 | ^^^^^^^^^^^^^^^^^^^^^^^ 147 | 148 | You can build more complex authorization rules by combining multiple permissions. This is done by passing a list of permissions to the :code:`CheckPermissions` method: 149 | 150 | .. code-block:: python 151 | :emphasize-lines: 4 152 | 153 | from fastapi import Depends 154 | from fastapi_keycloak_middleware import CheckPermissions 155 | 156 | @app.get("/view_user", dependencies=[Depends(CheckPermissions(["user:view", "user:view_own"]))]) 157 | def view_user(): 158 | return {"userinfo": "Hello World"} 159 | 160 | By default, the decorator will now enforce that the user bas both permissions. You can change this behavior by passing the :code:`match_strategy` parameter: 161 | 162 | .. code-block:: python 163 | :emphasize-lines: 2,4 164 | 165 | from fastapi import Depends 166 | from fastapi_keycloak_middleware import CheckPermissions, MatchStrategy 167 | 168 | @app.get("/view_user", dependencies=[Depends()]) 169 | def view_user(): 170 | return {"userinfo": "Hello World"} 171 | 172 | Now, it is sufficient for the user to have one of the mentioned permissions. 173 | 174 | Accessing the Authorization Result 175 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 176 | 177 | The method itself works like a regular FastAPI dependency and can be used either in the :code:`dependencies` parameter of the endpoint or as a parameter to the path function. 178 | When used as a parameter, the result of the authorization evaluation is passed to the function. 179 | 180 | .. code-block:: python 181 | :emphasize-lines: 2,5 182 | 183 | from fastapi import Depends 184 | from fastapi_keycloak_middleware import AuthorizationResult, CheckPermissions, MatchStrategy 185 | 186 | @app.get("/view_user") 187 | def view_user(authorization_result: AuthorizationResult = Depends(CheckPermissions(["user:view", "user:view_own"], match_strategy=MatchStrategy.OR))): 188 | return {"userinfo": "Hello World"} 189 | 190 | You can now access the permissions that actually matched and act based on this information. For example, if only the :code:`user:view_own` permission matched, you could check if the user requested matches the currently logged in user. 191 | 192 | .. note:: 193 | Note that previous versions of this library used a decorator to match permissions and therefore needed quite convoluted logic to make the result accessible. Using the :code:`@require_permission` decorator and therefore the :code:`get_authorization_result` dependency is deprecated and will be removed in future versions. 194 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # If extensions (or modules to document with autodoc) are in another directory, 5 | # add these directories to sys.path here. If the directory is relative to the 6 | # documentation root, use os.path.abspath to make it absolute, like shown here. 7 | # sys.path.insert(0, os.path.abspath('.')) 8 | 9 | # Insert Requests' path into the system. 10 | sys.path.insert(0, os.path.abspath("..")) 11 | 12 | import fastapi_keycloak_middleware # noqa: F401,E402 13 | 14 | # Configuration file for the Sphinx documentation builder. 15 | # 16 | # For the full list of built-in configuration values, see the documentation: 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 18 | 19 | # -- Project information ----------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 21 | 22 | project = "FastAPI Keycloak Middleware" 23 | copyright = "2024, Daniel Herrmann" 24 | author = "Daniel Herrmann" 25 | release = "1.3.0" 26 | 27 | # -- General configuration --------------------------------------------------- 28 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 29 | 30 | extensions = ["sphinx.ext.autodoc"] 31 | 32 | templates_path = ["_templates"] 33 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 34 | 35 | 36 | # -- Options for HTML output ------------------------------------------------- 37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 38 | 39 | html_theme = "sphinx_rtd_theme" 40 | html_static_path = ["_static"] 41 | 42 | html_theme_options = { 43 | "display_version": True, 44 | "prev_next_buttons_location": "bottom", 45 | "style_external_links": False, 46 | "vcs_pageview_mode": "", 47 | # Toc options 48 | "collapse_navigation": True, 49 | "sticky_navigation": True, 50 | "navigation_depth": 2, 51 | "includehidden": True, 52 | "titles_only": False, 53 | } 54 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. FastAPI Keycloak Middleware documentation master file, created by 2 | sphinx-quickstart on Sat Mar 25 20:24:27 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | FastAPI KeyCloak Middleware 7 | =========================== 8 | 9 | This package provides a middleware for `FastAPI `_ that simplifies 10 | integrating with `Keycloak `_ for 11 | authentication and authorization. It supports OIDC and supports validating access tokens, 12 | reading roles and basic authentication. In addition it provides several decorators and 13 | dependencies to easily integrate into your FastAPI application. 14 | 15 | It relies on the `python-keycloak `_ package, which 16 | is the only dependency outside of the FastAPI ecosystem which would be installed anyway. 17 | Shoutout to the author of `fastapi-auth-middleware `_ 18 | which served as inspiration for this package and some of its code. 19 | 20 | In the future, I plan to add support for fine grained authorization using Keycloak Authorization 21 | services. 22 | 23 | .. toctree:: 24 | :maxdepth: 5 25 | :caption: Contents: 26 | 27 | intro 28 | usage 29 | authorization 30 | advanced_topics 31 | testing 32 | api 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /docs/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Motivation 5 | ^^^^^^^^^^ 6 | 7 | Using FastAPI and Keycloak quite a lot, and keeping to repeat 8 | myself quite a lot when it comes to authentiating users, I 9 | decided to create this library to help with this. 10 | 11 | There is a clear separation between the authentication and 12 | authorization: 13 | 14 | - **Authentication** is about verifying the identity of the user 15 | (who they are). This is done by an authentication backend 16 | that verifies the users access token obtained from the 17 | identity provider (Keycloak in this case). 18 | - **Authorization** is about deciding which resources can be 19 | accessed. This package providers convenience decoraters to 20 | enforce certain roles or permissions on FastAPI endpoints. 21 | 22 | Installation 23 | ^^^^^^^^^^^^ 24 | 25 | Install the package using poetry: 26 | 27 | .. code-block:: bash 28 | 29 | poetry add fastapi-keycloak-middleware 30 | 31 | or pip: 32 | 33 | .. code-block:: bash 34 | 35 | pip install fastapi-keycloak-middleware 36 | 37 | Features 38 | ^^^^^^^^ 39 | 40 | The package helps with: 41 | 42 | * An easy to use middleware that validates the request for an access token 43 | * Validation can done in one of two ways: 44 | * Validate locally using the public key obtained from Keycloak 45 | * Validate using the Keycloak token introspection endpoint 46 | * Using Starlette authentication mechanisms to store both the user object as well as the authorization scopes in the Request object 47 | * Ability to provide custom callback functions to retrieve the user object (e.g. from your database) and to provide an arbitrary mapping to authentication scopes (e.g. roles to permissions) 48 | * A decorator to use previously stored information to enforce certain roles or permissions on FastAPI endpoints 49 | * Convenience dependencies to retrieve the user object or the authorization result after evaluation within the FastAPI endpoint 50 | 51 | Acknowledgements 52 | ^^^^^^^^^^^^^^^^ 53 | 54 | This package is heavily inspired by `fastapi-auth-middleware `_ 55 | which provides some of the same functionality but without the direct integration 56 | into Keycloak. Thanks for writing and providing this great piece of software! -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-autoapi==2.0.1 ; python_version >= "3.10" and python_version < "4.0" 2 | sphinx-rtd-theme==1.2.0 ; python_version >= "3.10" and python_version < "4.0" -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing with this library 2 | ========================= 3 | 4 | When testing your FastAPI application, the default tests will likely fail due to this middleware enforcing authentication. 5 | There are two main methods to test your application when using this middleware, one by simply providing valid access tokens and one by mocking the authentication. 6 | Mocking the authentication is the preferred approach as it allows independent testing of the middleware and the application. 7 | Note that you can still validate permissions if setup correctly. 8 | 9 | Prerequisites 10 | ------------- 11 | 12 | This example is based on pytest, but can be adapted to other testing frameworks. Some dependencies are required to run the tests: 13 | 14 | .. code-block:: bash 15 | 16 | poetry add --group dev pytest mock pytest-mock 17 | 18 | Mocking the authentication 19 | -------------------------- 20 | 21 | When not using authorization, you can simply mock the authentication middleware to not apply the middleware in the first place. 22 | To still use users and/or authorization, you need a different way how to pass users to the FastAPI path functions. 23 | Assuming you use a custom user mapper in your application, you can override the dependency to return a user object from your database. 24 | 25 | This scenario describes one option of using headers to pass the user information to the FastAPI path functions. 26 | Passing an `X-User` header would tell the overriden dependency which user to inject: 27 | 28 | .. code-block:: python 29 | 30 | # It is important that you import your own get_user dependency (when using a custom user mapper) 31 | # in order to be able to override it. If you use the built-in one, import that one instead. 32 | from your-library import get_user 33 | # or 34 | from fastapi_keycloak_middleware import get_user 35 | 36 | async def mocked_get_user(request: Request): 37 | """ 38 | This function can be used as FastAPI dependency to easily retrieve the user object from DB 39 | """ 40 | user = request.headers.get("X-User", "test_user_1") 41 | # Use whatever method you typically to fetch the user object 42 | return crud.user.get_by_username(sessionmanager.get_session(), user) 43 | 44 | @pytest.fixture(scope="session") 45 | def app(session_mocker): 46 | 47 | # Mock auth middleware, effectively remove it 48 | session_mocker.patch("fastapi_keycloak_middleware.setup_keycloak_middleware") 49 | 50 | # Its important to import the app after the middleware has been mocked 51 | from backend.main import app as backend_app 52 | 53 | # Override the get_user dependency with our mock method above 54 | backend_app.dependency_overrides[get_user] = mocked_get_user 55 | 56 | yield backend_app 57 | 58 | This code cannot be copied as is, you need to adapt it by either importing the correct ::code:`get_user` method to override 59 | and by implementing your own logic to fetch the user object from the database. With this setup, you can now test your application 60 | and specify the user to be used in the test by setting the `X-User` header: 61 | 62 | .. code-block:: python 63 | 64 | # Use default user1, do not specify anything 65 | def test_with_default_user(app): 66 | client = TestClient(app) 67 | response = client.get("/api/v1/your-endpoint") 68 | 69 | # Use specific user for all requests in this test 70 | def test_with_specific_user(app: FastAPI): 71 | client = TestClient(app, headers={"X-User": "your-other-user"}) 72 | response = client.get("/api/v1/your-endpoint") 73 | 74 | # Set user on a per request basis 75 | def test_with_specific_user_on_request(app): 76 | client = TestClient(app) 77 | response = client.get("/api/v1/your-endpoint", headers={"X-User": "test_user_2"}) 78 | 79 | Mocking authorization 80 | --------------------- 81 | 82 | When also using the authorization features of this library, the process needs to be extended to make sure 83 | the permissions are correctly passed as well and you are able to test your authorization logic. 84 | 85 | There are many methods how to implement this, all of them have in common that you need to override the 86 | `get_auth` dependency to return the permissions for the user you want to test with. The example below 87 | sets default roles based on the user, and introduces another header `X-Roles` that can be used to extend 88 | the permissions of a given user on a test-by-test basis. You can implement your own logic there as well, 89 | or rely on database queries to fetch the permissions, if you store your RBAC information in a database. 90 | 91 | .. code-block:: python 92 | 93 | async def mocked_get_auth(request: Request): 94 | user = request.headers.get("X-User", "test_user_1") 95 | 96 | # Set default roles based on the user 97 | if user.startswith("test_user_admin"): 98 | roles = ["Admin", "User"] 99 | elif user.startswith("test_user_guest"): 100 | roles = ["Guest"] 101 | elif user.startswith("test_user_"): 102 | roles = ["User"] 103 | else: 104 | roles = [user] 105 | 106 | # Add additional roles if requested 107 | requested_roles = request.headers.get("X-Roles", "") 108 | if requested_roles: 109 | roles += requested_roles.split(",") 110 | 111 | # Optionally, if you normally use an auth_mapper, you can apply it now 112 | # roles = auth_mapper(roles) 113 | # or if using async: 114 | # roles = await auth_mapper(roles) 115 | return roles 116 | 117 | @pytest.fixture(scope="session") 118 | def app(session_mocker): 119 | 120 | # Mock auth middleware, effectively remove it 121 | session_mocker.patch("fastapi_keycloak_middleware.setup_keycloak_middleware") 122 | 123 | # Its important to import the app after the middleware has been mocked 124 | from backend.main import app as backend_app 125 | 126 | # Override the get_user dependency with our mock method above 127 | backend_app.dependency_overrides[get_user] = mocked_get_user 128 | backend_app.dependency_overrides[get_auth] = mocked_get_auth 129 | 130 | yield backend_app 131 | 132 | With this setup, you can now test your application and specify the user to be used in the test by setting the `X-User` header 133 | and the roles by setting the `X-Roles` header: 134 | 135 | .. code-block:: python 136 | 137 | # Use default user1 with User role, do not specify anything 138 | def test_with_default_user(app): 139 | client = TestClient(app) 140 | response = client.get("/api/v1/your-endpoint") 141 | 142 | # Set user and roles on a per request basis 143 | def test_with_specific_user_and_roles_on_request(app): 144 | client = TestClient(app) 145 | response = client.get("/api/v1/your-endpoint", headers={"X-User": "test_user_2", "X-Roles": "Admin"}) 146 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage Guide 4 | =========== 5 | 6 | Basic Example 7 | ^^^^^^^^^^^^^ 8 | 9 | This is a very basic example on how to add the Middleware to a FastAPI application. All (full) examples are complete as is and can be run without modification. 10 | 11 | .. code-block:: python 12 | 13 | from fastapi import FastAPI 14 | from fastapi_keycloak_middleware import KeycloakConfiguration, setup_keycloak_middleware 15 | 16 | # Set up Keycloak 17 | keycloak_config = KeycloakConfiguration( 18 | url="https://sso.your-keycloak.com/auth/", 19 | realm="", 20 | client_id="", 21 | client_secret="", 22 | ) 23 | 24 | app = FastAPI() 25 | 26 | # Add middleware with basic config 27 | setup_keycloak_middleware( 28 | app, 29 | keycloak_configuration=keycloak_config, 30 | ) 31 | 32 | @app.get("/") 33 | async def root(): 34 | return {"message": "Hello World"} 35 | 36 | 37 | This is a minimal example of using the middleware and will already perform the following actions: 38 | 39 | * Parse the :code:`Authorization` header for a :code:`Bearer` token (the token scheme can be configured, see below). Return :code:`401` if no token is found. 40 | * Validate the token using the public key of the realm obtained from Keycloak. Return :code:`401` if the token is invalid or expired. 41 | * Extract user information from the token. The claims to use are configurable, by default the following claims are read: 42 | * :code:`sub` - part of the :code:`openid` scope, defining a mandatory, unique, immutable string identifier for the user 43 | * :code:`name` - part of the :code:`profile` scope, defining a human-readable name for the user 44 | * :code:`family_name` - part of the :code:`profile` scope, defining the user's family name 45 | * :code:`given_name` - part of the :code:`profile` scope, defining the user's given name 46 | * :code:`preferred_username` - part of the :code:`profile` scope, defining the user's preferred username. Note that as per openid specifications, you must not rely on this claim to be unique for all users 47 | * :code:`email` - part of the :code:`email` scope, defining the user's email address 48 | * Get the user object based on the extracted information. As no custom callback function is provided, it will return an instance of :code:`FastApiUser` containing extracted information. 49 | * Add proper 401 and 403 responses to the OpenAPI schema 50 | 51 | .. note:: 52 | **Authorization** is disabled by default, so no authorization scopes will be stored. 53 | 54 | Middleware Order 55 | ^^^^^^^^^^^^^^^^ 56 | 57 | When working with multiple middlewares in FastAPI, the order in which they are added is crucial. FastAPI processes middlewares using a "shell model" - the middleware that is added **last** will be the **first** to process incoming requests and the **last** to process outgoing responses. 58 | 59 | This means middlewares are processed in reverse order of how they were added: 60 | 61 | .. code-block:: python 62 | 63 | app = FastAPI() 64 | 65 | # This middleware will run SECOND for requests 66 | app.add_middleware(SomeMiddleware) 67 | 68 | # This middleware will run FIRST for requests 69 | app.add_middleware(AnotherMiddleware) 70 | 71 | **Common Issue with CORS** 72 | 73 | A frequent problem occurs when adding CORS middleware after the Keycloak authentication middleware: 74 | 75 | .. code-block:: python 76 | :caption: ❌ Problematic order 77 | 78 | app = FastAPI() 79 | 80 | # Adding CORS after auth first - THIS CAUSES ISSUES! 81 | app.add_middleware( 82 | CORSMiddleware, 83 | allow_origins=["*"], 84 | allow_credentials=True, 85 | allow_methods=["*"], 86 | allow_headers=["*"], 87 | ) 88 | 89 | # Add Keycloak middleware second 90 | setup_keycloak_middleware( 91 | app, 92 | keycloak_configuration=keycloak_config, 93 | ) 94 | 95 | In this incorrect setup, the authentication middleware will process OPTIONS preflight requests 96 | (used by browsers for CORS checks) and return a 401 Unauthorized response because OPTIONS requests 97 | typically don't include authentication headers. This prevents the CORS middleware from properly 98 | handling the preflight request. 99 | 100 | **Correct Order** 101 | 102 | To fix this, add the CORS middleware **after** the authentication middleware, ensuring it processs OPTIONS requests first: 103 | 104 | .. code-block:: python 105 | :caption: ✅ Correct order 106 | 107 | app = FastAPI() 108 | 109 | # Add Keycloak middleware first 110 | setup_keycloak_middleware( 111 | app, 112 | keycloak_configuration=keycloak_config, 113 | ) 114 | 115 | # Add CORS middleware last (so it runs first for requests) 116 | app.add_middleware( 117 | CORSMiddleware, 118 | allow_origins=["*"], 119 | allow_credentials=True, 120 | allow_methods=["*"], 121 | allow_headers=["*"], 122 | ) 123 | 124 | With this correct order: 125 | 126 | 1. CORS middleware processes the request first and handles OPTIONS preflight requests immediately 127 | 2. For non-OPTIONS requests, the authentication middleware then validates the token 128 | 3. Your endpoint handlers receive properly authenticated requests 129 | 130 | .. note:: 131 | This same principle applies to other middlewares like rate limiting, request logging, or custom middlewares. Always consider what should happen first in the request processing chain. 132 | 133 | Keycloak Configuration Scheme 134 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 135 | 136 | The :code:`KeycloakConfiguration` class is used to configure the Keycloak connection. Refer to the :ref:`Classes API documentation` for a complete list of parameters that are supported. 137 | 138 | Change Authentication Scheme 139 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 140 | 141 | The authentication scheme is essentially the prefix of the :code:`Authorization` header. By default, the middleware will look for a :code:`Bearer` token. This can be changed by setting :code:`authentication_scheme` attribute of the :code:`KeycloakConfiguration` class: 142 | 143 | .. code-block:: python 144 | :emphasize-lines: 7 145 | 146 | # Set up Keycloak 147 | keycloak_config = KeycloakConfiguration( 148 | url="https://sso.your-keycloak.com/auth/", 149 | realm="", 150 | client_id="", 151 | client_secret="", 152 | authentication_scheme="Token" 153 | ) 154 | 155 | This example will accept headers like :code:`Authorization: Token ` instead of :code:`Bearer`. 156 | 157 | Customize User Getter 158 | ^^^^^^^^^^^^^^^^^^^^^ 159 | 160 | By default an instance of :code:`FastApiUser` will be returned. Refer to the API documentation for details about the information stored in this class. 161 | 162 | In many cases, you'll have your own user model you want to work with and therefore would like to return your own user object. This can be done by providing a custom callback function to the :code:`user_mapper` parameter of the middleware initialization class: 163 | 164 | .. code-block:: python 165 | :emphasize-lines: 1,2,3,9 166 | 167 | async def map_user(userinfo: typing.Dict[str, typing.Any]) -> User: 168 | # Do something with the userinfo 169 | return User() 170 | 171 | # Add middleware with basic config 172 | setup_keycloak_middleware( 173 | app, 174 | keycloak_configuration=keycloak_config, 175 | user_mapper=map_user, 176 | ) 177 | 178 | The :code:`userinfo` parameter is a dictionary containing the claims extracted from the access token. You can rely on all the claims to be populated, as tokens without these claims are rejected in a previous step by default. This behavior can be changed by setting the :code:`reject_on_missing_claim` parameter of the :code:`KeycloakConfiguration` class to :code:`False`, then you need to handle potentially missing claims yourself. 179 | 180 | This is an example of what you can expect using the default configuration: 181 | 182 | .. code-block:: json 183 | 184 | { 185 | "sub": "1234567890", 186 | "name": "John Doe", 187 | "family_name": "Doe", 188 | "given_name": "John", 189 | "preferred_username": "jdoe", 190 | "email": "jon.doe@example.com" 191 | } 192 | 193 | .. note:: 194 | Depending on your application architecture, you can of course also use this method to create the user, if users are allowed to **register** (not just authenticate) to your application via Keycloak. 195 | 196 | **Rejecting on missing claims** 197 | 198 | If you opt to allow missing claims, you can still reject the user authentication within your :code:`get_user` class by simply returning :code:`None`. 199 | 200 | **Database / ORM mappings** 201 | 202 | Be careful when working with ORM tools like SQLAlchemy. Assume you're adding an ORM mapped model here, the association to the database session would be lost when using it within the FastAPI endpoint later. This means that accessing attributes which have not been loaded yet (lazy loading) would lead to an exception being raised. In such a scenario, you can opt for pre-planning and eager load the required attributes, but it might be better to simply store a unique identifier to the user here and use this to retrieve the user object later using dependencies. See the following sections for details. 203 | 204 | Getting the User in FastAPI Endpoints 205 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 206 | 207 | This package provides a very simple dependency to retrieve the user object from the request. This is useful for simple cases, but for more advanced cases you may want to provide your own dependency. 208 | 209 | **Simple Example** 210 | 211 | .. code-block:: python 212 | 213 | from fastapi_keycloak_middleware import get_user 214 | 215 | @app.get("/") 216 | async def root(user: User = Depends(get_user)): 217 | return {"message": "Hello World"} 218 | 219 | This will return whatever was stored in the request either by the built-in function or your custom function to retrieve the user object. 220 | 221 | **Advanced Example** 222 | 223 | Now assume you've not stored a model here but some unique identifier, to avoid the lazy loading issue mentioned above. You can then use this to retrieve the user object from the database using a dependency: 224 | 225 | .. code-block:: python 226 | 227 | def get_user(request: Request, db: Session = Depends(get_db)): 228 | """ 229 | Custom dependency to retrieve the user object from the request. 230 | """ 231 | 232 | if "user" in request.scope: 233 | # Do whatever you need to get the user object from the database 234 | user = User.get_by_id(db, request.scope["user"].id) 235 | if user: 236 | return user 237 | 238 | # Handle missing user scenario 239 | raise HTTPException( 240 | status_code=401, 241 | detail="Unable to retrieve user from request", 242 | ) 243 | 244 | @app.get("/") 245 | async def root(user: User = Depends(get_user)): 246 | return {"message": "Hello World"} 247 | 248 | This will give you a user object that is still bound to the database session, so you can work with it like with any other ORM object. 249 | 250 | .. note:: 251 | :code:`get_db` is assumed to be an existing dependency to retrieve a Session to your database. 252 | 253 | Modify Extracted Claims 254 | ^^^^^^^^^^^^^^^^^^^^^^^ 255 | 256 | You can also configure the class to extract other / additional claims from the token and pass it to the :code:`user_mapper` function: 257 | 258 | .. code-block:: python 259 | :emphasize-lines: 7 260 | 261 | # Set up Keycloak 262 | keycloak_config = KeycloakConfiguration( 263 | url="https://sso.your-keycloak.com/auth/", 264 | realm="", 265 | client_id="", 266 | client_secret="", 267 | claims=["sub", "name", "email", "your-claim"], # Modify claims 268 | reject_on_missing_claim=False, # Control behaviour when claims are missing 269 | ) 270 | 271 | Swagger UI Integration 272 | ^^^^^^^^^^^^^^^^^^^^^^ 273 | 274 | It is also possible to configure the Swagger UI to display endpoints being protected by this middleware correctly 275 | and handle authentication to test the endpoints. This has not been in place in earlier versions, so it is disabled 276 | by default for now. 277 | 278 | To enable this feature, you need to set :code:`add_swagger_auth` flag to :code:`True` when configuring the middleware. 279 | Also, it is recommended to setup a separate Keycloak client for this purpose, as it should be a public client. This 280 | separate client is then configured using the :code:`swagger_client_id` parameter of :code:`KeycloakConfiguration`. 281 | 282 | .. code-block:: python 283 | :emphasize-lines: 6,7,8,9,15 284 | 285 | keycloak_config = KeycloakConfiguration( 286 | url="https://sso.your-keycloak.com/auth/", 287 | realm="", 288 | client_id="", 289 | client_secret="", 290 | swagger_client_id="", 291 | swagger_auth_scopes=["openid", "profile"], # Optional 292 | swagger_auth_pkce=True, # Optional 293 | swagger_scheme_name="keycloak" # Optional 294 | ) 295 | 296 | setup_keycloak_middleware( 297 | app, 298 | keycloak_configuration=keycloak_config, 299 | add_swagger_auth=True 300 | ) 301 | 302 | There are four more parameters that can be used to customize the Swagger UI integration: 303 | 304 | * :code:`swagger_openId_base_url` - The base URL for the OpenID Connect configuration that will be used by the Swagger UI. It is explained in this `Github Issue `_. This parameter allows you to specify a different base URL than the one in keycloak_configuration.url. This is particularly useful in Docker container scenarios where the internal and external URLs differ. Defaults to using the keycloak_configuration.url. 305 | * :code:`swagger_auth_scopes` - The scopes that should be selected by default when hitting the Authorize button in Swagger UI. Defaults to :code:`['openid', 'profile']` 306 | * :code:`swagger_auth_pkce` - Whether to use PKCE for the Swagger UI client. Defaults to :code:`True`. It is recommended to use Authorization Code Flow with PKCE for public clients instead of implicit flow. In Keycloak, this flow is called "Standard flow" 307 | * :code:`swagger_scheme_name` - The name of the OpenAPI security scheme. Usually there is no need to change this. 308 | 309 | Full Example 310 | ^^^^^^^^^^^^ 311 | 312 | Everything combined might look like the following. Important note: the KeycloakConfiguration.verify attribute maps to the 313 | [KeycloakOpenID](https://github.com/marcospereirampj/python-keycloak/blob/5957607ad07536b94d878c3ce5d403c212b35220/src/keycloak/keycloak_openid.py#L62) verify 314 | attribute, which must be the True or False bool or the str path to the CA bundle used for the cert. The default KeycloakConfiguration.verify value is True. 315 | 316 | .. code-block:: python 317 | 318 | from fastapi import FastAPI 319 | from fastapi_keycloak_middleware import KeycloakConfiguration, setup_keycloak_middleware 320 | 321 | # Set up Keycloak connection 322 | keycloak_config = KeycloakConfiguration( 323 | url="https://sso.your-keycloak.com/auth/", 324 | realm="", 325 | client_id="", 326 | client_secret="", 327 | claims=["sub", "name", "email", "your-claim"], # Modify claims 328 | reject_on_missing_claim=False, # Control behaviour when claims are missing 329 | verify="/ca.pem" # Can be True, False or the path to the CA file used to sign certs 330 | ) 331 | 332 | async def map_user(userinfo: typing.Dict[str, typing.Any]) -> User: 333 | """ 334 | Map userinfo extracted from the claim 335 | to something you can use in your application. 336 | 337 | You could 338 | - Verify user presence in your database 339 | - Create user if it doesn't exist (depending on your application architecture) 340 | """ 341 | user = make_sure_user_exists(userinfo) # Replace with your logic 342 | return user 343 | 344 | def get_user(request: Request, db: Session = Depends(get_db)): 345 | """ 346 | Custom dependency to retrieve the user object from the request. 347 | """ 348 | 349 | if "user" in request.scope: 350 | # Do whatever you need to get the user object from the database 351 | user = User.get_by_id(db, request.scope["user"].id) 352 | if user: 353 | return user 354 | 355 | # Handle missing user scenario 356 | raise HTTPException( 357 | status_code=401, 358 | detail="Unable to retrieve user from request", 359 | ) 360 | 361 | app = FastAPI() 362 | 363 | # Add middleware with basic config 364 | setup_keycloak_middleware( 365 | app, 366 | keycloak_configuration=keycloak_config, 367 | user_mapper=map_user, 368 | ) 369 | 370 | @app.get("/") 371 | async def root(user: User = Depends(get_user)): 372 | return {"message": "Hello World"} 373 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Middleware for FastAPI that supports authenticating users against Keycloak 3 | """ 4 | 5 | __version__ = "1.3.0" 6 | 7 | import logging 8 | 9 | from fastapi_keycloak_middleware.decorators.require_permission import require_permission 10 | from fastapi_keycloak_middleware.decorators.strip_request import strip_request 11 | from fastapi_keycloak_middleware.dependencies.check_permission import CheckPermissions 12 | from fastapi_keycloak_middleware.dependencies.get_auth import get_auth 13 | from fastapi_keycloak_middleware.dependencies.get_authorization_result import ( 14 | get_authorization_result, 15 | ) 16 | from fastapi_keycloak_middleware.dependencies.get_user import get_user 17 | from fastapi_keycloak_middleware.fast_api_user import FastApiUser 18 | from fastapi_keycloak_middleware.middleware import KeycloakMiddleware 19 | from fastapi_keycloak_middleware.schemas.authorization_methods import ( 20 | AuthorizationMethod, 21 | ) 22 | from fastapi_keycloak_middleware.schemas.authorization_result import AuthorizationResult 23 | from fastapi_keycloak_middleware.schemas.keycloak_configuration import ( 24 | KeycloakConfiguration, 25 | ) 26 | from fastapi_keycloak_middleware.schemas.match_strategy import MatchStrategy 27 | from fastapi_keycloak_middleware.setup import setup_keycloak_middleware 28 | 29 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 30 | 31 | __all__ = [ 32 | AuthorizationResult.__name__, 33 | KeycloakMiddleware.__name__, 34 | KeycloakConfiguration.__name__, 35 | AuthorizationMethod.__name__, 36 | MatchStrategy.__name__, 37 | FastApiUser.__name__, 38 | CheckPermissions.__name__, 39 | get_auth.__name__, 40 | get_user.__name__, 41 | get_authorization_result.__name__, 42 | require_permission.__name__, 43 | setup_keycloak_middleware.__name__, 44 | strip_request.__name__, 45 | ] 46 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/decorators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waza-ari/fastapi-keycloak-middleware/b2c0c483d565303c9875fd8912db92061be5d833/fastapi_keycloak_middleware/decorators/__init__.py -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/decorators/require_permission.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module providers a decorator that can be used to check if a user has a specific 3 | permission. 4 | """ 5 | 6 | # pylint: disable=logging-not-lazy,consider-using-f-string 7 | # NOTE: Using % formatting is the safest way as we allow custom loggers. 8 | # There could be custom loggers that handle arguments differently 9 | 10 | import logging 11 | import typing 12 | from collections import OrderedDict 13 | from collections.abc import Callable 14 | from functools import wraps 15 | from inspect import Parameter, signature 16 | from warnings import warn 17 | 18 | import starlette 19 | from fastapi import HTTPException 20 | from starlette.requests import Request 21 | 22 | from fastapi_keycloak_middleware.decorators.strip_request import strip_request 23 | from fastapi_keycloak_middleware.schemas.authorization_methods import ( 24 | AuthorizationMethod, 25 | ) 26 | from fastapi_keycloak_middleware.schemas.authorization_result import AuthorizationResult 27 | from fastapi_keycloak_middleware.schemas.match_strategy import MatchStrategy 28 | 29 | log = logging.getLogger(__name__) 30 | 31 | 32 | def require_permission( 33 | permissions: typing.Union[str, typing.List[str]], 34 | match_strategy: MatchStrategy = MatchStrategy.AND, 35 | ) -> Callable: 36 | """ 37 | This decorator can be used to enfore certain permissions for the path 38 | function it is applied to. 39 | 40 | :param permissions: The permissions that are required to access the path 41 | function. Can be a single string or a list of strings. 42 | :type permissions: typing.Union[str, typing.List[str]], optional 43 | :param match_strategy: The strategy that is used to match the permissions. 44 | Defaults to ``MatchStrategy.AND``. 45 | :type match_strategy: MatchStrategy, optional 46 | :return: The decorated function 47 | """ 48 | 49 | warn( 50 | "The decorator method is deprecated and will be removed in the next major version. " 51 | "Please transition to dependency based permission checking.", 52 | DeprecationWarning, 53 | stacklevel=2, 54 | ) 55 | 56 | # Check if permissions is a single string, convert to list if so 57 | if isinstance(permissions, str): 58 | permissions = [permissions] 59 | 60 | # Check if match_strategy is valid 61 | if match_strategy not in MatchStrategy: 62 | raise ValueError(f"Invalid match strategy. Must be 'and' or 'or'. Got {match_strategy}") 63 | 64 | def _check_permission( 65 | requested_permission: typing.List[str], allowed_scopes: typing.List[str] 66 | ) -> typing.Tuple[bool, typing.List[str]]: 67 | """ 68 | Check if the user has permission based on the matching strategy 69 | """ 70 | # Get matching permissions 71 | matching_permissions = [ 72 | permission for permission in requested_permission if permission in allowed_scopes 73 | ] 74 | 75 | if match_strategy == MatchStrategy.AND: 76 | return ( 77 | set(requested_permission) == set(matching_permissions), 78 | matching_permissions, 79 | ) 80 | return len(matching_permissions) > 0, matching_permissions 81 | 82 | def decorator(func): 83 | """ 84 | Inner decorator 85 | """ 86 | 87 | # Remove the request argument by applying the provided decorator. See 88 | # https://stackoverflow.com/questions/44548047/creating-decorator-out-of-another-decorator-python 89 | func = strip_request(func) 90 | 91 | @wraps(func) 92 | async def wrapper(*args, **kwargs): 93 | request = kwargs.get("request", None) 94 | 95 | assert isinstance(request, Request) 96 | 97 | user = request.get("user", None) 98 | 99 | log.debug(f"Checking permission {permissions} for user {str(user)}") 100 | 101 | allowed_scopes = request.get("auth", []) 102 | 103 | # Check if user has permission 104 | allowed, matching_permissions = _check_permission(permissions, allowed_scopes) 105 | 106 | if allowed: 107 | log.info(f"Permission granted for user {str(user)}") 108 | log.debug(f"Matching permissions: {matching_permissions}") 109 | 110 | # Check if "matched_scopes" is in function signature. 111 | # If so, add it to the function call. 112 | if "authorization_result" in signature(func).parameters.keys(): 113 | kwargs["authorization_result"] = AuthorizationResult( 114 | method=AuthorizationMethod.CLAIM, 115 | authorized=True, 116 | matched_scopes=matching_permissions, 117 | ) 118 | 119 | return await func(*args, **kwargs) 120 | 121 | log.warning(f"Permission {permissions} denied for user {str(user)}") 122 | raise HTTPException(status_code=403, detail="Permission denied") 123 | 124 | # Override signature 125 | # See https://peps.python.org/pep-0362/#signature-object 126 | # Note that the signature is immutable, so we need to create a new one 127 | sig = signature(func) 128 | parameters: OrderedDict = sig.parameters 129 | if "request" in parameters.keys(): 130 | return wrapper 131 | 132 | parameters = [ 133 | Parameter( 134 | name="request", 135 | kind=Parameter.POSITIONAL_OR_KEYWORD, 136 | default=Parameter.empty, 137 | annotation=starlette.requests.Request, 138 | ), 139 | *parameters.values(), 140 | ] 141 | new_sig = sig.replace(parameters=parameters, return_annotation=sig.return_annotation) 142 | wrapper.__signature__ = new_sig 143 | return wrapper 144 | 145 | return decorator 146 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/decorators/strip_request.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convenience wrapper that handles removing the request argument from the function 3 | arguments if needed. As the actual function does not include the argument, it 4 | would lead to a Python exception if the argument is not removed. 5 | 6 | This wrapper will check if the function contains the request argument and if 7 | not, will remove it from kwargs before calling the function. 8 | """ 9 | 10 | from functools import wraps 11 | from inspect import signature 12 | 13 | 14 | def strip_request(func): 15 | """ 16 | Wrapper that strips the request argument from kwargs 17 | """ 18 | 19 | @wraps(func) 20 | async def wrapper(*args, **kwargs): 21 | parameters = signature(func).parameters 22 | if "request" in parameters.keys(): 23 | return await func(*args, **kwargs) 24 | 25 | if "request" in kwargs: 26 | del kwargs["request"] 27 | return await func(*args, **kwargs) 28 | 29 | return wrapper 30 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waza-ari/fastapi-keycloak-middleware/b2c0c483d565303c9875fd8912db92061be5d833/fastapi_keycloak_middleware/dependencies/__init__.py -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/dependencies/check_permission.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a FastAPI dependency that can be used to validate that a user 3 | has a certain permission before accessing a path 4 | """ 5 | 6 | import logging 7 | import typing 8 | 9 | from fastapi import Depends, HTTPException 10 | 11 | from ..schemas.authorization_methods import AuthorizationMethod 12 | from ..schemas.authorization_result import AuthorizationResult 13 | from ..schemas.match_strategy import MatchStrategy 14 | from .get_auth import get_auth 15 | from .get_user import get_user 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | class CheckPermissions: 21 | """ 22 | This class can be used as FastAPI dependency to check if a user has 23 | the required permissions to access a path 24 | """ 25 | 26 | def __init__( 27 | self, 28 | required_permission: typing.Union[str, typing.List[str]], 29 | match_strategy: MatchStrategy = MatchStrategy.AND, 30 | ): 31 | """ 32 | Initialize the dependency. The required permission can be a single string 33 | or a list of strings. The match strategy can be either AND or OR, meaning 34 | that all permissions must be present or at least one permission must be present. 35 | 36 | :param required_permission: _description_ 37 | :type required_permission: typing.Union[str, typing.List[str]] 38 | :param match_strategy: _description_, defaults to MatchStrategy.AND 39 | :type match_strategy: MatchStrategy, optional 40 | """ 41 | 42 | # Check if permissions is a single string, convert to list if so 43 | if isinstance(required_permission, str): 44 | required_permission = [required_permission] 45 | 46 | self.required_permission: typing.List[str] = required_permission 47 | 48 | # Check if match_strategy is valid 49 | if match_strategy not in MatchStrategy: 50 | raise ValueError(f"Invalid match strategy. Must be 'and' or 'or'. Got {match_strategy}") 51 | 52 | self.match_strategy = match_strategy 53 | 54 | def __call__(self, user=Depends(get_user), auth: typing.List[str] = Depends(get_auth)): 55 | log.debug(f"Checking permission {self.required_permission} for user {str(user)}") 56 | 57 | # Get matching permissions 58 | matching_permissions = [ 59 | permission for permission in self.required_permission if permission in auth 60 | ] 61 | 62 | # If match strategy is AND, all permissions must be present 63 | # If match strategy is OR, at least one permission must be present 64 | if ( 65 | self.match_strategy == MatchStrategy.AND 66 | and set(self.required_permission) == set(matching_permissions) 67 | ) or (self.match_strategy == MatchStrategy.OR and len(matching_permissions) > 0): 68 | log.info(f"Permission granted for user {str(user)}") 69 | log.debug(f"Matching permissions: {matching_permissions}") 70 | return AuthorizationResult( 71 | authorized=True, 72 | matched_scopes=matching_permissions, 73 | method=AuthorizationMethod.CLAIM, 74 | ) 75 | 76 | # No matching permissions found, raise an exception 77 | log.warning(f"Permission {self.required_permission} denied for user {str(user)}") 78 | raise HTTPException(status_code=403, detail="Permission denied") 79 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/dependencies/get_auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a helper function that can be used as FastAPI dependency 3 | to easily retrieve the resolved permissions for the user 4 | """ 5 | 6 | from fastapi import Request 7 | 8 | 9 | async def get_auth(request: Request): 10 | """ 11 | This function can be used as FastAPI dependency 12 | to easily retrieve the user object 13 | """ 14 | if "auth" in request.scope: 15 | auth = request.scope["auth"] 16 | 17 | # Check if auth is a single string, convert to list if so 18 | if isinstance(auth, str): 19 | return [auth] 20 | 21 | return request.scope["auth"] 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/dependencies/get_authorization_result.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a Dependency that results the authorization result 3 | """ 4 | 5 | from typing import Optional 6 | from warnings import warn 7 | 8 | from fastapi_keycloak_middleware.schemas.authorization_result import AuthorizationResult 9 | 10 | 11 | async def get_authorization_result( 12 | authorization_result: Optional[AuthorizationResult] = None, 13 | ): 14 | """ 15 | This function can be used as FastAPI dependency 16 | and returns the authorization result 17 | """ 18 | warn( 19 | "The decorator method is deprecated and will be removed in the next major version. " 20 | "Please transition to dependency based permission checking.", 21 | DeprecationWarning, 22 | stacklevel=2, 23 | ) 24 | yield authorization_result 25 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/dependencies/get_user.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a helper function that can be used as FastAPI dependency 3 | to easily retrieve the user object 4 | """ 5 | 6 | from fastapi import Request 7 | 8 | 9 | async def get_user(request: Request): 10 | """ 11 | This function can be used as FastAPI dependency 12 | to easily retrieve the user object 13 | """ 14 | if "user" in request.scope: 15 | return request.scope["user"] 16 | 17 | return None 18 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains exceptions used by the middleware. 3 | """ 4 | 5 | 6 | class AuthHeaderMissing(Exception): 7 | """ 8 | Raised when the Authorization header is missing. 9 | """ 10 | 11 | 12 | class AuthInvalidToken(Exception): 13 | """ 14 | Raised when the token is invalid or malformed. 15 | """ 16 | 17 | 18 | class AuthKeycloakError(Exception): 19 | """ 20 | Raised when there was a problem communicating with Keycloak 21 | """ 22 | 23 | 24 | class AuthClaimMissing(Exception): 25 | """ 26 | Raised when one of the expected claims is missing. 27 | """ 28 | 29 | 30 | class AuthUserError(Exception): 31 | """ 32 | Raised when there was a problem fetching the user object 33 | """ 34 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/fast_api_user.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a base user implementation 3 | 4 | It is mainly used if the user does not provide a custom function to retrieve 5 | the user based on the token claims 6 | """ 7 | 8 | import typing 9 | 10 | from starlette.authentication import BaseUser 11 | 12 | 13 | class FastApiUser(BaseUser): 14 | """Sample API User that gives basic functionality""" 15 | 16 | def __init__(self, first_name: str, last_name: str, user_id: typing.Any): 17 | """ 18 | FastAPIUser Constructor 19 | """ 20 | self.first_name = first_name 21 | self.last_name = last_name 22 | self.user_id = user_id 23 | 24 | @property 25 | def is_authenticated(self) -> bool: 26 | """ 27 | Checks if the user is authenticated. This method essentially does nothing, 28 | but it could implement session logic for example. 29 | """ 30 | return True 31 | 32 | @property 33 | def display_name(self) -> str: 34 | """Display name of the user""" 35 | return f"{self.first_name} {self.last_name}" 36 | 37 | @property 38 | def identity(self) -> str: 39 | """Identification attribute of the user""" 40 | return self.user_id 41 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/keycloak_backend.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the Keycloak backend. 3 | 4 | It is used by the middleware to perform the actual authentication. 5 | """ 6 | 7 | import logging 8 | import typing 9 | 10 | import keycloak 11 | from jwcrypto import jwk 12 | from keycloak import KeycloakOpenID 13 | from starlette.authentication import AuthenticationBackend, BaseUser 14 | from starlette.requests import HTTPConnection 15 | 16 | from fastapi_keycloak_middleware.exceptions import ( 17 | AuthClaimMissing, 18 | AuthHeaderMissing, 19 | AuthInvalidToken, 20 | AuthKeycloakError, 21 | AuthUserError, 22 | ) 23 | from fastapi_keycloak_middleware.fast_api_user import FastApiUser 24 | from fastapi_keycloak_middleware.schemas.authorization_methods import ( 25 | AuthorizationMethod, 26 | ) 27 | from fastapi_keycloak_middleware.schemas.keycloak_configuration import ( 28 | KeycloakConfiguration, 29 | ) 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | class KeycloakBackend(AuthenticationBackend): 35 | """ 36 | Backend to perform authentication using Keycloak 37 | """ 38 | 39 | def __init__( 40 | self, 41 | keycloak_configuration: KeycloakConfiguration, 42 | user_mapper: typing.Callable[[typing.Dict[str, typing.Any]], typing.Awaitable[typing.Any]] 43 | | None, 44 | ): 45 | self.keycloak_configuration = keycloak_configuration 46 | self.keycloak_openid = self._get_keycloak_openid() 47 | if not self.keycloak_configuration.use_introspection_endpoint: 48 | self.public_key = self._get_public_key() 49 | self.get_user = user_mapper if user_mapper else KeycloakBackend._get_user 50 | 51 | def _get_keycloak_openid(self) -> KeycloakOpenID: 52 | """ 53 | Instance-scoped KeycloakOpenID object 54 | """ 55 | return KeycloakOpenID( 56 | server_url=self.keycloak_configuration.url, 57 | client_id=self.keycloak_configuration.client_id, 58 | realm_name=self.keycloak_configuration.realm, 59 | client_secret_key=self.keycloak_configuration.client_secret, 60 | verify=self.keycloak_configuration.verify, 61 | ) 62 | 63 | def _get_public_key(self) -> jwk.JWK: 64 | """ 65 | Returns the public key used to validate tokens. 66 | This is only used if the introspection endpoint is not used. 67 | """ 68 | log.debug("Fetching public key from Keycloak server at %s", self.keycloak_configuration.url) 69 | try: 70 | key = ( 71 | "-----BEGIN PUBLIC KEY-----\n" 72 | + self.keycloak_openid.public_key() 73 | + "\n-----END PUBLIC KEY-----" 74 | ) 75 | return jwk.JWK.from_pem(key.encode("utf-8")) 76 | except keycloak.exceptions.KeycloakGetError as exc: 77 | log.error("Failed to fetch public key from Keycloak server: %s", exc.error_message) 78 | raise AuthKeycloakError from exc 79 | 80 | @staticmethod 81 | async def _get_user(userinfo: typing.Dict[str, typing.Any]) -> BaseUser: 82 | """ 83 | Default implementation of the get_user method. 84 | """ 85 | return FastApiUser( 86 | first_name=userinfo.get("given_name", ""), 87 | last_name=userinfo.get("family_name", ""), 88 | user_id=userinfo.get("user_id", ""), 89 | ) 90 | 91 | async def authenticate(self, conn: HTTPConnection) -> tuple[list[str], BaseUser | None]: 92 | """ 93 | The authenticate method is invoked each time a route is called that 94 | the middleware is applied to. 95 | """ 96 | 97 | # If this is a websocket connection, we can extract the token 98 | # from the cookies 99 | if ( 100 | self.keycloak_configuration.enable_websocket_support 101 | and conn.headers.get("upgrade") == "websocket" 102 | ): 103 | auth_header = conn.cookies.get(self.keycloak_configuration.websocket_cookie_name, None) 104 | else: 105 | auth_header = conn.headers.get("Authorization", None) 106 | 107 | if not auth_header: 108 | raise AuthHeaderMissing 109 | 110 | # Check if token starts with the authentication scheme 111 | token = auth_header.split(" ") 112 | if len(token) != 2 or token[0] != self.keycloak_configuration.authentication_scheme: 113 | raise AuthInvalidToken 114 | 115 | # Depending on the chosen method by the user, either 116 | # use the introspection endpoint or decode the token 117 | if self.keycloak_configuration.use_introspection_endpoint: 118 | log.debug("Using introspection endpoint to validate token") 119 | # Call introspect endpoint to check if token is valid 120 | try: 121 | token_info = await self.keycloak_openid.a_introspect(token[1]) 122 | except keycloak.exceptions.KeycloakPostError as exc: 123 | raise AuthKeycloakError from exc 124 | else: 125 | log.debug("Using keycloak public key to validate token") 126 | # Decode Token locally using the public key 127 | token_info = await self.keycloak_openid.a_decode_token( 128 | token[1], 129 | self.keycloak_configuration.validate_token, 130 | **self.keycloak_configuration.validation_options, 131 | key=self.public_key, 132 | ) 133 | 134 | # Calculate claims to extract 135 | # Default is user configured claims 136 | claims = self.keycloak_configuration.claims 137 | # If device auth is enabled + device claim is present... 138 | if ( 139 | self.keycloak_configuration.enable_device_authentication 140 | and self.keycloak_configuration.device_authentication_claim in token_info 141 | ): 142 | # ...only add the device auth claim to the claims to extract 143 | claims = [self.keycloak_configuration.device_authentication_claim] 144 | # If claim based authorization is enabled... 145 | if self.keycloak_configuration.authorization_method == AuthorizationMethod.CLAIM: 146 | # ...add the authorization claim to the claims to extract 147 | claims.append(self.keycloak_configuration.authorization_claim) 148 | 149 | # Extract claims from token 150 | user_info = {} 151 | for claim in claims: 152 | try: 153 | user_info[claim] = token_info[claim] 154 | except KeyError: 155 | log.warning("Claim %s is configured but missing in the token", claim) 156 | if self.keycloak_configuration.reject_on_missing_claim: 157 | log.warning("Rejecting request because of missing claim") 158 | raise AuthClaimMissing from KeyError 159 | log.debug("Backend is configured to ignore missing claims, continuing...") 160 | 161 | # Handle Authorization depending on the Claim Method 162 | scope_auth = None 163 | if self.keycloak_configuration.authorization_method == AuthorizationMethod.CLAIM: 164 | if self.keycloak_configuration.authorization_claim not in token_info: 165 | raise AuthClaimMissing 166 | scope_auth = token_info[self.keycloak_configuration.authorization_claim] 167 | 168 | # Check if the device authentication claim is present and evaluated to true 169 | # If so, the rest (mapping claims, user mapper, authorization) is skipped 170 | if self.keycloak_configuration.enable_device_authentication: 171 | log.debug("Device authentication is enabled, checking for device claim") 172 | try: 173 | if token_info[self.keycloak_configuration.device_authentication_claim]: 174 | log.info("Request contains a device token, skipping user mapping") 175 | return scope_auth, None 176 | except KeyError: 177 | log.debug( 178 | "Device authentication claim is missing in the token, " 179 | "proceeding with normal authentication" 180 | ) 181 | 182 | # Call user function to get user object 183 | try: 184 | user = await self.get_user(user_info) 185 | except Exception as exc: 186 | log.warning( 187 | "Error while getting user object: %s. " 188 | "The user-provided function raised an exception", 189 | exc, 190 | ) 191 | raise AuthUserError from exc 192 | 193 | if not user: 194 | log.warning("User object is None. The user-provided function returned None") 195 | raise AuthUserError 196 | 197 | return scope_auth, user 198 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides the middleware for the FastAPI framework. 3 | 4 | It is inspired by the fastapi-auth-middleware package published 5 | here: https://github.com/code-specialist/fastapi-auth-middleware 6 | """ 7 | 8 | import logging 9 | import re 10 | import typing 11 | 12 | from jwcrypto.common import JWException 13 | from starlette.requests import HTTPConnection 14 | from starlette.responses import JSONResponse 15 | from starlette.types import ASGIApp, Receive, Scope, Send 16 | 17 | from fastapi_keycloak_middleware.exceptions import ( 18 | AuthHeaderMissing, 19 | AuthInvalidToken, 20 | AuthUserError, 21 | ) 22 | from fastapi_keycloak_middleware.keycloak_backend import KeycloakBackend 23 | from fastapi_keycloak_middleware.schemas.keycloak_configuration import ( 24 | KeycloakConfiguration, 25 | ) 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | 30 | class KeycloakMiddleware: 31 | """ 32 | This class represents the middleware for FastAPI. It will authenticate 33 | a user based on a token. It currently only supports one backend 34 | (Keycloak backend). 35 | 36 | The middleware will add the user object to the request object. It 37 | optionally can also compile a list of scopes and add it to the request 38 | object as well, which can later be used for authorization. 39 | 40 | :param app: The FastAPI app instance, is automatically passed by FastAPI 41 | :type app: FastAPI 42 | :param keycloak_configuration: KeyCloak configuration object. For potential 43 | options, see the KeycloakConfiguration schema. 44 | :type keycloak_configuration: KeycloakConfiguration 45 | :param exclude_patterns: List of paths that should be excluded from authentication. 46 | Defaults to an empty list. The strings will be compiled to regular expressions 47 | and used to match the path. If the path matches, the middleware 48 | will skip authentication. 49 | :type exclude_patterns: typing.List[str], optional 50 | :param user_mapper: Custom async function that gets the userinfo extracted from AT 51 | and should return a representation of the user that is meaningful to you, 52 | the user of this library, defaults to None 53 | :type user_mapper: 54 | typing.Callable[ [typing.Dict[str, typing.Any]], typing.Awaitable[typing.Any] ] 55 | optional 56 | :param scope_mapper: Custom async function that transforms the claim values 57 | extracted from the token to permissions meaningful for your application, 58 | defaults to None 59 | :type scope_mapper: 60 | typing.Callable[[typing.List[str]], typing.Awaitable[typing.List[str]]], optional 61 | """ 62 | 63 | def __init__( 64 | self, 65 | app: ASGIApp, 66 | keycloak_configuration: KeycloakConfiguration, 67 | exclude_patterns: typing.List[str] | None = None, 68 | user_mapper: typing.Callable[[typing.Dict[str, typing.Any]], typing.Awaitable[typing.Any]] 69 | | None = None, 70 | scope_mapper: typing.Callable[[typing.List[str]], typing.Awaitable[typing.List[str]]] 71 | | None = None, 72 | ): 73 | """Middleware constructor""" 74 | log.info("Initializing Keycloak Middleware") 75 | self.app = app 76 | self.backend: KeycloakBackend = KeycloakBackend( 77 | keycloak_configuration=keycloak_configuration, 78 | user_mapper=user_mapper, 79 | ) 80 | self.scope_mapper = scope_mapper 81 | self.inspect_websockets = keycloak_configuration.enable_websocket_support 82 | log.debug("Keycloak Middleware initialized") 83 | 84 | # Try to compile patterns 85 | self.exclude_paths = [] 86 | if exclude_patterns and isinstance(exclude_patterns, list): 87 | for path in exclude_patterns: 88 | try: 89 | self.exclude_paths.append(re.compile(path)) 90 | except re.error: 91 | log.error("Could not compile regex for exclude pattern %s", path) 92 | 93 | async def _exclude_path(self, path: str) -> bool: 94 | """ 95 | Checks if a path should be excluded from authentication 96 | 97 | :param path: The path to check 98 | :type path: str 99 | :return: True if the path should be excluded, False otherwise 100 | :rtype: bool 101 | """ 102 | for pattern in self.exclude_paths: 103 | if pattern.match(path): 104 | return True 105 | return False 106 | 107 | async def __call__(self, scope: Scope, receive: Receive, send: Send): 108 | log.debug("Keycloak Middleware is handling request") 109 | 110 | supported_protocols = ["http"] 111 | if self.inspect_websockets: 112 | supported_protocols.append("websocket") 113 | 114 | if scope["type"] not in supported_protocols: # Filter for relevant requests 115 | log.debug("Skipping non-HTTP request") 116 | await self.app(scope, receive, send) # pragma nocover # Bypass 117 | return 118 | 119 | # Extract path from scope 120 | path = scope["path"] 121 | if await self._exclude_path(path): 122 | log.debug("Skipping authentication for excluded path %s", path) 123 | await self.app(scope, receive, send) 124 | return 125 | 126 | connection = HTTPConnection(scope) # Scoped connection 127 | 128 | try: # to Authenticate 129 | # Run Backend authentication 130 | 131 | log.info("Trying to authenticate user") 132 | 133 | auth, user = await self.backend.authenticate(connection) 134 | 135 | log.debug("User has been authenticated successfully") 136 | 137 | # Map scope if needed 138 | if self.scope_mapper: 139 | log.debug("Calling user provided scope mapper") 140 | auth = await self.scope_mapper(auth) 141 | 142 | scope["auth"], scope["user"] = auth, user 143 | 144 | except AuthHeaderMissing: # Request has no 'Authorization' HTTP Header 145 | response = JSONResponse( 146 | {"detail": "Your request is missing an 'Authorization' HTTP header"}, 147 | status_code=401, 148 | ) 149 | log.warning("Request is missing an 'Authorization' HTTP header") 150 | await response(scope, receive, send) 151 | return 152 | 153 | except AuthUserError: 154 | response = JSONResponse( 155 | {"detail": "Could not find a user based on this token"}, status_code=401 156 | ) 157 | log.warning("Could not find a user based on the provided token") 158 | await response(scope, receive, send) 159 | return 160 | 161 | except AuthInvalidToken: 162 | response = JSONResponse( 163 | {"detail": "Unable to verify provided access token"}, status_code=401 164 | ) 165 | log.warning("Provided access token could not be validated") 166 | await response(scope, receive, send) 167 | return 168 | 169 | except JWException as exc: 170 | response = JSONResponse( 171 | {"detail": f"Error while validating access token: {exc}"}, 172 | status_code=401, 173 | ) 174 | log.warning("An error occurred while validating the token") 175 | await response(scope, receive, send) 176 | return 177 | 178 | except Exception as exc: # pylint: disable=broad-except 179 | response = JSONResponse( 180 | {"detail": f"An error occurred: {exc.__class__.__name__}"}, 181 | status_code=401, 182 | ) 183 | log.error("An error occurred while authenticating the user") 184 | log.exception(exc) 185 | await response(scope, receive, send) 186 | return 187 | 188 | log.debug("Sending request to next middleware") 189 | await self.app(scope, receive, send) # Token is valid 190 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waza-ari/fastapi-keycloak-middleware/b2c0c483d565303c9875fd8912db92061be5d833/fastapi_keycloak_middleware/schemas/__init__.py -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/authorization_methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains an Enum specifying the authorization methods. 3 | """ 4 | 5 | from enum import Enum 6 | 7 | 8 | class AuthorizationMethod(Enum): 9 | """ 10 | This Enum can be used to set authorization methods. Please use the Enum values 11 | instead of the values behind the Enums. 12 | 13 | Supported options are: 14 | 15 | - ``AuthorizationMethod.NONE``: No authorization is performed. 16 | - ``AuthorizationMethod.CLAIM``: Authorization is performed by extracting 17 | the authorization scopes from a claim. 18 | """ 19 | 20 | NONE = 0 21 | CLAIM = 1 22 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/authorization_result.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the schema holding an authorization result 3 | 4 | It is used by the FastAPI dependency to return the result of the authorization 5 | for further processing by the API endpoint 6 | """ 7 | 8 | import typing 9 | 10 | from pydantic import BaseModel, Field 11 | 12 | from fastapi_keycloak_middleware.schemas.authorization_methods import ( 13 | AuthorizationMethod, 14 | ) 15 | 16 | 17 | class AuthorizationResult(BaseModel): # pylint: disable=too-few-public-methods 18 | """ 19 | This class contains the schema representing an authorization result. 20 | 21 | The following attributes will be set when returning the class in your 22 | path function: 23 | """ 24 | 25 | #: The method that was used to authorize the user 26 | method: typing.Union[None, AuthorizationMethod] = Field( 27 | default=None, 28 | title="Method", 29 | description="The method used to authorize the user.", 30 | ) 31 | #: Whether the user is authorized or not 32 | authorized: bool = Field( 33 | default=False, 34 | title="Authorized", 35 | description="Whether the user is authorized or not.", 36 | ) 37 | #: The scopes that matched the user's scopes 38 | matched_scopes: typing.List[str] = Field( 39 | default=[], 40 | title="Matched Scopes", 41 | description="The scopes that matched the user's scopes.", 42 | ) 43 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/exception_response.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pydantic schema used to represent an exception response. 3 | """ 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | class ExceptionResponse(BaseModel): 9 | """ 10 | Schema used to describe an exception response 11 | """ 12 | 13 | detail: str | None = None 14 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/keycloak_configuration.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the schema to configure Keycloak. 3 | """ 4 | 5 | from typing import Optional, Union 6 | 7 | from jwcrypto import jwk 8 | from pydantic import BaseModel, ConfigDict, Field 9 | 10 | from fastapi_keycloak_middleware.schemas.authorization_methods import ( 11 | AuthorizationMethod, 12 | ) 13 | 14 | 15 | class KeycloakConfiguration(BaseModel): # pylint: disable=too-few-public-methods 16 | """ 17 | This is a Pydantic schema used to pass backend configuration 18 | for the Keycloak Backend to the middleware. 19 | 20 | :param realm: Keycloak realm that should be used for token authentication. 21 | :type realm: str 22 | :param url: URL of the Keycloak server. If you use legacy Keycloak versions 23 | or still have the auth context, you need to add the auth context to the URL. 24 | :type url: str 25 | :param client_id: Client ID of the client used to validate the token. 26 | :type client_id: str 27 | :param swagger_client_id: Client ID for swagger UI authentication. Defaults to None. 28 | :type swagger_client_id: str, optional 29 | :param client_secret: Client secret of the client used to validate the token. 30 | The client secret is only needed if you use the introspection endpoint. 31 | :type client_secret: str, optional 32 | :param claims: List of claims that should be extracted from the access token. 33 | Defaults to 34 | ``["sub", "name", "family_name", "given_name", "preferred_username", "email"]``. 35 | :type claims: list[str], optional 36 | :param reject_on_missing_claim: Whether to reject the request if a claim is missing. 37 | Defaults to ``True``. 38 | :type reject_on_missing_claim: bool, optional 39 | :param authentication_scheme: Authentication scheme to use. Defaults to ``Bearer``. 40 | :type authentication_scheme: str, optional 41 | :param authorization_method: Authorization method to use. Defaults to ``NONE``. 42 | :type authorization_method: AuthorizationMethod, optional 43 | :param authorization_claim: Claim to use for extracting authorization scopes. 44 | Defaults to ``roles``. 45 | :type authorization_claim: str, optional 46 | :param use_introspection_endpoint: Whether to use the introspection endpoint 47 | for token validation. Should not be needed for Keycloak in general 48 | as Keycloak doesn't support opaque tokens. Defaults to ``False``. 49 | :type use_introspection_endpoint: bool, optional 50 | :param enable_device_authentication: Whether to enable device authentication. 51 | If device authentication is enabled, the middleware will ignore required user 52 | claims and not attempt to map the user. The token will be validated and then the 53 | request forwarded unmodified. Defaults to ``False``. 54 | :type enable_device_authentication: bool, optional 55 | :param device_authentication_claim: This claim will be used to check if the token 56 | is a device token. Defaults to ``is_device``. This is only used if 57 | ``enable_device_authentication`` is set to ``True``. The value 58 | is extracted from the claim and checked if its a truthy value. 59 | To be specific, ``bool(value)`` must evaluate to ``True``. 60 | :param verify: Whether to verify SSL connection. Defaults to ``True`` 61 | :type verify: Union[bool,str], optional 62 | :param validate_token: Whether to validate the token. Defaults to ``True``. 63 | :type validate_token: bool, optional 64 | :param validation_options: Decode options that are passed to `JWCrypto`'s JWT 65 | constructor. Defaults to ``{}``. See the following project for an overview of 66 | acceptable options: https://jwcrypto.readthedocs.io/en/latest/jwt.html 67 | :type validation_options: dict[str, Union[str, dict[str, Union[None, str]], list[str]]], 68 | optional 69 | :param enable_websocket_support: Whether to enable WebSocket support. Defaults to ``True``. 70 | :type enable_websocket_support: bool, optional 71 | :param websocket_cookie_name: Name of the cookie that contains the access token. 72 | Defaults to ``access_token``. 73 | :type websocket_cookie_name: str, optional 74 | """ 75 | 76 | model_config = ConfigDict(arbitrary_types_allowed=True) 77 | 78 | realm: str = Field(title="Realm", description="The realm to use.") 79 | url: str = Field(title="URL", description="The URL of the Keycloak server.") 80 | client_id: str = Field(title="Client ID", description="The client ID.") 81 | swagger_client_id: Optional[str] = Field( 82 | default=None, title="Swagger Client ID", description="The client ID for the swagger UI." 83 | ) 84 | client_secret: Optional[str] = Field( 85 | default=None, title="Client Secret", description="The client secret." 86 | ) 87 | claims: list[str] = Field( 88 | default=[ 89 | "sub", 90 | "name", 91 | "family_name", 92 | "given_name", 93 | "preferred_username", 94 | "email", 95 | ], 96 | title="Claims", 97 | description="The claims to add to the user object.", 98 | ) 99 | reject_on_missing_claim: bool = Field( 100 | default=True, 101 | title="Reject on Missing Claim", 102 | description="Whether to reject the request if a claim is missing.", 103 | ) 104 | authentication_scheme: str = Field( 105 | default="Bearer", 106 | title="Authentication Scheme", 107 | description="The authentication scheme to use.", 108 | ) 109 | authorization_method: AuthorizationMethod = Field( 110 | default=AuthorizationMethod.NONE, 111 | title="Authorization Method", 112 | description="The authorization method to use.", 113 | ) 114 | authorization_claim: str = Field( 115 | default="roles", 116 | title="Authorization Claim", 117 | description="The claim to use for authorization.", 118 | ) 119 | use_introspection_endpoint: bool = Field( 120 | default=False, 121 | title="Use Introspection Endpoint", 122 | description="Whether to use the introspection endpoint.", 123 | ) 124 | enable_device_authentication: bool = Field( 125 | default=False, 126 | title="Enable Device Authentication", 127 | description="Whether to enable device authentication. If device authentication" 128 | " is enabled, the middleware will ignore required user claims and not attempt" 129 | " to map the user. The token will be validated and then the request" 130 | " forwarded unmodified.", 131 | ) 132 | device_authentication_claim: str = Field( 133 | default="is_device", 134 | title="Device Authentication Claim", 135 | description="The claim that will be checked. If present and if it evaluates to" 136 | " true, the device authentication will be applied for the request.", 137 | ) 138 | verify: Union[bool, str] = Field( 139 | default=True, 140 | title="Verify", 141 | description="Whether to verify the SSL connection", 142 | ) 143 | validate_token: bool = Field( 144 | default=True, 145 | title="Validate Token", 146 | description="Whether to validate the token.", 147 | ) 148 | validation_options: dict[ 149 | str, Union[str, dict[str, Union[None, str]], list[str], jwk.JWK, jwk.JWKSet] 150 | ] = Field( 151 | default={}, 152 | title="JWCrypto JWT Options", 153 | description="Decode options that are passed to jwcrypto's JWT constructor.", 154 | ) 155 | enable_websocket_support: bool = Field( 156 | default=True, 157 | title="Enable WebSocket Support", 158 | description="if enabled, websocket connections are also checked for valid tokens.", 159 | ) 160 | websocket_cookie_name: str = Field( 161 | default="access_token", 162 | title="WebSocket Cookie Name", 163 | description="The name of the cookie that contains the access token.", 164 | ) 165 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/schemas/match_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains an Enum specifying the match strategy used 3 | by the require_permission decorator 4 | """ 5 | 6 | from enum import Enum 7 | 8 | 9 | class MatchStrategy(Enum): 10 | """ 11 | This Enum can be used to set the authorization match strategy 12 | if multiple scopes are bassed to the ``require_permission`` decorator. 13 | Please use the Enum values instead of the values behind the Enums. 14 | 15 | Supported options are: 16 | 17 | - ``MatchStrategy.OR``: One of the provided scopes must match 18 | - ``MatchStrategy.AND``: All of the provided scopes must match 19 | """ 20 | 21 | OR = "or" 22 | AND = "and" 23 | -------------------------------------------------------------------------------- /fastapi_keycloak_middleware/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a helper method that can be used to initialize the 3 | middleware. It can also automatically add exception responses for 401 4 | and 403 responses and modify the OpenAPI schema to correctly include 5 | OpenID Connect information. 6 | """ 7 | 8 | import logging 9 | import typing 10 | 11 | from fastapi import Depends, FastAPI 12 | from fastapi.security import OpenIdConnect 13 | 14 | from fastapi_keycloak_middleware.middleware import KeycloakMiddleware 15 | from fastapi_keycloak_middleware.schemas.exception_response import ExceptionResponse 16 | from fastapi_keycloak_middleware.schemas.keycloak_configuration import ( 17 | KeycloakConfiguration, 18 | ) 19 | 20 | log = logging.getLogger(__name__) 21 | 22 | 23 | def setup_keycloak_middleware( # pylint: disable=too-many-arguments 24 | app: FastAPI, 25 | keycloak_configuration: KeycloakConfiguration, 26 | exclude_patterns: typing.List[str] | None = None, 27 | user_mapper: typing.Callable[[typing.Dict[str, typing.Any]], typing.Awaitable[typing.Any]] 28 | | None = None, 29 | scope_mapper: typing.Callable[[typing.List[str]], typing.Awaitable[typing.List[str]]] 30 | | None = None, 31 | add_exception_response: bool = True, 32 | add_swagger_auth: bool = False, 33 | swagger_openId_base_url: str | None = None, 34 | swagger_auth_scopes: typing.List[str] | None = None, 35 | swagger_auth_pkce: bool = True, 36 | swagger_scheme_name: str = "keycloak-openid", 37 | ): 38 | """ 39 | This function can be used to initialize the middleware on an existing 40 | FastAPI application. Note that the middleware can also be added directly. 41 | 42 | This function adds the benefit of automatically adding correct response 43 | types as well as the OpenAPI configuration. 44 | 45 | :param app: The FastAPI app instance, required 46 | :param keycloak_configuration: KeyCloak configuration object. For potential 47 | options, see the KeycloakConfiguration schema. 48 | :type keycloak_configuration: KeycloakConfiguration 49 | :param exclude_patterns: List of paths that should be excluded from authentication. 50 | Defaults to an empty list. The strings will be compiled to regular expressions 51 | and used to match the path. If the path matches, the middleware 52 | will skip authentication. 53 | :type exclude_patterns: typing.List[str], optional 54 | :param user_mapper: Custom async function that gets the userinfo extracted from AT 55 | and should return a representation of the user that is meaningful to you, 56 | the user of this library, defaults to None 57 | :type user_mapper: 58 | typing.Callable[ [typing.Dict[str, typing.Any]], typing.Awaitable[typing.Any] ] 59 | optional 60 | :param scope_mapper: Custom async function that transforms the claim values 61 | extracted from the token to permissions meaningful for your application, 62 | defaults to None 63 | :type scope_mapper: typing.Callable[[typing.List[str]], typing.List[str]], optional 64 | :param add_exception_response: Whether to add exception responses for 401 and 403. 65 | Defaults to True. 66 | :type add_exception_response: bool, optional 67 | :param add_swagger_auth: Whether to add OpenID Connect authentication to the OpenAPI 68 | schema. Defaults to False. 69 | :type add_swagger_auth: bool, optional 70 | :param swagger_openId_base_url: Base URL for the OpenID Connect configuration that will be used 71 | by the Swagger UI. This parameter allows you to specify a different base URL than 72 | the one in keycloak_configuration.url. This is particularly useful in Docker 73 | container scenarios where the internal and external URLs differ. 74 | 75 | For example, inside a Docker container network, Keycloak's OpenID configuration 76 | endpoint might be available at: 77 | http://host.docker.internal:8080/auth/realms/master/.well-known/openid-configuration 78 | 79 | However, external clients like Swagger UI cannot resolve host.docker.internal. 80 | In this case, you would set: 81 | - keycloak_configuration.url: 82 | -- "http://host.docker.internal:8080" (for internal communication) 83 | - swagger_openId_base_url: 84 | -- "http://localhost:8080" (for Swagger UI access) 85 | 86 | If not specified, defaults to using keycloak_configuration.url. 87 | :type swagger_openId_base_url: str, optional 88 | :param swagger_auth_scopes: Scopes to use for the Swagger UI authentication. 89 | Defaults to ['openid', 'profile']. 90 | :type swagger_auth_scopes: typing.List[str], optional 91 | :param swagger_auth_pkce: Whether to use PKCE with the Swagger UI authentication. 92 | Defaults to True. 93 | :type swagger_auth_pkce: bool, optional 94 | :param swagger_scheme_name: Name of the OpenAPI security scheme. Defaults to 95 | 'keycloak-openid'. 96 | :type swagger_scheme_name: str, optional 97 | """ 98 | 99 | # Add middleware 100 | app.add_middleware( 101 | KeycloakMiddleware, 102 | keycloak_configuration=keycloak_configuration, 103 | user_mapper=user_mapper, 104 | scope_mapper=scope_mapper, 105 | exclude_patterns=exclude_patterns, 106 | ) 107 | 108 | # Add exception responses if requested 109 | if add_exception_response: 110 | router = app.router if isinstance(app, FastAPI) else app 111 | if 401 not in router.responses: 112 | log.debug("Adding 401 exception response") 113 | router.responses[401] = { 114 | "description": "Unauthorized", 115 | "model": ExceptionResponse, 116 | } 117 | else: 118 | log.warning( 119 | "Middleware is configured to add 401 exception response but it already exists" 120 | ) 121 | 122 | if 403 not in router.responses: 123 | log.debug("Adding 403 exception response") 124 | router.responses[403] = { 125 | "description": "Forbidden", 126 | "model": ExceptionResponse, 127 | } 128 | else: 129 | log.warning( 130 | "Middleware is configured to add 403 exception response but it already exists" 131 | ) 132 | else: 133 | log.debug("Skipping adding exception responses") 134 | 135 | # Add OpenAPI schema 136 | if add_swagger_auth: 137 | suffix = ".well-known/openid-configuration" 138 | openId_base_url = swagger_openId_base_url or keycloak_configuration.url 139 | security_scheme = OpenIdConnect( 140 | openIdConnectUrl=f"{openId_base_url}/realms/{keycloak_configuration.realm}/{suffix}", 141 | scheme_name=swagger_scheme_name, 142 | auto_error=False, 143 | ) 144 | client_id = ( 145 | keycloak_configuration.swagger_client_id 146 | if keycloak_configuration.swagger_client_id 147 | else keycloak_configuration.client_id 148 | ) 149 | scopes = swagger_auth_scopes if swagger_auth_scopes else ["openid", "profile"] 150 | swagger_ui_init_oauth = { 151 | "clientId": client_id, 152 | "scopes": scopes, 153 | "appName": app.title, 154 | "usePkceWithAuthorizationCodeGrant": swagger_auth_pkce, 155 | } 156 | app.swagger_ui_init_oauth = swagger_ui_init_oauth 157 | app.router.dependencies.append(Depends(security_scheme)) 158 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiofiles" 5 | version = "24.1.0" 6 | description = "File support for asyncio." 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["main"] 10 | files = [ 11 | {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, 12 | {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, 13 | ] 14 | 15 | [[package]] 16 | name = "alabaster" 17 | version = "1.0.0" 18 | description = "A light, configurable Sphinx theme" 19 | optional = false 20 | python-versions = ">=3.10" 21 | groups = ["dev"] 22 | files = [ 23 | {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, 24 | {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, 25 | ] 26 | 27 | [[package]] 28 | name = "annotated-types" 29 | version = "0.7.0" 30 | description = "Reusable constraint types to use with typing.Annotated" 31 | optional = false 32 | python-versions = ">=3.8" 33 | groups = ["main"] 34 | files = [ 35 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 36 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 37 | ] 38 | 39 | [[package]] 40 | name = "anyio" 41 | version = "4.9.0" 42 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 43 | optional = false 44 | python-versions = ">=3.9" 45 | groups = ["main"] 46 | files = [ 47 | {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, 48 | {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, 49 | ] 50 | 51 | [package.dependencies] 52 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 53 | idna = ">=2.8" 54 | sniffio = ">=1.1" 55 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 56 | 57 | [package.extras] 58 | doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 59 | test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] 60 | trio = ["trio (>=0.26.1)"] 61 | 62 | [[package]] 63 | name = "async-property" 64 | version = "0.2.2" 65 | description = "Python decorator for async properties." 66 | optional = false 67 | python-versions = "*" 68 | groups = ["main"] 69 | files = [ 70 | {file = "async_property-0.2.2-py2.py3-none-any.whl", hash = "sha256:8924d792b5843994537f8ed411165700b27b2bd966cefc4daeefc1253442a9d7"}, 71 | {file = "async_property-0.2.2.tar.gz", hash = "sha256:17d9bd6ca67e27915a75d92549df64b5c7174e9dc806b30a3934dc4ff0506380"}, 72 | ] 73 | 74 | [[package]] 75 | name = "attrs" 76 | version = "25.3.0" 77 | description = "Classes Without Boilerplate" 78 | optional = false 79 | python-versions = ">=3.8" 80 | groups = ["dev"] 81 | files = [ 82 | {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, 83 | {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, 84 | ] 85 | 86 | [package.extras] 87 | benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 88 | cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 89 | dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 90 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 91 | tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 92 | tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] 93 | 94 | [[package]] 95 | name = "babel" 96 | version = "2.17.0" 97 | description = "Internationalization utilities" 98 | optional = false 99 | python-versions = ">=3.8" 100 | groups = ["dev"] 101 | files = [ 102 | {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, 103 | {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, 104 | ] 105 | 106 | [package.extras] 107 | dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] 108 | 109 | [[package]] 110 | name = "cattrs" 111 | version = "25.1.0" 112 | description = "Composable complex class support for attrs and dataclasses." 113 | optional = false 114 | python-versions = ">=3.9" 115 | groups = ["dev"] 116 | files = [ 117 | {file = "cattrs-25.1.0-py3-none-any.whl", hash = "sha256:b07bd2082298f8915d53ed7254c4c34d90995d4a79467b7df7bbd544eef532f1"}, 118 | {file = "cattrs-25.1.0.tar.gz", hash = "sha256:3bf01e9592b38a49bdae47a26385583f000c98862d0efcda2c03a508b02b95b8"}, 119 | ] 120 | 121 | [package.dependencies] 122 | attrs = ">=24.3.0" 123 | exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} 124 | typing-extensions = ">=4.12.2" 125 | 126 | [package.extras] 127 | bson = ["pymongo (>=4.4.0)"] 128 | cbor2 = ["cbor2 (>=5.4.6)"] 129 | msgpack = ["msgpack (>=1.0.5)"] 130 | msgspec = ["msgspec (>=0.19.0) ; implementation_name == \"cpython\""] 131 | orjson = ["orjson (>=3.10.7) ; implementation_name == \"cpython\""] 132 | pyyaml = ["pyyaml (>=6.0)"] 133 | tomlkit = ["tomlkit (>=0.11.8)"] 134 | ujson = ["ujson (>=5.10.0)"] 135 | 136 | [[package]] 137 | name = "certifi" 138 | version = "2025.4.26" 139 | description = "Python package for providing Mozilla's CA Bundle." 140 | optional = false 141 | python-versions = ">=3.6" 142 | groups = ["main", "dev"] 143 | files = [ 144 | {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, 145 | {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, 146 | ] 147 | 148 | [[package]] 149 | name = "cffi" 150 | version = "1.17.1" 151 | description = "Foreign Function Interface for Python calling C code." 152 | optional = false 153 | python-versions = ">=3.8" 154 | groups = ["main"] 155 | markers = "platform_python_implementation != \"PyPy\"" 156 | files = [ 157 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 158 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 159 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 160 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 161 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 162 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 163 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 164 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 165 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 166 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 167 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 168 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 169 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 170 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 171 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 172 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 173 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 174 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 175 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 176 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 177 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 178 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 179 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 180 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 181 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 182 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 183 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 184 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 185 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 186 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 187 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 188 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 189 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 190 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 191 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 192 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 193 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 194 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 195 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 196 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 197 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 198 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 199 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 200 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 201 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 202 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 203 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 204 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 205 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 206 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 207 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 208 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 209 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 210 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 211 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 212 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 213 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 214 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 215 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 216 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 217 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 218 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 219 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 220 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 221 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 222 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 223 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 224 | ] 225 | 226 | [package.dependencies] 227 | pycparser = "*" 228 | 229 | [[package]] 230 | name = "charset-normalizer" 231 | version = "3.4.2" 232 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 233 | optional = false 234 | python-versions = ">=3.7" 235 | groups = ["main", "dev"] 236 | files = [ 237 | {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, 238 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, 239 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, 240 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, 241 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, 242 | {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, 243 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, 244 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, 245 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, 246 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, 247 | {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, 248 | {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, 249 | {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, 250 | {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, 251 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, 252 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, 253 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, 254 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, 255 | {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, 256 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, 257 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, 258 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, 259 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, 260 | {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, 261 | {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, 262 | {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, 263 | {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, 264 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, 265 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, 266 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, 267 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, 268 | {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, 269 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, 270 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, 271 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, 272 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, 273 | {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, 274 | {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, 275 | {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, 276 | {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, 277 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, 278 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, 279 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, 280 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, 281 | {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, 282 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, 283 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, 284 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, 285 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, 286 | {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, 287 | {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, 288 | {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, 289 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, 290 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, 291 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, 292 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, 293 | {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, 294 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, 295 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, 296 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, 297 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, 298 | {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, 299 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, 300 | {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, 301 | {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, 302 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, 303 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, 304 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, 305 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, 306 | {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, 307 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, 308 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, 309 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, 310 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, 311 | {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, 312 | {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, 313 | {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, 314 | {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, 315 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, 316 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, 317 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, 318 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, 319 | {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, 320 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, 321 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, 322 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, 323 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, 324 | {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, 325 | {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, 326 | {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, 327 | {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, 328 | {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, 329 | ] 330 | 331 | [[package]] 332 | name = "colorama" 333 | version = "0.4.6" 334 | description = "Cross-platform colored terminal text." 335 | optional = false 336 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 337 | groups = ["dev"] 338 | markers = "sys_platform == \"win32\"" 339 | files = [ 340 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 341 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 342 | ] 343 | 344 | [[package]] 345 | name = "cryptography" 346 | version = "45.0.3" 347 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 348 | optional = false 349 | python-versions = "!=3.9.0,!=3.9.1,>=3.7" 350 | groups = ["main"] 351 | files = [ 352 | {file = "cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71"}, 353 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b"}, 354 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f"}, 355 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942"}, 356 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9"}, 357 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56"}, 358 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca"}, 359 | {file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1"}, 360 | {file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578"}, 361 | {file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497"}, 362 | {file = "cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710"}, 363 | {file = "cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490"}, 364 | {file = "cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06"}, 365 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57"}, 366 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716"}, 367 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8"}, 368 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc"}, 369 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342"}, 370 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b"}, 371 | {file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782"}, 372 | {file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65"}, 373 | {file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b"}, 374 | {file = "cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab"}, 375 | {file = "cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2"}, 376 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49"}, 377 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9"}, 378 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc"}, 379 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1"}, 380 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e"}, 381 | {file = "cryptography-45.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0"}, 382 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7"}, 383 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8"}, 384 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4"}, 385 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972"}, 386 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c"}, 387 | {file = "cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19"}, 388 | {file = "cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899"}, 389 | ] 390 | 391 | [package.dependencies] 392 | cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} 393 | 394 | [package.extras] 395 | docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] 396 | docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] 397 | nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] 398 | pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] 399 | sdist = ["build (>=1.0.0)"] 400 | ssh = ["bcrypt (>=3.1.5)"] 401 | test = ["certifi (>=2024)", "cryptography-vectors (==45.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] 402 | test-randomorder = ["pytest-randomly"] 403 | 404 | [[package]] 405 | name = "deprecation" 406 | version = "2.1.0" 407 | description = "A library to handle automated deprecations" 408 | optional = false 409 | python-versions = "*" 410 | groups = ["main"] 411 | files = [ 412 | {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, 413 | {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, 414 | ] 415 | 416 | [package.dependencies] 417 | packaging = "*" 418 | 419 | [[package]] 420 | name = "docutils" 421 | version = "0.21.2" 422 | description = "Docutils -- Python Documentation Utilities" 423 | optional = false 424 | python-versions = ">=3.9" 425 | groups = ["dev"] 426 | files = [ 427 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 428 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 429 | ] 430 | 431 | [[package]] 432 | name = "exceptiongroup" 433 | version = "1.3.0" 434 | description = "Backport of PEP 654 (exception groups)" 435 | optional = false 436 | python-versions = ">=3.7" 437 | groups = ["main", "dev"] 438 | markers = "python_version == \"3.10\"" 439 | files = [ 440 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 441 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 442 | ] 443 | 444 | [package.dependencies] 445 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 446 | 447 | [package.extras] 448 | test = ["pytest (>=6)"] 449 | 450 | [[package]] 451 | name = "fastapi" 452 | version = "0.115.12" 453 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 454 | optional = false 455 | python-versions = ">=3.8" 456 | groups = ["main"] 457 | files = [ 458 | {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, 459 | {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, 460 | ] 461 | 462 | [package.dependencies] 463 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 464 | starlette = ">=0.40.0,<0.47.0" 465 | typing-extensions = ">=4.8.0" 466 | 467 | [package.extras] 468 | all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 469 | standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] 470 | 471 | [[package]] 472 | name = "h11" 473 | version = "0.16.0" 474 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 475 | optional = false 476 | python-versions = ">=3.8" 477 | groups = ["main"] 478 | files = [ 479 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 480 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 481 | ] 482 | 483 | [[package]] 484 | name = "httpcore" 485 | version = "1.0.9" 486 | description = "A minimal low-level HTTP client." 487 | optional = false 488 | python-versions = ">=3.8" 489 | groups = ["main"] 490 | files = [ 491 | {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, 492 | {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, 493 | ] 494 | 495 | [package.dependencies] 496 | certifi = "*" 497 | h11 = ">=0.16" 498 | 499 | [package.extras] 500 | asyncio = ["anyio (>=4.0,<5.0)"] 501 | http2 = ["h2 (>=3,<5)"] 502 | socks = ["socksio (==1.*)"] 503 | trio = ["trio (>=0.22.0,<1.0)"] 504 | 505 | [[package]] 506 | name = "httpx" 507 | version = "0.28.1" 508 | description = "The next generation HTTP client." 509 | optional = false 510 | python-versions = ">=3.8" 511 | groups = ["main"] 512 | files = [ 513 | {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, 514 | {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, 515 | ] 516 | 517 | [package.dependencies] 518 | anyio = "*" 519 | certifi = "*" 520 | httpcore = "==1.*" 521 | idna = "*" 522 | 523 | [package.extras] 524 | brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] 525 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 526 | http2 = ["h2 (>=3,<5)"] 527 | socks = ["socksio (==1.*)"] 528 | zstd = ["zstandard (>=0.18.0)"] 529 | 530 | [[package]] 531 | name = "idna" 532 | version = "3.10" 533 | description = "Internationalized Domain Names in Applications (IDNA)" 534 | optional = false 535 | python-versions = ">=3.6" 536 | groups = ["main", "dev"] 537 | files = [ 538 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 539 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 540 | ] 541 | 542 | [package.extras] 543 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 544 | 545 | [[package]] 546 | name = "imagesize" 547 | version = "1.4.1" 548 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 549 | optional = false 550 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 551 | groups = ["dev"] 552 | files = [ 553 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 554 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 555 | ] 556 | 557 | [[package]] 558 | name = "jinja2" 559 | version = "3.1.6" 560 | description = "A very fast and expressive template engine." 561 | optional = false 562 | python-versions = ">=3.7" 563 | groups = ["dev"] 564 | files = [ 565 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 566 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 567 | ] 568 | 569 | [package.dependencies] 570 | MarkupSafe = ">=2.0" 571 | 572 | [package.extras] 573 | i18n = ["Babel (>=2.7)"] 574 | 575 | [[package]] 576 | name = "jwcrypto" 577 | version = "1.5.6" 578 | description = "Implementation of JOSE Web standards" 579 | optional = false 580 | python-versions = ">= 3.8" 581 | groups = ["main"] 582 | files = [ 583 | {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, 584 | {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, 585 | ] 586 | 587 | [package.dependencies] 588 | cryptography = ">=3.4" 589 | typing-extensions = ">=4.5.0" 590 | 591 | [[package]] 592 | name = "lsprotocol" 593 | version = "2023.0.1" 594 | description = "Python implementation of the Language Server Protocol." 595 | optional = false 596 | python-versions = ">=3.7" 597 | groups = ["dev"] 598 | files = [ 599 | {file = "lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2"}, 600 | {file = "lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d"}, 601 | ] 602 | 603 | [package.dependencies] 604 | attrs = ">=21.3.0" 605 | cattrs = "!=23.2.1" 606 | 607 | [[package]] 608 | name = "markupsafe" 609 | version = "3.0.2" 610 | description = "Safely add untrusted strings to HTML/XML markup." 611 | optional = false 612 | python-versions = ">=3.9" 613 | groups = ["dev"] 614 | files = [ 615 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 616 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 617 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 618 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 619 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 620 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 621 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 622 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 623 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 624 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 625 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 626 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 627 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 628 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 629 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 630 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 631 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 632 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 633 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 634 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 635 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 636 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 637 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 638 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 639 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 640 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 641 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 642 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 643 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 644 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 645 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 646 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 647 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 648 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 649 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 650 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 651 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 652 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 653 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 654 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 655 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 656 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 657 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 658 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 659 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 660 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 661 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 662 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 663 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 664 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 665 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 666 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 667 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 668 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 669 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 670 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 671 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 672 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 673 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 674 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 675 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 676 | ] 677 | 678 | [[package]] 679 | name = "packaging" 680 | version = "25.0" 681 | description = "Core utilities for Python packages" 682 | optional = false 683 | python-versions = ">=3.8" 684 | groups = ["main", "dev"] 685 | files = [ 686 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 687 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 688 | ] 689 | 690 | [[package]] 691 | name = "pycparser" 692 | version = "2.22" 693 | description = "C parser in Python" 694 | optional = false 695 | python-versions = ">=3.8" 696 | groups = ["main"] 697 | markers = "platform_python_implementation != \"PyPy\"" 698 | files = [ 699 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 700 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 701 | ] 702 | 703 | [[package]] 704 | name = "pydantic" 705 | version = "2.11.5" 706 | description = "Data validation using Python type hints" 707 | optional = false 708 | python-versions = ">=3.9" 709 | groups = ["main"] 710 | files = [ 711 | {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, 712 | {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, 713 | ] 714 | 715 | [package.dependencies] 716 | annotated-types = ">=0.6.0" 717 | pydantic-core = "2.33.2" 718 | typing-extensions = ">=4.12.2" 719 | typing-inspection = ">=0.4.0" 720 | 721 | [package.extras] 722 | email = ["email-validator (>=2.0.0)"] 723 | timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] 724 | 725 | [[package]] 726 | name = "pydantic-core" 727 | version = "2.33.2" 728 | description = "Core functionality for Pydantic validation and serialization" 729 | optional = false 730 | python-versions = ">=3.9" 731 | groups = ["main"] 732 | files = [ 733 | {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, 734 | {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, 735 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, 736 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, 737 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, 738 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, 739 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, 740 | {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, 741 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, 742 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, 743 | {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, 744 | {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, 745 | {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, 746 | {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, 747 | {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, 748 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, 749 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, 750 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, 751 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, 752 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, 753 | {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, 754 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, 755 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, 756 | {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, 757 | {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, 758 | {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, 759 | {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, 760 | {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, 761 | {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, 762 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, 763 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, 764 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, 765 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, 766 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, 767 | {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, 768 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, 769 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, 770 | {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, 771 | {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, 772 | {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, 773 | {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, 774 | {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, 775 | {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, 776 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, 777 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, 778 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, 779 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, 780 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, 781 | {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, 782 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, 783 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, 784 | {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, 785 | {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, 786 | {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, 787 | {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, 788 | {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, 789 | {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, 790 | {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, 791 | {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, 792 | {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, 793 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, 794 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, 795 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, 796 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, 797 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, 798 | {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, 799 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, 800 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, 801 | {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, 802 | {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, 803 | {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, 804 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, 805 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, 806 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, 807 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, 808 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, 809 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, 810 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, 811 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, 812 | {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, 813 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, 814 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, 815 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, 816 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, 817 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, 818 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, 819 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, 820 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, 821 | {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, 822 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, 823 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, 824 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, 825 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, 826 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, 827 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, 828 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, 829 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, 830 | {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, 831 | {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, 832 | ] 833 | 834 | [package.dependencies] 835 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 836 | 837 | [[package]] 838 | name = "pygls" 839 | version = "1.3.1" 840 | description = "A pythonic generic language server (pronounced like 'pie glass')" 841 | optional = false 842 | python-versions = ">=3.8" 843 | groups = ["dev"] 844 | files = [ 845 | {file = "pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e"}, 846 | {file = "pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018"}, 847 | ] 848 | 849 | [package.dependencies] 850 | cattrs = ">=23.1.2" 851 | lsprotocol = "2023.0.1" 852 | 853 | [package.extras] 854 | ws = ["websockets (>=11.0.3)"] 855 | 856 | [[package]] 857 | name = "pygments" 858 | version = "2.19.1" 859 | description = "Pygments is a syntax highlighting package written in Python." 860 | optional = false 861 | python-versions = ">=3.8" 862 | groups = ["dev"] 863 | files = [ 864 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, 865 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, 866 | ] 867 | 868 | [package.extras] 869 | windows-terminal = ["colorama (>=0.4.6)"] 870 | 871 | [[package]] 872 | name = "python-keycloak" 873 | version = "5.5.1" 874 | description = "python-keycloak is a Python package providing access to the Keycloak API." 875 | optional = false 876 | python-versions = "<4.0,>=3.9" 877 | groups = ["main"] 878 | files = [ 879 | {file = "python_keycloak-5.5.1-py3-none-any.whl", hash = "sha256:fb2041ff36121e409a08b5bf150bb43b611302a18e0f0519f5858aac380a1416"}, 880 | {file = "python_keycloak-5.5.1.tar.gz", hash = "sha256:c2196fea266a948c39ef72e80aa7c474b919988a8ca9fe4201c20be4744400eb"}, 881 | ] 882 | 883 | [package.dependencies] 884 | aiofiles = ">=24.1.0" 885 | async-property = ">=0.2.2" 886 | deprecation = ">=2.1.0" 887 | httpx = ">=0.23.2" 888 | jwcrypto = ">=1.5.4" 889 | requests = ">=2.20.0" 890 | requests-toolbelt = ">=0.6.0" 891 | 892 | [[package]] 893 | name = "requests" 894 | version = "2.32.3" 895 | description = "Python HTTP for Humans." 896 | optional = false 897 | python-versions = ">=3.8" 898 | groups = ["main", "dev"] 899 | files = [ 900 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 901 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 902 | ] 903 | 904 | [package.dependencies] 905 | certifi = ">=2017.4.17" 906 | charset-normalizer = ">=2,<4" 907 | idna = ">=2.5,<4" 908 | urllib3 = ">=1.21.1,<3" 909 | 910 | [package.extras] 911 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 912 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 913 | 914 | [[package]] 915 | name = "requests-toolbelt" 916 | version = "1.0.0" 917 | description = "A utility belt for advanced users of python-requests" 918 | optional = false 919 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 920 | groups = ["main"] 921 | files = [ 922 | {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, 923 | {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, 924 | ] 925 | 926 | [package.dependencies] 927 | requests = ">=2.0.1,<3.0.0" 928 | 929 | [[package]] 930 | name = "ruff" 931 | version = "0.11.12" 932 | description = "An extremely fast Python linter and code formatter, written in Rust." 933 | optional = false 934 | python-versions = ">=3.7" 935 | groups = ["dev"] 936 | files = [ 937 | {file = "ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc"}, 938 | {file = "ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3"}, 939 | {file = "ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa"}, 940 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012"}, 941 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a"}, 942 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7"}, 943 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a"}, 944 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13"}, 945 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be"}, 946 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd"}, 947 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef"}, 948 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5"}, 949 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02"}, 950 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c"}, 951 | {file = "ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6"}, 952 | {file = "ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832"}, 953 | {file = "ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5"}, 954 | {file = "ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603"}, 955 | ] 956 | 957 | [[package]] 958 | name = "ruff-lsp" 959 | version = "0.0.62" 960 | description = "A Language Server Protocol implementation for Ruff." 961 | optional = false 962 | python-versions = ">=3.7" 963 | groups = ["dev"] 964 | files = [ 965 | {file = "ruff_lsp-0.0.62-py3-none-any.whl", hash = "sha256:fb6c04a0cb09bb3ae316121b084ff09497edd01df58b36fa431f14515c63029e"}, 966 | {file = "ruff_lsp-0.0.62.tar.gz", hash = "sha256:6db2a39375973ecb16c64d3c8dc37e23e1e191dcb7aebcf525b1f85ebd338c0d"}, 967 | ] 968 | 969 | [package.dependencies] 970 | lsprotocol = ">=2023.0.0" 971 | packaging = ">=23.1" 972 | pygls = ">=1.1.0" 973 | ruff = ">=0.0.274" 974 | typing-extensions = "*" 975 | 976 | [package.extras] 977 | dev = ["mypy (==1.4.1)", "pip-tools (>=6.13.0,<7.0.0)", "pytest (>=7.3.1,<8.0.0)", "pytest-asyncio (==0.21.2)", "python-lsp-jsonrpc (==1.0.0)"] 978 | 979 | [[package]] 980 | name = "sniffio" 981 | version = "1.3.1" 982 | description = "Sniff out which async library your code is running under" 983 | optional = false 984 | python-versions = ">=3.7" 985 | groups = ["main"] 986 | files = [ 987 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 988 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 989 | ] 990 | 991 | [[package]] 992 | name = "snowballstemmer" 993 | version = "3.0.1" 994 | description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." 995 | optional = false 996 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" 997 | groups = ["dev"] 998 | files = [ 999 | {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, 1000 | {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "sphinx" 1005 | version = "8.1.3" 1006 | description = "Python documentation generator" 1007 | optional = false 1008 | python-versions = ">=3.10" 1009 | groups = ["dev"] 1010 | files = [ 1011 | {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, 1012 | {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, 1013 | ] 1014 | 1015 | [package.dependencies] 1016 | alabaster = ">=0.7.14" 1017 | babel = ">=2.13" 1018 | colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} 1019 | docutils = ">=0.20,<0.22" 1020 | imagesize = ">=1.3" 1021 | Jinja2 = ">=3.1" 1022 | packaging = ">=23.0" 1023 | Pygments = ">=2.17" 1024 | requests = ">=2.30.0" 1025 | snowballstemmer = ">=2.2" 1026 | sphinxcontrib-applehelp = ">=1.0.7" 1027 | sphinxcontrib-devhelp = ">=1.0.6" 1028 | sphinxcontrib-htmlhelp = ">=2.0.6" 1029 | sphinxcontrib-jsmath = ">=1.0.1" 1030 | sphinxcontrib-qthelp = ">=1.0.6" 1031 | sphinxcontrib-serializinghtml = ">=1.1.9" 1032 | tomli = {version = ">=2", markers = "python_version < \"3.11\""} 1033 | 1034 | [package.extras] 1035 | docs = ["sphinxcontrib-websupport"] 1036 | lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] 1037 | test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] 1038 | 1039 | [[package]] 1040 | name = "sphinx-rtd-theme" 1041 | version = "3.0.2" 1042 | description = "Read the Docs theme for Sphinx" 1043 | optional = false 1044 | python-versions = ">=3.8" 1045 | groups = ["dev"] 1046 | files = [ 1047 | {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, 1048 | {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, 1049 | ] 1050 | 1051 | [package.dependencies] 1052 | docutils = ">0.18,<0.22" 1053 | sphinx = ">=6,<9" 1054 | sphinxcontrib-jquery = ">=4,<5" 1055 | 1056 | [package.extras] 1057 | dev = ["bump2version", "transifex-client", "twine", "wheel"] 1058 | 1059 | [[package]] 1060 | name = "sphinxcontrib-applehelp" 1061 | version = "2.0.0" 1062 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 1063 | optional = false 1064 | python-versions = ">=3.9" 1065 | groups = ["dev"] 1066 | files = [ 1067 | {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, 1068 | {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, 1069 | ] 1070 | 1071 | [package.extras] 1072 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 1073 | standalone = ["Sphinx (>=5)"] 1074 | test = ["pytest"] 1075 | 1076 | [[package]] 1077 | name = "sphinxcontrib-devhelp" 1078 | version = "2.0.0" 1079 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 1080 | optional = false 1081 | python-versions = ">=3.9" 1082 | groups = ["dev"] 1083 | files = [ 1084 | {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, 1085 | {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, 1086 | ] 1087 | 1088 | [package.extras] 1089 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 1090 | standalone = ["Sphinx (>=5)"] 1091 | test = ["pytest"] 1092 | 1093 | [[package]] 1094 | name = "sphinxcontrib-htmlhelp" 1095 | version = "2.1.0" 1096 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 1097 | optional = false 1098 | python-versions = ">=3.9" 1099 | groups = ["dev"] 1100 | files = [ 1101 | {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, 1102 | {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, 1103 | ] 1104 | 1105 | [package.extras] 1106 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 1107 | standalone = ["Sphinx (>=5)"] 1108 | test = ["html5lib", "pytest"] 1109 | 1110 | [[package]] 1111 | name = "sphinxcontrib-jquery" 1112 | version = "4.1" 1113 | description = "Extension to include jQuery on newer Sphinx releases" 1114 | optional = false 1115 | python-versions = ">=2.7" 1116 | groups = ["dev"] 1117 | files = [ 1118 | {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, 1119 | {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, 1120 | ] 1121 | 1122 | [package.dependencies] 1123 | Sphinx = ">=1.8" 1124 | 1125 | [[package]] 1126 | name = "sphinxcontrib-jsmath" 1127 | version = "1.0.1" 1128 | description = "A sphinx extension which renders display math in HTML via JavaScript" 1129 | optional = false 1130 | python-versions = ">=3.5" 1131 | groups = ["dev"] 1132 | files = [ 1133 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 1134 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 1135 | ] 1136 | 1137 | [package.extras] 1138 | test = ["flake8", "mypy", "pytest"] 1139 | 1140 | [[package]] 1141 | name = "sphinxcontrib-qthelp" 1142 | version = "2.0.0" 1143 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 1144 | optional = false 1145 | python-versions = ">=3.9" 1146 | groups = ["dev"] 1147 | files = [ 1148 | {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, 1149 | {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, 1150 | ] 1151 | 1152 | [package.extras] 1153 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 1154 | standalone = ["Sphinx (>=5)"] 1155 | test = ["defusedxml (>=0.7.1)", "pytest"] 1156 | 1157 | [[package]] 1158 | name = "sphinxcontrib-serializinghtml" 1159 | version = "2.0.0" 1160 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 1161 | optional = false 1162 | python-versions = ">=3.9" 1163 | groups = ["dev"] 1164 | files = [ 1165 | {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, 1166 | {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, 1167 | ] 1168 | 1169 | [package.extras] 1170 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 1171 | standalone = ["Sphinx (>=5)"] 1172 | test = ["pytest"] 1173 | 1174 | [[package]] 1175 | name = "starlette" 1176 | version = "0.46.2" 1177 | description = "The little ASGI library that shines." 1178 | optional = false 1179 | python-versions = ">=3.9" 1180 | groups = ["main"] 1181 | files = [ 1182 | {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, 1183 | {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, 1184 | ] 1185 | 1186 | [package.dependencies] 1187 | anyio = ">=3.6.2,<5" 1188 | 1189 | [package.extras] 1190 | full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] 1191 | 1192 | [[package]] 1193 | name = "tomli" 1194 | version = "2.2.1" 1195 | description = "A lil' TOML parser" 1196 | optional = false 1197 | python-versions = ">=3.8" 1198 | groups = ["dev"] 1199 | markers = "python_version == \"3.10\"" 1200 | files = [ 1201 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 1202 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 1203 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 1204 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 1205 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 1206 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 1207 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 1208 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 1209 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 1210 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 1211 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 1212 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 1213 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 1214 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 1215 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 1216 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 1217 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 1218 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 1219 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 1220 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 1221 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 1222 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 1223 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 1224 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 1225 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 1226 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 1227 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 1228 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 1229 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 1230 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 1231 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 1232 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "typing-extensions" 1237 | version = "4.14.0" 1238 | description = "Backported and Experimental Type Hints for Python 3.9+" 1239 | optional = false 1240 | python-versions = ">=3.9" 1241 | groups = ["main", "dev"] 1242 | files = [ 1243 | {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, 1244 | {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "typing-inspection" 1249 | version = "0.4.1" 1250 | description = "Runtime typing introspection tools" 1251 | optional = false 1252 | python-versions = ">=3.9" 1253 | groups = ["main"] 1254 | files = [ 1255 | {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, 1256 | {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, 1257 | ] 1258 | 1259 | [package.dependencies] 1260 | typing-extensions = ">=4.12.0" 1261 | 1262 | [[package]] 1263 | name = "urllib3" 1264 | version = "2.4.0" 1265 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1266 | optional = false 1267 | python-versions = ">=3.9" 1268 | groups = ["main", "dev"] 1269 | files = [ 1270 | {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, 1271 | {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, 1272 | ] 1273 | 1274 | [package.extras] 1275 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 1276 | h2 = ["h2 (>=4,<5)"] 1277 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1278 | zstd = ["zstandard (>=0.18.0)"] 1279 | 1280 | [metadata] 1281 | lock-version = "2.1" 1282 | python-versions = "^3.10" 1283 | content-hash = "fa23d03982808cf6e76e4c9e17cfb97d4ceb5db6d330268f110fa5765b6971b0" 1284 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fastapi-keycloak-middleware" 3 | version = "1.3.0" 4 | description = "Middleware for FastAPI to authenticate a user against keycloak" 5 | authors = ["Daniel Herrmann "] 6 | readme = "README.md" 7 | packages = [{ include = "fastapi_keycloak_middleware" }] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.10" 11 | fastapi = ">=0.73.0" 12 | python-keycloak = "^4,>=4.1 || ^5" 13 | 14 | 15 | [tool.poetry.group.dev.dependencies] 16 | sphinx = "^8" 17 | sphinx-rtd-theme = "^3" 18 | ruff = "^0" 19 | ruff-lsp = "^0" 20 | 21 | [build-system] 22 | requires = ["poetry-core"] 23 | build-backend = "poetry.core.masonry.api" 24 | 25 | [tool.ruff] 26 | target-version = "py310" 27 | line-length = 100 28 | fix = true 29 | 30 | [tool.ruff.lint.isort] 31 | case-sensitive = true 32 | 33 | [tool.ruff.lint] 34 | select = [ 35 | # https://docs.astral.sh/ruff/rules/#pyflakes-f 36 | "F", # Pyflakes 37 | # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w 38 | "E", # pycodestyle 39 | "W", # Warning 40 | # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 41 | # https://docs.astral.sh/ruff/rules/#mccabe-c90 42 | "C", # Complexity (mccabe+) & comprehensions 43 | # https://docs.astral.sh/ruff/rules/#pyupgrade-up 44 | "UP", # pyupgrade 45 | # https://docs.astral.sh/ruff/rules/#isort-i 46 | "I", # isort 47 | ] 48 | ignore = [ 49 | # https://docs.astral.sh/ruff/rules/#pyupgrade-up 50 | "UP006", # use-pep585-annotation 51 | "UP007", # use-pep604-annotation 52 | "E741", # Ambiguous variable name 53 | ] 54 | 55 | [tool.ruff.lint.mccabe] 56 | max-complexity = 24 57 | 58 | [tool.ruff.lint.pydocstyle] 59 | convention = "numpy" 60 | 61 | [tool.ruff.lint.per-file-ignores] 62 | "__init__.py" = [ 63 | "F401", # unused import 64 | "F403", # star imports 65 | ] 66 | 67 | [tool.mypy] 68 | disable_error_code = "import-untyped" 69 | --------------------------------------------------------------------------------