├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── poetry.lock ├── pyproject.toml ├── renovate.json ├── sample_app ├── __init__.py ├── app.py └── keys │ ├── README.md │ ├── ec.key │ ├── ec.pub │ ├── rsa.key │ └── rsa.pub ├── starlette_authlib ├── __init__.py └── middleware.py └── tests ├── __init__.py └── test_session.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .dependabot 3 | .github 4 | .gitignore 5 | .mypy_cache 6 | .pytest_cache 7 | .python-version 8 | CHANGELOG.md 9 | Dockerfile 10 | coverage.xml 11 | dist 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: develop 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | target-branch: develop 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "14 9 * * 3" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | # on: 5 | # workflow_run: 6 | # workflows: 7 | # - Run Tests 8 | # types: 9 | # - completed 10 | on: 11 | push: 12 | tags: 13 | - '*' 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | # if: github.ref_type == 'tag' 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: release 22 | env: 23 | DOCKER_BUILDKIT: 1 24 | run: | 25 | 26 | docker build . \ 27 | --target release \ 28 | --build-arg PYPI_TOKEN=${{ secrets.PYPI_TOKEN }} \ 29 | --build-arg CODECOV_TOKEN=${{ secrets.CODECOV_TOKEN }} \ 30 | --build-arg GIT_SHA=${{ github.sha }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Tests 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | 9 | run_tests: 10 | strategy: 11 | matrix: 12 | version: 13 | - 3.8 14 | - 3.9 15 | - "3.10" 16 | - "3.11" 17 | - "3.12" 18 | - "3.13" 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: test 23 | env: 24 | DOCKER_BUILDKIT: 1 25 | run: | 26 | 27 | docker build . \ 28 | --target test \ 29 | --build-arg PYTHON_VERSION=${{ matrix.version }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,eclipse 3 | # Edit at https://www.gitignore.io/?templates=python,eclipse 4 | 5 | ### Eclipse ### 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # CDT- autotools 31 | .autotools 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Annotation Processing 55 | .apt_generated/ 56 | 57 | # Scala IDE specific (Scala & Java development for Eclipse) 58 | .cache-main 59 | .scala_dependencies 60 | .worksheet 61 | 62 | ### Eclipse Patch ### 63 | # Eclipse Core 64 | .project 65 | 66 | # JDT-specific (Eclipse Java Development Tools) 67 | .classpath 68 | 69 | # Annotation Processing 70 | .apt_generated 71 | 72 | .sts4-cache/ 73 | 74 | ### Python ### 75 | # Byte-compiled / optimized / DLL files 76 | __pycache__/ 77 | *.py[cod] 78 | *$py.class 79 | 80 | # C extensions 81 | *.so 82 | 83 | # Distribution / packaging 84 | .Python 85 | build/ 86 | develop-eggs/ 87 | dist/ 88 | downloads/ 89 | eggs/ 90 | .eggs/ 91 | lib/ 92 | lib64/ 93 | parts/ 94 | sdist/ 95 | var/ 96 | wheels/ 97 | pip-wheel-metadata/ 98 | share/python-wheels/ 99 | *.egg-info/ 100 | .installed.cfg 101 | *.egg 102 | MANIFEST 103 | 104 | # PyInstaller 105 | # Usually these files are written by a python script from a template 106 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 107 | *.manifest 108 | *.spec 109 | 110 | # Installer logs 111 | pip-log.txt 112 | pip-delete-this-directory.txt 113 | 114 | # Unit test / coverage reports 115 | htmlcov/ 116 | .tox/ 117 | .nox/ 118 | .coverage 119 | .coverage.* 120 | .cache 121 | nosetests.xml 122 | coverage.xml 123 | *.cover 124 | .hypothesis/ 125 | .pytest_cache/ 126 | 127 | # Translations 128 | *.mo 129 | *.pot 130 | 131 | # Scrapy stuff: 132 | .scrapy 133 | 134 | # Sphinx documentation 135 | docs/_build/ 136 | 137 | # PyBuilder 138 | target/ 139 | 140 | # pyenv 141 | .python-version 142 | 143 | # pipenv 144 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 145 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 146 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 147 | # install all needed dependencies. 148 | #Pipfile.lock 149 | 150 | # celery beat schedule file 151 | celerybeat-schedule 152 | 153 | # SageMath parsed files 154 | *.sage.py 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # Mr Developer 164 | .mr.developer.cfg 165 | .pydevproject 166 | 167 | # mkdocs documentation 168 | /site 169 | 170 | # mypy 171 | .mypy_cache/ 172 | .dmypy.json 173 | dmypy.json 174 | 175 | # Pyre type checker 176 | .pyre/ 177 | 178 | # End of https://www.gitignore.io/api/python,eclipse 179 | .env 180 | .mise.toml 181 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/PyCQA/isort 12 | rev: 5.11.5 13 | hooks: 14 | - id: isort 15 | args: 16 | - --multi-line=3 17 | - --trailing-comma 18 | - --force-grid-wrap=0 19 | - --combine-as 20 | - --line-width=88 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.3.0 23 | hooks: 24 | - id: mypy 25 | exclude: ^(tests|sample_app)/ 26 | args: 27 | - --ignore-missing-imports 28 | - --disallow-untyped-defs 29 | - --follow-imports 30 | - skip 31 | - repo: https://github.com/PyCQA/autoflake 32 | rev: v2.1.1 33 | hooks: 34 | - id: autoflake 35 | args: 36 | - --in-place 37 | - --recursive 38 | - repo: https://github.com/psf/black 39 | rev: 23.3.0 40 | hooks: 41 | - id: black 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.12] - 2025-05-30 9 | 10 | ### Changed 11 | 12 | - bump starlette dependency 13 | 14 | ## [0.3.11] - 2025-05-27 15 | 16 | ### Changed 17 | 18 | - bump authlib dependency 19 | 20 | ## [0.3.10] - 2025-03-06 21 | 22 | ### Changed 23 | 24 | - bump authlib dependency 25 | 26 | ## [0.3.9] - 2025-02-22 27 | 28 | ### Changed 29 | 30 | - bump starlette dependency 31 | 32 | ## [0.3.8] - 2024-12-31 33 | 34 | ### Changed 35 | 36 | - bump starlette dependency 37 | 38 | ## [0.3.7] - 2024-12-28 39 | 40 | ### Changed 41 | 42 | - bump starlette dependency 43 | 44 | ## [0.3.6] - 2024-12-20 45 | 46 | ### Changed 47 | 48 | - bump authlib dependency 49 | 50 | ## [0.3.5] - 2024-12-15 51 | 52 | ### Changed 53 | 54 | - bump starlette dependency 55 | 56 | ## [0.3.4] - 2024-10-15 57 | 58 | ### Changed 59 | 60 | - bump starlette dependency 61 | 62 | ## [0.3.3] - 2024-10-15 63 | 64 | ### Changed 65 | 66 | - bump starlette dependency 67 | 68 | ## [0.3.2] - 2024-09-23 69 | 70 | ### Changed 71 | 72 | - bump starlette dependency 73 | 74 | ## [0.3.1] - 2024-07-20 75 | 76 | ### Changed 77 | 78 | - bump starlette dependency 79 | 80 | ## [0.3.0] - 2024-06-28 81 | 82 | ### Changed 83 | 84 | - dropped python 3.7 support 85 | 86 | ## [0.2.1] - 2024-06-23 87 | 88 | ### Fixed 89 | 90 | - missing parenthesis 91 | 92 | ## [0.2.0] - 2024-06-23 93 | 94 | ### Added 95 | 96 | - configurable session cookie path - [#239](https://github.com/aogier/starlette-authlib/issues/239) 97 | 98 | ## [0.1.40] - 2024-04-14 99 | 100 | ### Changed 101 | 102 | - bump starlette dependency 103 | 104 | ## [0.1.39] - 2024-03-25 105 | 106 | ### Security 107 | 108 | - starlette bump 109 | 110 | ## [0.1.38] - 2024-02-05 111 | 112 | ### Changed 113 | 114 | - bump starlette dependency 115 | 116 | ## [0.1.37] - 2024-01-23 117 | 118 | ### Changed 119 | 120 | - bump starlette dependency 121 | 122 | ## [0.1.36] - 2024-01-11 123 | 124 | ### Changed 125 | 126 | - bump starlette dependency 127 | 128 | ## [0.1.35] - 2023-12-22 129 | 130 | ### Added 131 | 132 | - license added 133 | 134 | ## [0.1.34] - 2023-12-18 135 | 136 | ### Changed 137 | 138 | - bump authlib dependency 139 | 140 | ## [0.1.33] - 2023-12-16 141 | 142 | ### Changed 143 | 144 | - bump starlette dependency 145 | - bumps dev deps 146 | 147 | ## [0.1.32] - 2023-12-01 148 | 149 | ### Changed 150 | 151 | - bump starlette dependency 152 | 153 | ## [0.1.31] - 2023-11-05 154 | 155 | ### Changed 156 | 157 | - bump starlette requirement 158 | 159 | ## [0.1.30] - 2023-08-26 160 | 161 | ### Fixed 162 | 163 | - #175 honor nbf claim (lannuttia) 164 | 165 | ## [0.1.29] - 2023-07-24 166 | 167 | ### Changed 168 | 169 | - bump starlette requirement 170 | 171 | ## [0.1.28] - 2023-07-13 172 | 173 | ### Changed 174 | 175 | - bump starlette requirement 176 | 177 | ## [0.1.27] - 2023-07-13 178 | 179 | ### Changed 180 | 181 | - bump starlette requirement 182 | 183 | ## [0.1.26] - 2023-07-01 184 | 185 | ### Added 186 | 187 | - sample app enhancements 188 | 189 | ### Deprecated 190 | 191 | - python 3.7 support will be removed on 2024-06-27 192 | 193 | ### Removed 194 | 195 | - py3.7: no longer run checks 196 | 197 | ## [0.1.25] - 2023-06-07 198 | 199 | ### Changed 200 | 201 | - bump starlette requirement 202 | 203 | ## [0.1.24] - 2023-05-16 204 | 205 | ### Changed 206 | 207 | - bump starlette requirement 208 | 209 | ## [0.1.23] - 2023-03-10 210 | 211 | ### Changed 212 | 213 | - bump starlette requirement 214 | 215 | ## [0.1.22] - 2023-02-15 216 | 217 | ### Security 218 | 219 | - bumped starlette dependency 220 | 221 | ## [0.1.21] - 2023-02-12 222 | 223 | ### Added 224 | 225 | - bumped starlette dependency 226 | 227 | ## [0.1.20] - 2022-12-27 228 | 229 | ### Changed 230 | 231 | - drop discontinued lgtm.com service 232 | - remove useless uvicorn dependency 233 | 234 | ## [0.1.19] - 2022-12-06 235 | 236 | ### Changed 237 | 238 | - bump authlib requirement 239 | 240 | ## [0.1.18] - 2022-12-05 241 | 242 | ### Changed 243 | 244 | - bump autoflake requirement 245 | - bump starlette requirement 246 | - introducing python 3.11 247 | 248 | ## [0.1.17] - 2022-11-20 249 | 250 | ### Changed 251 | 252 | - bump uvicorn requirement 253 | 254 | ## [0.1.16] - 2022-11-17 255 | 256 | ### Added 257 | 258 | - bump starlette requirement 259 | 260 | ## [0.1.15] - 2022-11-16 261 | 262 | ### Changed 263 | 264 | - bumped dev dependencies 265 | 266 | ### Security 267 | 268 | - bumped cryptography 269 | 270 | ## [0.1.14] - 2022-10-20 271 | 272 | ### Changed 273 | 274 | - bumped uvicorn dependency 275 | 276 | ## [0.1.13] - 2022-09-28 277 | 278 | ### Changed 279 | 280 | - starlette dependency bumped 281 | 282 | ## [0.1.12] - 2022-07-31 283 | 284 | ### Changed 285 | 286 | - dev dependencies bump 287 | - uvicorn dependency bumped to current minor 288 | 289 | ## [0.1.11] - 2022-06-13 290 | 291 | ### Changed 292 | 293 | - dev dependencies bumps 294 | 295 | ## [0.1.10] - 2022-05-28 296 | 297 | ### Changed 298 | 299 | - pytest version bump 300 | - pytest-cov 301 | - enabled codecov from the docker build 302 | - authlib dependency bump 303 | 304 | ### Removed 305 | 306 | - deprecated poetry-version 307 | 308 | ## [0.1.9] - 2022-05-14 309 | 310 | ### Added 311 | 312 | - github actions workflows 313 | 314 | ### Changed 315 | 316 | - more flexible dependencies specification for the three main libraries (authlib, uvicorn, starlette) 317 | - dev dependencies bumps 318 | - better project metadata in `pyproject.toml` 319 | - minor isort reformatting 320 | 321 | ### Removed 322 | 323 | - travis CI config 324 | 325 | ### Fixed 326 | 327 | - type annotations for newer libraries version 328 | 329 | ## [0.1.8] - 2021-05-18 330 | 331 | ### Added 332 | 333 | - python 3.9 support 334 | 335 | ### Changed 336 | 337 | - removed ipython from dev dependencies 338 | - fixed 3.6 quirks in CI 339 | - capture decode errors too, thx @rcyrus 340 | 341 | ## [0.1.7] - 2020-12-13 342 | 343 | ### Changed 344 | 345 | - switched to poetry for everything 346 | - bumped mypy for py 3.9 347 | - security fixes 348 | 349 | ## [0.1.6] - 2020-02-24 350 | 351 | ### Changed 352 | 353 | - Expose whole jwt session cookie to starlette request.session (ie. `exp` claim is now visible). 354 | - Adopted [Keep a Changelog](http://keepachangelog.com/) changelog format. 355 | 356 | ## [0.1.5] - 2020-02-21 357 | 358 | ### Changed 359 | 360 | - Phased out `__version__` in module, there is no a sane way to keep it up to 361 | - date anyway. 362 | 363 | ## [0.1.4] - 2020-02-21 364 | 365 | ### Fixed 366 | 367 | - Python 3.6 namedtuple lack `default` parameter. 368 | 369 | ## [0.1.3] - 2020-02-21 370 | 371 | ### Added 372 | 373 | - A changelog. 374 | - Added linting in CI. 375 | 376 | ### Changed 377 | 378 | - Correctly implemented ES* and RS* algorithms. 379 | 380 | [Unreleased]: https://github.com/aogier/starlette-authlib/compare/0.3.12...HEAD 381 | [0.3.12]: https://github.com/aogier/starlette-authlib/compare/0.3.11...0.3.12 382 | [0.3.11]: https://github.com/aogier/starlette-authlib/compare/0.3.10...0.3.11 383 | [0.3.10]: https://github.com/aogier/starlette-authlib/compare/0.3.9...0.3.10 384 | [0.3.9]: https://github.com/aogier/starlette-authlib/compare/0.3.8...0.3.9 385 | [0.3.8]: https://github.com/aogier/starlette-authlib/compare/0.3.7...0.3.8 386 | [0.3.7]: https://github.com/aogier/starlette-authlib/compare/0.3.6...0.3.7 387 | [0.3.6]: https://github.com/aogier/starlette-authlib/compare/0.3.5...0.3.6 388 | [0.3.5]: https://github.com/aogier/starlette-authlib/compare/0.3.4...0.3.5 389 | [0.3.4]: https://github.com/aogier/starlette-authlib/compare/0.3.3...0.3.4 390 | [0.3.3]: https://github.com/aogier/starlette-authlib/compare/0.3.2...0.3.3 391 | [0.3.2]: https://github.com/aogier/starlette-authlib/compare/0.3.1...0.3.2 392 | [0.3.1]: https://github.com/aogier/starlette-authlib/compare/0.3.0...0.3.1 393 | [0.3.0]: https://github.com/aogier/starlette-authlib/compare/0.2.1...0.3.0 394 | [0.2.1]: https://github.com/aogier/starlette-authlib/compare/0.2.0...0.2.1 395 | [0.2.0]: https://github.com/aogier/starlette-authlib/compare/0.1.40...0.2.0 396 | [0.1.40]: https://github.com/aogier/starlette-authlib/compare/0.1.39...0.1.40 397 | [0.1.39]: https://github.com/aogier/starlette-authlib/compare/0.1.38...0.1.39 398 | [0.1.38]: https://github.com/aogier/starlette-authlib/compare/0.1.37...0.1.38 399 | [0.1.37]: https://github.com/aogier/starlette-authlib/compare/0.1.36...0.1.37 400 | [0.1.36]: https://github.com/aogier/starlette-authlib/compare/0.1.35...0.1.36 401 | [0.1.35]: https://github.com/aogier/starlette-authlib/compare/0.1.34...0.1.35 402 | [0.1.34]: https://github.com/aogier/starlette-authlib/compare/0.1.33...0.1.34 403 | [0.1.33]: https://github.com/aogier/starlette-authlib/compare/0.1.32...0.1.33 404 | [0.1.32]: https://github.com/aogier/starlette-authlib/compare/0.1.31...0.1.32 405 | [0.1.31]: https://github.com/aogier/starlette-authlib/compare/0.1.30...0.1.31 406 | [0.1.30]: https://github.com/aogier/starlette-authlib/compare/0.1.29...0.1.30 407 | [0.1.29]: https://github.com/aogier/starlette-authlib/compare/0.1.28...0.1.29 408 | [0.1.28]: https://github.com/aogier/starlette-authlib/compare/0.1.27...0.1.28 409 | [0.1.27]: https://github.com/aogier/starlette-authlib/compare/0.1.26...0.1.27 410 | [0.1.26]: https://github.com/aogier/starlette-authlib/compare/0.1.25...0.1.26 411 | [0.1.25]: https://github.com/aogier/starlette-authlib/compare/0.1.24...0.1.25 412 | [0.1.24]: https://github.com/aogier/starlette-authlib/compare/0.1.23...0.1.24 413 | [0.1.23]: https://github.com/aogier/starlette-authlib/compare/0.1.22...0.1.23 414 | [0.1.22]: https://github.com/aogier/starlette-authlib/compare/0.1.21...0.1.22 415 | [0.1.21]: https://github.com/aogier/starlette-authlib/compare/0.1.20...0.1.21 416 | [0.1.20]: https://github.com/aogier/starlette-authlib/compare/0.1.19...0.1.20 417 | [0.1.19]: https://github.com/aogier/starlette-authlib/compare/0.1.18...0.1.19 418 | [0.1.18]: https://github.com/aogier/starlette-authlib/compare/0.1.17...0.1.18 419 | [0.1.17]: https://github.com/aogier/starlette-authlib/compare/0.1.16...0.1.17 420 | [0.1.16]: https://github.com/aogier/starlette-authlib/compare/0.1.15...0.1.16 421 | [0.1.15]: https://github.com/aogier/starlette-authlib/compare/0.1.14...0.1.15 422 | [0.1.14]: https://github.com/aogier/starlette-authlib/compare/0.1.13...0.1.14 423 | [0.1.13]: https://github.com/aogier/starlette-authlib/compare/0.1.12...0.1.13 424 | [0.1.12]: https://github.com/aogier/starlette-authlib/compare/0.1.11...0.1.12 425 | [0.1.11]: https://github.com/aogier/starlette-authlib/compare/0.1.10...0.1.11 426 | [0.1.10]: https://github.com/aogier/starlette-authlib/compare/0.1.9...0.1.10 427 | [0.1.9]: https://github.com/aogier/starlette-authlib/compare/0.1.8...0.1.9 428 | [0.1.8]: https://github.com/aogier/starlette-authlib/compare/0.1.7...0.1.8 429 | [0.1.7]: https://github.com/aogier/starlette-authlib/compare/0.1.6...0.1.7 430 | [0.1.6]: https://github.com/aogier/starlette-authlib/compare/0.1.5...0.1.6 431 | [0.1.5]: https://github.com/aogier/starlette-authlib/compare/0.1.4...0.1.5 432 | [0.1.4]: https://github.com/aogier/starlette-authlib/compare/0.1.3...0.1.4 433 | [0.1.3]: https://github.com/aogier/starlette-authlib/releases/tag/0.1.3 434 | 435 | [//]: # (C3-2-DKAC:GGH:Raogier/starlette-authlib:T{t}) 436 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION=3.10 2 | FROM python:${PYTHON_VERSION} as base 3 | 4 | WORKDIR /srv 5 | COPY . . 6 | 7 | # hadolint ignore=DL3042,DL3013 8 | RUN set -x \ 9 | && pip install poetry \ 10 | && poetry config virtualenvs.create false \ 11 | && poetry install 12 | 13 | FROM base as test 14 | 15 | ARG PYTHON_VERSION=3.10 16 | 17 | RUN if [ $PYTHON_VERSION != 3.7 ] \ 18 | ;then \ 19 | poetry run \ 20 | pre-commit run \ 21 | -a --show-diff-on-failure \ 22 | ;fi \ 23 | && poetry run \ 24 | pytest \ 25 | --ignore venv \ 26 | -W ignore::DeprecationWarning \ 27 | --cov-report=xml \ 28 | --cov=starlette_authlib \ 29 | --cov=tests \ 30 | --cov-fail-under=100 \ 31 | --cov-report=term-missing 32 | 33 | FROM base as release 34 | 35 | ARG PYPI_TOKEN 36 | ARG CODECOV_TOKEN 37 | ARG GIT_SHA 38 | 39 | COPY --from=test /srv/coverage.xml . 40 | 41 | RUN set -x \ 42 | && poetry publish --build \ 43 | --username __token__ \ 44 | --password $PYPI_TOKEN \ 45 | && codecov \ 46 | --token $CODECOV_TOKEN \ 47 | --commit $GIT_SHA 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2018, [Encode OSS Ltd](https://www.encode.io/). 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starlette Authlib Middleware 2 | 3 | [![codecov](https://codecov.io/gh/aogier/starlette-authlib/branch/master/graph/badge.svg)](https://codecov.io/gh/aogier/starlette-authlib) 4 | [![Package version](https://badge.fury.io/py/starlette-authlib.svg)](https://pypi.org/project/starlette-authlib) 5 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/starlette-authlib) 6 | 7 | ## Introduction 8 | 9 | A drop-in replacement for Starlette session middleware, using [authlib's jwt](https://docs.authlib.org/en/latest/jose/jwt.html). 10 | 11 | ## Rationale 12 | 13 | It is sometimes necessary to integrate a Starlette-based application into more 14 | complex scenarios where other actors need to make decisions based on session 15 | data. This middleware makes this possible by using a standard JWT token instead 16 | of the Starlette-encrypted one, thus simplifying interaction with third-party 17 | components. 18 | 19 | ## Requirements 20 | 21 | * Python 3.7+ 22 | * Starlette 0.9+ 23 | 24 | ## Installation 25 | 26 | ```console 27 | pip install starlette-authlib 28 | ``` 29 | 30 | ## Usage 31 | 32 | A complete example where we drop-in replace standard session middleware: 33 | 34 | ```python 35 | from starlette.applications import Starlette 36 | 37 | from starlette_authlib.middleware import AuthlibMiddleware as SessionMiddleware 38 | 39 | 40 | app = Starlette() 41 | 42 | app.add_middleware(SessionMiddleware, secret_key='secret') 43 | ``` 44 | 45 | Other things you can configure either via environment variables or `.env` file: 46 | 47 | * `DOMAIN` - declare cookie domain. App must be under this domain. If empty, 48 | the cookie is restricted to the subdomain of the app (this is useful when you 49 | write eg. SSO portals) 50 | * `JWT_ALG` - one of authlib JWT [supported algorithms](https://docs.authlib.org/en/latest/specs/rfc7518.html#specs-rfc7518) 51 | * `JWT_SECRET` - jwt secret. Only useful for HS* algorithms, see the 52 | `sample_app` folder for middleware usage w/ crypto keys. 53 | 54 | ## See it in action: sample application 55 | 56 | A sample application is included, and you can run it with either Starlette-based session middleware or this one, just by setting a variable: 57 | 58 | ``` 59 | # run with vanilla Starlette-based session middleware 60 | VANILLA=1 uvicorn sample_app.app:app 61 | 62 | # run with this drop-in replacement 63 | uvicorn sample_app.app:app 64 | ``` 65 | 66 | As you can notice in code [here](sample_app/app.py), the only difference is an 67 | import name, based on this `VANILLA` env var. 68 | 69 | ## Contributing 70 | 71 | This project is absolutely open to contributions so if you have a nice idea, 72 | create an issue to let the community discuss it. 73 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.5.2" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, 11 | {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, 12 | ] 13 | 14 | [package.dependencies] 15 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 16 | idna = ">=2.8" 17 | sniffio = ">=1.1" 18 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 19 | 20 | [package.extras] 21 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 22 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] 23 | trio = ["trio (>=0.26.1)"] 24 | 25 | [[package]] 26 | name = "authlib" 27 | version = "1.3.2" 28 | description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." 29 | optional = false 30 | python-versions = ">=3.8" 31 | files = [ 32 | {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"}, 33 | {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"}, 34 | ] 35 | 36 | [package.dependencies] 37 | cryptography = "*" 38 | 39 | [[package]] 40 | name = "autoflake" 41 | version = "2.2.1" 42 | description = "Removes unused imports and unused variables" 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "autoflake-2.2.1-py3-none-any.whl", hash = "sha256:265cde0a43c1f44ecfb4f30d95b0437796759d07be7706a2f70e4719234c0f79"}, 47 | {file = "autoflake-2.2.1.tar.gz", hash = "sha256:62b7b6449a692c3c9b0c916919bbc21648da7281e8506bcf8d3f8280e431ebc1"}, 48 | ] 49 | 50 | [package.dependencies] 51 | pyflakes = ">=3.0.0" 52 | tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} 53 | 54 | [[package]] 55 | name = "black" 56 | version = "24.8.0" 57 | description = "The uncompromising code formatter." 58 | optional = false 59 | python-versions = ">=3.8" 60 | files = [ 61 | {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, 62 | {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, 63 | {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, 64 | {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, 65 | {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, 66 | {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, 67 | {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, 68 | {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, 69 | {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, 70 | {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, 71 | {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, 72 | {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, 73 | {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, 74 | {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, 75 | {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, 76 | {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, 77 | {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, 78 | {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, 79 | {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, 80 | {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, 81 | {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, 82 | {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, 83 | ] 84 | 85 | [package.dependencies] 86 | click = ">=8.0.0" 87 | mypy-extensions = ">=0.4.3" 88 | packaging = ">=22.0" 89 | pathspec = ">=0.9.0" 90 | platformdirs = ">=2" 91 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 92 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 93 | 94 | [package.extras] 95 | colorama = ["colorama (>=0.4.3)"] 96 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 97 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 98 | uvloop = ["uvloop (>=0.15.2)"] 99 | 100 | [[package]] 101 | name = "certifi" 102 | version = "2024.8.30" 103 | description = "Python package for providing Mozilla's CA Bundle." 104 | optional = false 105 | python-versions = ">=3.6" 106 | files = [ 107 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 108 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 109 | ] 110 | 111 | [[package]] 112 | name = "cffi" 113 | version = "1.17.1" 114 | description = "Foreign Function Interface for Python calling C code." 115 | optional = false 116 | python-versions = ">=3.8" 117 | files = [ 118 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 119 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 120 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 121 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 122 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 123 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 124 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 125 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 126 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 127 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 128 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 129 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 130 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 131 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 132 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 133 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 134 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 135 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 136 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 137 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 138 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 139 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 140 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 141 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 142 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 143 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 144 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 145 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 146 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 147 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 148 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 149 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 150 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 151 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 152 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 153 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 154 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 155 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 156 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 157 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 158 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 159 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 160 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 161 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 162 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 163 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 164 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 165 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 166 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 167 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 168 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 169 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 170 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 171 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 172 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 173 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 174 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 175 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 176 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 177 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 178 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 179 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 180 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 181 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 182 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 183 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 184 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 185 | ] 186 | 187 | [package.dependencies] 188 | pycparser = "*" 189 | 190 | [[package]] 191 | name = "cfgv" 192 | version = "3.4.0" 193 | description = "Validate configuration and produce human readable error messages." 194 | optional = false 195 | python-versions = ">=3.8" 196 | files = [ 197 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 198 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 199 | ] 200 | 201 | [[package]] 202 | name = "charset-normalizer" 203 | version = "3.4.0" 204 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 205 | optional = false 206 | python-versions = ">=3.7.0" 207 | files = [ 208 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 209 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 210 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 211 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 212 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 213 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 214 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 215 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 216 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 217 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 218 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 219 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 220 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 221 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 222 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 223 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 224 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 225 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 226 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 227 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 228 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 229 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 230 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 231 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 232 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 233 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 234 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 235 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 236 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 237 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 238 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 239 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 240 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 241 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 242 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 243 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 244 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 245 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 246 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 247 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 248 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 249 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 250 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 251 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 252 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 253 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 254 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 255 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 256 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 257 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 258 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 259 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 260 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 261 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 262 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 263 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 264 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 265 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 266 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 267 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 268 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 269 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 270 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 271 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 272 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 273 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 274 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 275 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 276 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 277 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 278 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 279 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 280 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 281 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 282 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 283 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 284 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 285 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 286 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 287 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 288 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 289 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 290 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 291 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 292 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 293 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 294 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 295 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 296 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 297 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 298 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 299 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 300 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 301 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 302 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 303 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 304 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 305 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 306 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 307 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 308 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 309 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 310 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 311 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 312 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 313 | ] 314 | 315 | [[package]] 316 | name = "click" 317 | version = "8.1.7" 318 | description = "Composable command line interface toolkit" 319 | optional = false 320 | python-versions = ">=3.7" 321 | files = [ 322 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 323 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 324 | ] 325 | 326 | [package.dependencies] 327 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 328 | 329 | [[package]] 330 | name = "codecov" 331 | version = "2.1.13" 332 | description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" 333 | optional = false 334 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 335 | files = [ 336 | {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, 337 | {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, 338 | ] 339 | 340 | [package.dependencies] 341 | coverage = "*" 342 | requests = ">=2.7.9" 343 | 344 | [[package]] 345 | name = "colorama" 346 | version = "0.4.6" 347 | description = "Cross-platform colored terminal text." 348 | optional = false 349 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 350 | files = [ 351 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 352 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 353 | ] 354 | 355 | [[package]] 356 | name = "coverage" 357 | version = "7.6.1" 358 | description = "Code coverage measurement for Python" 359 | optional = false 360 | python-versions = ">=3.8" 361 | files = [ 362 | {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, 363 | {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, 364 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, 365 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, 366 | {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, 367 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, 368 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, 369 | {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, 370 | {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, 371 | {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, 372 | {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, 373 | {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, 374 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, 375 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, 376 | {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, 377 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, 378 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, 379 | {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, 380 | {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, 381 | {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, 382 | {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, 383 | {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, 384 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, 385 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, 386 | {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, 387 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, 388 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, 389 | {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, 390 | {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, 391 | {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, 392 | {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, 393 | {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, 394 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, 395 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, 396 | {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, 397 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, 398 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, 399 | {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, 400 | {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, 401 | {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, 402 | {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, 403 | {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, 404 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, 405 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, 406 | {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, 407 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, 408 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, 409 | {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, 410 | {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, 411 | {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, 412 | {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, 413 | {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, 414 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, 415 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, 416 | {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, 417 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, 418 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, 419 | {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, 420 | {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, 421 | {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, 422 | {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, 423 | {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, 424 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, 425 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, 426 | {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, 427 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, 428 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, 429 | {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, 430 | {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, 431 | {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, 432 | {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, 433 | {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, 434 | ] 435 | 436 | [package.dependencies] 437 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 438 | 439 | [package.extras] 440 | toml = ["tomli"] 441 | 442 | [[package]] 443 | name = "cryptography" 444 | version = "43.0.1" 445 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 446 | optional = false 447 | python-versions = ">=3.7" 448 | files = [ 449 | {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, 450 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, 451 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, 452 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, 453 | {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, 454 | {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, 455 | {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, 456 | {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, 457 | {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, 458 | {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, 459 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, 460 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, 461 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, 462 | {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, 463 | {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, 464 | {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, 465 | {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, 466 | {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, 467 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, 468 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, 469 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, 470 | {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, 471 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, 472 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, 473 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, 474 | {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, 475 | {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, 476 | ] 477 | 478 | [package.dependencies] 479 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 480 | 481 | [package.extras] 482 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 483 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 484 | nox = ["nox"] 485 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 486 | sdist = ["build"] 487 | ssh = ["bcrypt (>=3.1.5)"] 488 | test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 489 | test-randomorder = ["pytest-randomly"] 490 | 491 | [[package]] 492 | name = "distlib" 493 | version = "0.3.9" 494 | description = "Distribution utilities" 495 | optional = false 496 | python-versions = "*" 497 | files = [ 498 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 499 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 500 | ] 501 | 502 | [[package]] 503 | name = "exceptiongroup" 504 | version = "1.2.2" 505 | description = "Backport of PEP 654 (exception groups)" 506 | optional = false 507 | python-versions = ">=3.7" 508 | files = [ 509 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 510 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 511 | ] 512 | 513 | [package.extras] 514 | test = ["pytest (>=6)"] 515 | 516 | [[package]] 517 | name = "filelock" 518 | version = "3.16.1" 519 | description = "A platform independent file lock." 520 | optional = false 521 | python-versions = ">=3.8" 522 | files = [ 523 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 524 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 525 | ] 526 | 527 | [package.extras] 528 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 529 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 530 | typing = ["typing-extensions (>=4.12.2)"] 531 | 532 | [[package]] 533 | name = "h11" 534 | version = "0.14.0" 535 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 536 | optional = false 537 | python-versions = ">=3.7" 538 | files = [ 539 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 540 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 541 | ] 542 | 543 | [[package]] 544 | name = "httpcore" 545 | version = "0.17.3" 546 | description = "A minimal low-level HTTP client." 547 | optional = false 548 | python-versions = ">=3.7" 549 | files = [ 550 | {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, 551 | {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, 552 | ] 553 | 554 | [package.dependencies] 555 | anyio = ">=3.0,<5.0" 556 | certifi = "*" 557 | h11 = ">=0.13,<0.15" 558 | sniffio = "==1.*" 559 | 560 | [package.extras] 561 | http2 = ["h2 (>=3,<5)"] 562 | socks = ["socksio (==1.*)"] 563 | 564 | [[package]] 565 | name = "httpx" 566 | version = "0.24.1" 567 | description = "The next generation HTTP client." 568 | optional = false 569 | python-versions = ">=3.7" 570 | files = [ 571 | {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, 572 | {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, 573 | ] 574 | 575 | [package.dependencies] 576 | certifi = "*" 577 | httpcore = ">=0.15.0,<0.18.0" 578 | idna = "*" 579 | sniffio = "*" 580 | 581 | [package.extras] 582 | brotli = ["brotli", "brotlicffi"] 583 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 584 | http2 = ["h2 (>=3,<5)"] 585 | socks = ["socksio (==1.*)"] 586 | 587 | [[package]] 588 | name = "identify" 589 | version = "2.6.1" 590 | description = "File identification library for Python" 591 | optional = false 592 | python-versions = ">=3.8" 593 | files = [ 594 | {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, 595 | {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, 596 | ] 597 | 598 | [package.extras] 599 | license = ["ukkonen"] 600 | 601 | [[package]] 602 | name = "idna" 603 | version = "3.10" 604 | description = "Internationalized Domain Names in Applications (IDNA)" 605 | optional = false 606 | python-versions = ">=3.6" 607 | files = [ 608 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 609 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 610 | ] 611 | 612 | [package.extras] 613 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 614 | 615 | [[package]] 616 | name = "iniconfig" 617 | version = "2.0.0" 618 | description = "brain-dead simple config-ini parsing" 619 | optional = false 620 | python-versions = ">=3.7" 621 | files = [ 622 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 623 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 624 | ] 625 | 626 | [[package]] 627 | name = "isort" 628 | version = "5.13.2" 629 | description = "A Python utility / library to sort Python imports." 630 | optional = false 631 | python-versions = ">=3.8.0" 632 | files = [ 633 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 634 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 635 | ] 636 | 637 | [package.extras] 638 | colors = ["colorama (>=0.4.6)"] 639 | 640 | [[package]] 641 | name = "mypy" 642 | version = "1.12.0" 643 | description = "Optional static typing for Python" 644 | optional = false 645 | python-versions = ">=3.8" 646 | files = [ 647 | {file = "mypy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed"}, 648 | {file = "mypy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469"}, 649 | {file = "mypy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e"}, 650 | {file = "mypy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a"}, 651 | {file = "mypy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff"}, 652 | {file = "mypy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7"}, 653 | {file = "mypy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57"}, 654 | {file = "mypy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309"}, 655 | {file = "mypy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f"}, 656 | {file = "mypy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9"}, 657 | {file = "mypy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164"}, 658 | {file = "mypy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475"}, 659 | {file = "mypy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9"}, 660 | {file = "mypy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642"}, 661 | {file = "mypy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601"}, 662 | {file = "mypy-1.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521"}, 663 | {file = "mypy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893"}, 664 | {file = "mypy-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721"}, 665 | {file = "mypy-1.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3"}, 666 | {file = "mypy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b"}, 667 | {file = "mypy-1.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f"}, 668 | {file = "mypy-1.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e"}, 669 | {file = "mypy-1.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7"}, 670 | {file = "mypy-1.12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b"}, 671 | {file = "mypy-1.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0"}, 672 | {file = "mypy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8"}, 673 | {file = "mypy-1.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0"}, 674 | {file = "mypy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1"}, 675 | {file = "mypy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e"}, 676 | {file = "mypy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa"}, 677 | {file = "mypy-1.12.0-py3-none-any.whl", hash = "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266"}, 678 | {file = "mypy-1.12.0.tar.gz", hash = "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d"}, 679 | ] 680 | 681 | [package.dependencies] 682 | mypy-extensions = ">=1.0.0" 683 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 684 | typing-extensions = ">=4.6.0" 685 | 686 | [package.extras] 687 | dmypy = ["psutil (>=4.0)"] 688 | install-types = ["pip"] 689 | mypyc = ["setuptools (>=50)"] 690 | reports = ["lxml"] 691 | 692 | [[package]] 693 | name = "mypy-extensions" 694 | version = "1.0.0" 695 | description = "Type system extensions for programs checked with the mypy type checker." 696 | optional = false 697 | python-versions = ">=3.5" 698 | files = [ 699 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 700 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 701 | ] 702 | 703 | [[package]] 704 | name = "nodeenv" 705 | version = "1.9.1" 706 | description = "Node.js virtual environment builder" 707 | optional = false 708 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 709 | files = [ 710 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 711 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 712 | ] 713 | 714 | [[package]] 715 | name = "packaging" 716 | version = "24.1" 717 | description = "Core utilities for Python packages" 718 | optional = false 719 | python-versions = ">=3.8" 720 | files = [ 721 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 722 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 723 | ] 724 | 725 | [[package]] 726 | name = "pathspec" 727 | version = "0.12.1" 728 | description = "Utility library for gitignore style pattern matching of file paths." 729 | optional = false 730 | python-versions = ">=3.8" 731 | files = [ 732 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 733 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 734 | ] 735 | 736 | [[package]] 737 | name = "platformdirs" 738 | version = "4.3.6" 739 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 740 | optional = false 741 | python-versions = ">=3.8" 742 | files = [ 743 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 744 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 745 | ] 746 | 747 | [package.extras] 748 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 749 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 750 | type = ["mypy (>=1.11.2)"] 751 | 752 | [[package]] 753 | name = "pluggy" 754 | version = "1.5.0" 755 | description = "plugin and hook calling mechanisms for python" 756 | optional = false 757 | python-versions = ">=3.8" 758 | files = [ 759 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 760 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 761 | ] 762 | 763 | [package.extras] 764 | dev = ["pre-commit", "tox"] 765 | testing = ["pytest", "pytest-benchmark"] 766 | 767 | [[package]] 768 | name = "pre-commit" 769 | version = "2.21.0" 770 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 771 | optional = false 772 | python-versions = ">=3.7" 773 | files = [ 774 | {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, 775 | {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, 776 | ] 777 | 778 | [package.dependencies] 779 | cfgv = ">=2.0.0" 780 | identify = ">=1.0.0" 781 | nodeenv = ">=0.11.1" 782 | pyyaml = ">=5.1" 783 | virtualenv = ">=20.10.0" 784 | 785 | [[package]] 786 | name = "pycparser" 787 | version = "2.22" 788 | description = "C parser in Python" 789 | optional = false 790 | python-versions = ">=3.8" 791 | files = [ 792 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 793 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 794 | ] 795 | 796 | [[package]] 797 | name = "pyflakes" 798 | version = "3.2.0" 799 | description = "passive checker of Python programs" 800 | optional = false 801 | python-versions = ">=3.8" 802 | files = [ 803 | {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, 804 | {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, 805 | ] 806 | 807 | [[package]] 808 | name = "pytest" 809 | version = "7.4.4" 810 | description = "pytest: simple powerful testing with Python" 811 | optional = false 812 | python-versions = ">=3.7" 813 | files = [ 814 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 815 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 816 | ] 817 | 818 | [package.dependencies] 819 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 820 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 821 | iniconfig = "*" 822 | packaging = "*" 823 | pluggy = ">=0.12,<2.0" 824 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 825 | 826 | [package.extras] 827 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 828 | 829 | [[package]] 830 | name = "pytest-cov" 831 | version = "4.1.0" 832 | description = "Pytest plugin for measuring coverage." 833 | optional = false 834 | python-versions = ">=3.7" 835 | files = [ 836 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 837 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 838 | ] 839 | 840 | [package.dependencies] 841 | coverage = {version = ">=5.2.1", extras = ["toml"]} 842 | pytest = ">=4.6" 843 | 844 | [package.extras] 845 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 846 | 847 | [[package]] 848 | name = "pyyaml" 849 | version = "6.0.2" 850 | description = "YAML parser and emitter for Python" 851 | optional = false 852 | python-versions = ">=3.8" 853 | files = [ 854 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 855 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 856 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 857 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 858 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 859 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 860 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 861 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 862 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 863 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 864 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 865 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 866 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 867 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 868 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 869 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 870 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 871 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 872 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 873 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 874 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 875 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 876 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 877 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 878 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 879 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 880 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 881 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 882 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 883 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 884 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 885 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 886 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 887 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 888 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 889 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 890 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 891 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 892 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 893 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 894 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 895 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 896 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 897 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 898 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 899 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 900 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 901 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 902 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 903 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 904 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 905 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 906 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 907 | ] 908 | 909 | [[package]] 910 | name = "requests" 911 | version = "2.32.3" 912 | description = "Python HTTP for Humans." 913 | optional = false 914 | python-versions = ">=3.8" 915 | files = [ 916 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 917 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 918 | ] 919 | 920 | [package.dependencies] 921 | certifi = ">=2017.4.17" 922 | charset-normalizer = ">=2,<4" 923 | idna = ">=2.5,<4" 924 | urllib3 = ">=1.21.1,<3" 925 | 926 | [package.extras] 927 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 928 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 929 | 930 | [[package]] 931 | name = "sniffio" 932 | version = "1.3.1" 933 | description = "Sniff out which async library your code is running under" 934 | optional = false 935 | python-versions = ">=3.7" 936 | files = [ 937 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 938 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 939 | ] 940 | 941 | [[package]] 942 | name = "starlette" 943 | version = "0.44.0" 944 | description = "The little ASGI library that shines." 945 | optional = false 946 | python-versions = ">=3.8" 947 | files = [ 948 | {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, 949 | {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, 950 | ] 951 | 952 | [package.dependencies] 953 | anyio = ">=3.4.0,<5" 954 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 955 | 956 | [package.extras] 957 | full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] 958 | 959 | [[package]] 960 | name = "tomli" 961 | version = "2.0.2" 962 | description = "A lil' TOML parser" 963 | optional = false 964 | python-versions = ">=3.8" 965 | files = [ 966 | {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, 967 | {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, 968 | ] 969 | 970 | [[package]] 971 | name = "typing-extensions" 972 | version = "4.12.2" 973 | description = "Backported and Experimental Type Hints for Python 3.8+" 974 | optional = false 975 | python-versions = ">=3.8" 976 | files = [ 977 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 978 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 979 | ] 980 | 981 | [[package]] 982 | name = "urllib3" 983 | version = "2.2.3" 984 | description = "HTTP library with thread-safe connection pooling, file post, and more." 985 | optional = false 986 | python-versions = ">=3.8" 987 | files = [ 988 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 989 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 990 | ] 991 | 992 | [package.extras] 993 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 994 | h2 = ["h2 (>=4,<5)"] 995 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 996 | zstd = ["zstandard (>=0.18.0)"] 997 | 998 | [[package]] 999 | name = "virtualenv" 1000 | version = "20.26.6" 1001 | description = "Virtual Python Environment builder" 1002 | optional = false 1003 | python-versions = ">=3.7" 1004 | files = [ 1005 | {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, 1006 | {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, 1007 | ] 1008 | 1009 | [package.dependencies] 1010 | distlib = ">=0.3.7,<1" 1011 | filelock = ">=3.12.2,<4" 1012 | platformdirs = ">=3.9.1,<5" 1013 | 1014 | [package.extras] 1015 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1016 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1017 | 1018 | [metadata] 1019 | lock-version = "2.0" 1020 | python-versions = "^3.8" 1021 | content-hash = "8dc37a24e16c84f3078875ca0396d638203abe5481392cf587ed0ee12fb948b7" 1022 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "starlette-authlib" 3 | version = "0.3.12" 4 | description = "A drop-in replacement for Starlette session middleware, using authlib's jwt" 5 | authors = ["Alessandro Ogier "] 6 | readme = "README.md" 7 | repository = "https://github.com/aogier/starlette-authlib" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8" 11 | authlib = "<1.7" 12 | starlette = "<0.48" 13 | 14 | [tool.poetry.group.dev.dependencies] 15 | autoflake = "<2.3" 16 | black = "^24.0.0" 17 | codecov = "^2.1.12" 18 | httpx = "^0.24.0" 19 | mypy = "^1.1" 20 | pre-commit = "2.21.0" 21 | pytest = "^7" 22 | pytest-cov = "^4" 23 | requests = "^2.28.2" 24 | isort = "^5.11.5" 25 | 26 | [build-system] 27 | requires = ["poetry>=0.12"] 28 | build-backend = "poetry.masonry.api" 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "baseBranches": [ 7 | "develop" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /sample_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aogier/starlette-authlib/f4414f1b234e99975974852b6ad6376af0fca617/sample_app/__init__.py -------------------------------------------------------------------------------- /sample_app/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 20 feb 2020 3 | 4 | @author: Alessandro Ogier 5 | 6 | A sample demo application you can invoke with: 7 | 8 | JWT_ALG=RS256 uvicorn sample_app.app:app 9 | 10 | default alg is HS256 11 | 12 | You can then check it via curl using a cookie-jar 13 | 14 | 1. "check" endpoint returns a 401 15 | 16 | oggei@cane ~ curl -Lv -c /tmp/cookiejar localhost:8000/ 17 | [...] 18 | 19 | > GET / HTTP/1.1 20 | > Host: localhost:8000 21 | > User-Agent: curl/7.67.0 22 | > Accept: */* 23 | > 24 | * Mark bundle as not supporting multiuse 25 | < HTTP/1.1 401 Unauthorized 26 | < date: Fri, 21 Feb 2020 15:24:54 GMT 27 | < server: uvicorn 28 | < transfer-encoding: chunked 29 | < 30 | * Connection #0 to host localhost left intact 31 | 32 | 2. "login" endpoint correctly put you in session, and now / return http/200 33 | 34 | oggei@cane ~ curl -Lv -c /tmp/cookiejar localhost:8000/login 35 | [...] 36 | 37 | > GET /login HTTP/1.1 38 | > Host: localhost:8000 39 | > User-Agent: curl/7.67.0 40 | > Accept: */* 41 | > 42 | * Mark bundle as not supporting multiuse 43 | < HTTP/1.1 307 Temporary Redirect 44 | < date: Fri, 21 Feb 2020 15:25:44 GMT 45 | < server: uvicorn 46 | < location: http://localhost:8000/ 47 | * Added cookie session="eyJhb[...]xHtOQ" for domain localhost, path /, expire 1583508344 48 | < set-cookie: session=eyJhb[...]xHtOQ; path=/; Max-Age=1209600; httponly; samesite=lax 49 | 50 | [...] 51 | 52 | > GET / HTTP/1.1 53 | > Host: localhost:8000 54 | > User-Agent: curl/7.67.0 55 | > Accept: */* 56 | > Cookie: session=eyJhb[...]xHtOQ 57 | > 58 | * Mark bundle as not supporting multiuse 59 | < HTTP/1.1 200 OK 60 | < date: Fri, 21 Feb 2020 15:25:44 GMT 61 | < server: uvicorn 62 | * Replaced cookie session="eyJhb[...]xHtOQ" for domain localhost, path /, expire 1583508344 63 | < set-cookie: session=eyJhb[...]xHtOQ; path=/; Max-Age=1209600; httponly; samesite=lax 64 | < transfer-encoding: chunked 65 | < 66 | * Connection #0 to host localhost left intact 67 | 68 | """ 69 | 70 | import os 71 | 72 | from starlette.applications import Starlette 73 | from starlette.config import Config 74 | from starlette.datastructures import Secret 75 | from starlette.responses import RedirectResponse, Response 76 | from starlette.routing import Route 77 | 78 | if os.environ.get("VANILLA"): 79 | print("using vanilla") 80 | from starlette.middleware.sessions import SessionMiddleware 81 | else: 82 | from starlette_authlib.middleware import ( 83 | AuthlibMiddleware as SessionMiddleware, 84 | SecretKey, 85 | ) 86 | 87 | 88 | config = Config(".env") # pylint: disable=invalid-name 89 | KEYS_DIR = os.path.join(os.path.dirname(__file__), "keys") 90 | 91 | # Override this via command line env vars eg. 92 | # JWT_ALG=RS256 uvicorn sample_app.app:app 93 | JWT_ALG = config("JWT_ALG", cast=str, default="HS256") 94 | 95 | 96 | async def check(request): 97 | """ 98 | Check if we are in session. 99 | """ 100 | content = """ 101 | 102 | starlette authlib demo 103 | 104 | your status is: %(status)s, click here to %(action)s 105 | 106 | """ 107 | if not request.session.get("user"): 108 | return Response( 109 | content % {"status": "logged out", "action": "login"}, 110 | headers={"content-type": "text/html"}, 111 | status_code=401, 112 | ) 113 | 114 | return Response(content % {"status": "logged in", "action": "logout"}) 115 | 116 | 117 | async def login(request): 118 | """ 119 | A login endpoint that creates a session. 120 | """ 121 | request.session.update( 122 | { 123 | "iss": "myself", 124 | "user": "username", 125 | } 126 | ) 127 | return RedirectResponse(url=request.url_for("check")) 128 | 129 | 130 | async def logout(request): 131 | """ 132 | A login endpoint that creates a session. 133 | """ 134 | request.session.clear() 135 | return RedirectResponse(url=request.url_for("check")) 136 | 137 | 138 | routes = [ # pylint: disable=invalid-name 139 | Route("/", endpoint=check, name="check"), 140 | Route("/login", endpoint=login), 141 | Route("/logout", endpoint=logout), 142 | ] 143 | 144 | 145 | app = Starlette(debug=True, routes=routes) # pylint: disable=invalid-name 146 | 147 | if JWT_ALG.startswith("HS"): 148 | secret_key = config( # pylint: disable=invalid-name 149 | "JWT_SECRET", cast=Secret, default="secret" 150 | ) 151 | else: 152 | if JWT_ALG.startswith("RS"): 153 | private_key = open( # pylint: disable=invalid-name 154 | os.path.join(KEYS_DIR, "rsa.key") 155 | ).read() 156 | 157 | public_key = open( # pylint: disable=invalid-name 158 | os.path.join(KEYS_DIR, "rsa.pub") 159 | ).read() 160 | 161 | elif JWT_ALG.startswith("ES"): 162 | private_key = open( # pylint: disable=invalid-name 163 | os.path.join(KEYS_DIR, "ec.key") 164 | ).read() 165 | 166 | public_key = open( # pylint: disable=invalid-name 167 | os.path.join(KEYS_DIR, "ec.pub") 168 | ).read() 169 | 170 | ## WIP: can't find a proper way to generate them 171 | 172 | # elif JWT_ALG.startswith("PS"): 173 | # 174 | # private_key = open( # pylint: disable=invalid-name 175 | # os.path.join(KEYS_DIR, "ps.key") 176 | # ).read() 177 | # 178 | # public_key = open( # pylint: disable=invalid-name 179 | # os.path.join(KEYS_DIR, "ps.pub") 180 | # ).read() 181 | 182 | secret_key = SecretKey( # pylint: disable=invalid-name 183 | Secret(private_key), Secret(public_key) 184 | ) 185 | 186 | app.add_middleware(SessionMiddleware, secret_key=secret_key) 187 | -------------------------------------------------------------------------------- /sample_app/keys/README.md: -------------------------------------------------------------------------------- 1 | # DO NOT USE THOSE CERTIFICATES FOR NOTHING ELSE THAN SAMPLE APP 2 | 3 | Yeah I know you already knew it but anyway: don't use them for anything 4 | else. Go generate your pairs. You really' don't want to use them. You really 5 | don't. 6 | -------------------------------------------------------------------------------- /sample_app/keys/ec.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIHcAgEBBEIBFW5mosW9Q/iQxLMLsodEaJ6wIkcFGWiEw5AvsGUNo3lANJ0d8JP8 3 | 5TxmEzHXK9of71SmaSp4wTj4N1a/+papLaugBwYFK4EEACOhgYkDgYYABAFHAzXC 4 | oImBUCfT0h3XwyJ6tMopzexz6YDhrCLi+E5eWsyd+/v+xDigCU5lR9pv2E9mcj0I 5 | f9eF7xFFws0SX4MFxwH8x3xuAGs8e+EBu2anpWjVK7lzUFqTNw/bw3dqp1bvJM2X 6 | 4IWf5eBN6lrsI1vqRmz3VH8qN4SIRUIjjmtMJZRl5A== 7 | -----END EC PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /sample_app/keys/ec.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBRwM1wqCJgVAn09Id18MierTKKc3s 3 | c+mA4awi4vhOXlrMnfv7/sQ4oAlOZUfab9hPZnI9CH/Xhe8RRcLNEl+DBccB/Md8 4 | bgBrPHvhAbtmp6Vo1Su5c1BakzcP28N3aqdW7yTNl+CFn+XgTepa7CNb6kZs91R/ 5 | KjeEiEVCI45rTCWUZeQ= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /sample_app/keys/rsa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAo+M4isp/TdGmhC0O0LwEv/0USIkCLBlecCn54gZLQzgCymCm 3 | NZPOvHDDp8Wn7pwWtHaKA2EJLHapFw0Maokm/d2KayG0QYkMbQV2ACus1QF/8nme 4 | mdERRj16C8GYTUiR/wkO6HdrYh8Nj+UZ/o57Hbh0BkmNcYdxT1wuBtg4gNP/geod 5 | P4Uhm02BSyclle3IWbtr0O0L/IFX0lBV27rhFcwL7ynORVxivUDW9iuLA9LTSfM4 6 | xJ6JmBs87RtXA0wAYfxkPbWHgnE/85HIB8X4QHQG9AgUNRfomoXfdz+9a7CUQ3qK 7 | l1rd7ObCk7FaIrHWegAVV0b0IPWRCxO+G72rpQIDAQABAoIBAQCJQBCpQ18L5+kB 8 | Rs7ihqOfua4T9RHpprFNVAoefVDokW7c18Y6lneLnxBsX/78uKHXLnL0034gS5ve 9 | XyIn0obiEngKsmZIgCL05p7lE66hkKj7g1kMzmceIPwl/lKzXnROVuKyUtiOix0L 10 | hj2XzvFSeZhJb63SBzEg2jz9pHTlhc4zweOT+urbfgltuIWSOmNEU76EWKw0W7xB 11 | 0UTDvbunlmE4DZtuoewsB9eFphPjTC9WRL23gmM1RLDNHFdzA/UXPH3/FJpvwV+B 12 | jk+rf9y6dn5ZN/qTbDNj5qaP/rCXDngNLZlR0Rz4moABicp/jTAGbO9u2cPzzVwk 13 | g3kC2Es9AoGBANc/RT0+RqTh+E9y65WrQyLl84cSPHz4wkKEnMdoZwmvysARgbYY 14 | WTUz9YUnUk/Azc5sS9gDH18Bu5IGSXIA2RnwTua1vK1Lvx6+fu+grJgEL22TFVi7 15 | AbO8ZVuEc9nYD6++Uoc1j8GcrH3Y3wN59IMFrFskdS9O0b1F+WMdYr2PAoGBAMLq 16 | n6dza0xBXOVVKCOAOj0zrejSMLLGkdwnahTq4Wan/aBfH9WDN2B/py4qcqptzgwb 17 | fPE2cIWTUa3U7U8HA2ZOOL3sfCLP0PgFmxya4AEFhtel/zXVZDQq5YFWi8Lv5bkj 18 | n2+CAMY6/SY60MowAqTj408gOgHqdUjnsMWeMdGLAoGAcQ0NG6Z6yhzZIzlTnK9v 19 | uoOWDWo5lNW+idtG0MD35TGMRqarPu245OWeXUoSxEqajYF5sEGrl2W8k2xz5Mmn 20 | PSVm+2uWZlpzTf11g/wayljgZCetPYYy1ajsf02o31DIpGBMzjEGiQT1378pGpVS 21 | JAK1zMTwO2GlbUi0PCdx9ecCgYBJ6pnS8aUuDNeCrJGFLPpo/TElSphnI4Bq4ZOM 22 | DSqfOHOpHom++XzFnoMysaw8T1nFI6N1AfUX4q8l4cqAnqM+Z8mC1Kyasv6HH/Eb 23 | nuv11ze0jeHDc4IFeoMAUma77SfL1uN+cOmEQxh/J+zHz/8gsr8f4ZO96EDD/gD+ 24 | YCh6UwKBgA7DfbgbkmtkR1f87c7qrh2trZD776aksa1PpLE7plE/ymbZDBnV/SBT 25 | +ROzRKyCeWYtR5YGeYz496r7v7is3chYb/KKaC5Cfm7T14OZmu042IJcW/cPmX9V 26 | EA5QW9vO+HlcoQoR72VGs2QS4oPNLqSSnw+1uCh4Dg60gvAcY2za 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /sample_app/keys/rsa.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+M4isp/TdGmhC0O0LwE 3 | v/0USIkCLBlecCn54gZLQzgCymCmNZPOvHDDp8Wn7pwWtHaKA2EJLHapFw0Maokm 4 | /d2KayG0QYkMbQV2ACus1QF/8nmemdERRj16C8GYTUiR/wkO6HdrYh8Nj+UZ/o57 5 | Hbh0BkmNcYdxT1wuBtg4gNP/geodP4Uhm02BSyclle3IWbtr0O0L/IFX0lBV27rh 6 | FcwL7ynORVxivUDW9iuLA9LTSfM4xJ6JmBs87RtXA0wAYfxkPbWHgnE/85HIB8X4 7 | QHQG9AgUNRfomoXfdz+9a7CUQ3qKl1rd7ObCk7FaIrHWegAVV0b0IPWRCxO+G72r 8 | pQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /starlette_authlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aogier/starlette-authlib/f4414f1b234e99975974852b6ad6376af0fca617/starlette_authlib/__init__.py -------------------------------------------------------------------------------- /starlette_authlib/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 20 feb 2020 3 | 4 | @author: Alessandro Ogier 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import time 10 | import typing 11 | from collections import namedtuple 12 | 13 | from authlib.jose import jwt 14 | from authlib.jose.errors import ( 15 | BadSignatureError, 16 | DecodeError, 17 | ExpiredTokenError, 18 | InvalidTokenError, 19 | ) 20 | from starlette.config import Config 21 | from starlette.datastructures import MutableHeaders, Secret 22 | from starlette.requests import HTTPConnection 23 | from starlette.types import ASGIApp, Message, Receive, Scope, Send 24 | 25 | config = Config(".env") 26 | 27 | SecretKey = namedtuple("SecretKey", ("encode", "decode")) 28 | 29 | 30 | class AuthlibMiddleware: 31 | def __init__( 32 | self, 33 | app: ASGIApp, 34 | secret_key: typing.Union[str, Secret, SecretKey], 35 | session_cookie: str = "session", 36 | max_age: int | None = 14 * 24 * 60 * 60, # 14 days, in seconds 37 | path: str = "/", 38 | same_site: str = "lax", 39 | https_only: bool = False, 40 | domain: typing.Optional[str] = config("DOMAIN", cast=str, default=None), 41 | jwt_alg: str = config("JWT_ALG", cast=str, default="HS256"), 42 | ) -> None: 43 | self.app = app 44 | 45 | self.jwt_header = {"alg": jwt_alg} 46 | if not isinstance(secret_key, SecretKey): 47 | self.jwt_secret = SecretKey(Secret(str(secret_key)), None) 48 | else: 49 | self.jwt_secret = secret_key 50 | 51 | # check crypto setup so we bail out if needed 52 | _jwt = jwt.encode(self.jwt_header, {"1": 2}, str(self.jwt_secret.encode)) 53 | assert {"1": 2} == jwt.decode( 54 | _jwt, 55 | str( 56 | self.jwt_secret.decode 57 | if self.jwt_secret.decode 58 | else self.jwt_secret.encode 59 | ), 60 | ), "wrong crypto setup" 61 | 62 | self.session_cookie = session_cookie 63 | self.max_age = max_age 64 | self.path = path 65 | self.security_flags = "httponly; samesite=" + same_site 66 | if https_only: # Secure flag can be used with HTTPS only 67 | self.security_flags += "; secure" 68 | if domain is not None: 69 | self.security_flags += f"; domain={domain}" 70 | 71 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 72 | if scope["type"] not in ("http", "websocket"): # pragma: no cover 73 | await self.app(scope, receive, send) 74 | return 75 | 76 | connection = HTTPConnection(scope) 77 | initial_session_was_empty = True 78 | 79 | if self.session_cookie in connection.cookies: 80 | data = connection.cookies[self.session_cookie].encode("utf-8") 81 | try: 82 | jwt_payload = jwt.decode( 83 | data, 84 | str( 85 | self.jwt_secret.decode 86 | if self.jwt_secret.decode 87 | else self.jwt_secret.encode 88 | ), 89 | ) 90 | jwt_payload.validate_exp(time.time(), 0) 91 | jwt_payload.validate_nbf(time.time(), 0) 92 | scope["session"] = jwt_payload 93 | initial_session_was_empty = False 94 | except ( 95 | BadSignatureError, 96 | ExpiredTokenError, 97 | DecodeError, 98 | InvalidTokenError, 99 | ): 100 | scope["session"] = {} 101 | else: 102 | scope["session"] = {} 103 | 104 | async def send_wrapper(message: Message) -> None: 105 | if message["type"] == "http.response.start": 106 | if scope["session"]: 107 | if "exp" not in scope["session"]: 108 | scope["session"]["exp"] = int(time.time()) + ( 109 | self.max_age if self.max_age else 0 110 | ) 111 | data = jwt.encode( 112 | self.jwt_header, scope["session"], str(self.jwt_secret.encode) 113 | ) 114 | 115 | headers = MutableHeaders(scope=message) 116 | header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format( # noqa E501 117 | session_cookie=self.session_cookie, 118 | data=data.decode("utf-8"), 119 | path=self.path, 120 | max_age=f"Max-Age={self.max_age}; " if self.max_age else "", 121 | security_flags=self.security_flags, 122 | ) 123 | headers.append("Set-Cookie", header_value) 124 | elif not initial_session_was_empty: 125 | # The session has been cleared. 126 | headers = MutableHeaders(scope=message) 127 | header_value = "{session_cookie}=null; path={path}; {expires}{security_flags}".format( # noqa E501 128 | session_cookie=self.session_cookie, 129 | path=self.path, 130 | expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ", 131 | security_flags=self.security_flags, 132 | ) 133 | headers.append("Set-Cookie", header_value) 134 | await send(message) 135 | 136 | await self.app(scope, receive, send_wrapper) 137 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aogier/starlette-authlib/f4414f1b234e99975974852b6ad6376af0fca617/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from datetime import datetime, timedelta 4 | 5 | import pytest 6 | from starlette.applications import Starlette 7 | from starlette.datastructures import Secret 8 | from starlette.middleware import Middleware 9 | from starlette.responses import JSONResponse 10 | from starlette.routing import Mount, Route 11 | from starlette.testclient import TestClient 12 | 13 | from starlette_authlib.middleware import ( 14 | AuthlibMiddleware as SessionMiddleware, 15 | SecretKey, 16 | ) 17 | 18 | KEYS_DIR = os.path.join(os.path.dirname(__file__), "..", "sample_app", "keys") 19 | 20 | 21 | def view_session(request): 22 | return JSONResponse({"session": request.session}) 23 | 24 | 25 | async def update_session(request): 26 | data = await request.json() 27 | request.session.update(data) 28 | return JSONResponse({"session": request.session}) 29 | 30 | 31 | async def clear_session(request): 32 | request.session.clear() 33 | return JSONResponse({"session": request.session}) 34 | 35 | 36 | def create_app(): 37 | app = Starlette() 38 | app.add_route("/view_session", view_session) 39 | app.add_route("/update_session", update_session, methods=["POST"]) 40 | app.add_route("/clear_session", clear_session, methods=["POST"]) 41 | return app 42 | 43 | 44 | def test_failing_session_setup(): 45 | jwt_alg = "ES256" 46 | secret_key = SecretKey( 47 | Secret(open(os.path.join(KEYS_DIR, "ec.key")).read()), 48 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 49 | ) 50 | 51 | app = create_app() 52 | with pytest.raises(Exception): 53 | app.add_middleware(SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key) 54 | app.build_middleware_stack() 55 | 56 | 57 | def test_session(): 58 | for jwt_alg, secret_key in ( 59 | ("HS256", "example"), 60 | ( 61 | "RS256", 62 | SecretKey( 63 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 64 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 65 | ), 66 | ), 67 | ): 68 | app = create_app() 69 | app.add_middleware(SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key) 70 | client = TestClient(app) 71 | 72 | response = client.get("/view_session") 73 | assert response.json() == {"session": {}} 74 | 75 | response = client.post("/update_session", json={"some": "data"}) 76 | assert response.json() == {"session": {"some": "data"}} 77 | 78 | # check cookie max-age 79 | set_cookie = response.headers["set-cookie"] 80 | max_age_matches = re.search(r"; Max-Age=([0-9]+);", set_cookie) 81 | assert max_age_matches is not None 82 | assert int(max_age_matches[1]) == 14 * 24 * 3600 83 | 84 | response = client.get("/view_session").json() 85 | assert "exp" in response["session"] 86 | del response["session"]["exp"] 87 | assert response == {"session": {"some": "data"}} 88 | 89 | response = client.post("/clear_session") 90 | assert response.json() == {"session": {}} 91 | 92 | response = client.get("/view_session") 93 | assert response.json() == {"session": {}} 94 | 95 | 96 | def test_session_expires(): 97 | for jwt_alg, secret_key in ( 98 | ("HS256", "example"), 99 | ( 100 | "RS256", 101 | SecretKey( 102 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 103 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 104 | ), 105 | ), 106 | ): 107 | app = create_app() 108 | app.add_middleware( 109 | SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key, max_age=-1 110 | ) 111 | client = TestClient(app) 112 | 113 | response = client.post("/update_session", json={"some": "data"}) 114 | assert response.json() == {"session": {"some": "data"}} 115 | 116 | # requests removes expired cookies from response.cookies, we need to 117 | # fetch session id from the headers and pass it explicitly 118 | expired_cookie_header = response.headers["set-cookie"] 119 | expired_session_value = re.search(r"session=([^;]*);", expired_cookie_header)[1] 120 | 121 | response = client.get( 122 | "/view_session", cookies={"session": expired_session_value} 123 | ) 124 | assert response.json() == {"session": {}} 125 | 126 | 127 | def test_session_futue_nbf(): 128 | now = datetime.now() 129 | nbf = datetime.timestamp(now + timedelta(days=1)) 130 | claims = {"nbf": nbf, "some": "data"} 131 | for jwt_alg, secret_key in ( 132 | ("HS256", "example"), 133 | ( 134 | "RS256", 135 | SecretKey( 136 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 137 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 138 | ), 139 | ), 140 | ): 141 | app = create_app() 142 | app.add_middleware( 143 | SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key, https_only=True 144 | ) 145 | secure_client = TestClient(app, base_url="https://testserver") 146 | 147 | response = secure_client.get("/view_session") 148 | assert response.json() == {"session": {}} 149 | 150 | response = secure_client.post("/update_session", json=claims.copy()) 151 | assert response.json() == {"session": claims.copy()} 152 | 153 | response = secure_client.get("/view_session").json() 154 | assert response == {"session": {}} 155 | 156 | response = secure_client.post("/clear_session") 157 | assert response.json() == {"session": {}} 158 | 159 | response = secure_client.get("/view_session") 160 | assert response.json() == {"session": {}} 161 | 162 | 163 | def test_session_past_nbf(): 164 | now = datetime.now() 165 | nbf = datetime.timestamp(now - timedelta(seconds=1)) 166 | claims = {"nbf": nbf, "some": "data"} 167 | for jwt_alg, secret_key in ( 168 | ("HS256", "example"), 169 | ( 170 | "RS256", 171 | SecretKey( 172 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 173 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 174 | ), 175 | ), 176 | ): 177 | app = create_app() 178 | app.add_middleware( 179 | SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key, https_only=True 180 | ) 181 | secure_client = TestClient(app, base_url="https://testserver") 182 | 183 | response = secure_client.get("/view_session") 184 | assert response.json() == {"session": {}} 185 | 186 | response = secure_client.post("/update_session", json=claims.copy()) 187 | assert response.json() == {"session": claims.copy()} 188 | 189 | response = secure_client.get("/view_session").json() 190 | assert "exp" in response["session"] 191 | del response["session"]["exp"] 192 | assert response == {"session": claims.copy()} 193 | 194 | response = secure_client.post("/clear_session") 195 | assert response.json() == {"session": {}} 196 | 197 | response = secure_client.get("/view_session") 198 | assert response.json() == {"session": {}} 199 | 200 | 201 | def test_secure_session(): 202 | for jwt_alg, secret_key in ( 203 | ("HS256", "example"), 204 | ( 205 | "RS256", 206 | SecretKey( 207 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 208 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 209 | ), 210 | ), 211 | ): 212 | app = create_app() 213 | app.add_middleware( 214 | SessionMiddleware, jwt_alg=jwt_alg, secret_key=secret_key, https_only=True 215 | ) 216 | secure_client = TestClient(app, base_url="https://testserver") 217 | unsecure_client = TestClient(app, base_url="http://testserver") 218 | 219 | response = unsecure_client.get("/view_session") 220 | assert response.json() == {"session": {}} 221 | 222 | response = unsecure_client.post("/update_session", json={"some": "data"}) 223 | assert response.json() == {"session": {"some": "data"}} 224 | 225 | response = unsecure_client.get("/view_session") 226 | assert response.json() == {"session": {}} 227 | 228 | response = secure_client.get("/view_session") 229 | assert response.json() == {"session": {}} 230 | 231 | response = secure_client.post("/update_session", json={"some": "data"}) 232 | assert response.json() == {"session": {"some": "data"}} 233 | 234 | response = secure_client.get("/view_session").json() 235 | assert "exp" in response["session"] 236 | del response["session"]["exp"] 237 | assert response == {"session": {"some": "data"}} 238 | 239 | response = secure_client.post("/clear_session") 240 | assert response.json() == {"session": {}} 241 | 242 | response = secure_client.get("/view_session") 243 | assert response.json() == {"session": {}} 244 | 245 | 246 | def test_session_cookie_subpath(): 247 | for jwt_alg, secret_key in ( 248 | ("HS256", "example"), 249 | ( 250 | "RS256", 251 | SecretKey( 252 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 253 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 254 | ), 255 | ), 256 | ): 257 | second_app = Starlette( 258 | routes=[ 259 | Route( 260 | "/update_session", 261 | endpoint=update_session, 262 | methods=["POST"], 263 | ), 264 | ], 265 | middleware=[ 266 | Middleware( 267 | SessionMiddleware, 268 | jwt_alg=jwt_alg, 269 | secret_key=secret_key, 270 | path="/second_app", 271 | ) 272 | ], 273 | ) 274 | 275 | app = Starlette(routes=[Mount("/second_app", app=second_app)]) 276 | client = TestClient(app, base_url="https://testserver") 277 | response = client.post("/second_app/update_session", json={"some": "data"}) 278 | assert response.status_code == 200 279 | cookie = response.headers["set-cookie"] 280 | cookie_path_match = re.search(r"; path=(\S+);", cookie) 281 | assert cookie_path_match is not None 282 | cookie_path = cookie_path_match.groups()[0] 283 | assert cookie_path == "/second_app" 284 | 285 | 286 | def test_domain_cookie() -> None: 287 | for jwt_alg, secret_key in ( 288 | ("HS256", "example"), 289 | ( 290 | "RS256", 291 | SecretKey( 292 | Secret(open(os.path.join(KEYS_DIR, "rsa.key")).read()), 293 | Secret(open(os.path.join(KEYS_DIR, "rsa.pub")).read()), 294 | ), 295 | ), 296 | ): 297 | app = Starlette( 298 | routes=[ 299 | Route("/view_session", endpoint=view_session), 300 | Route("/update_session", endpoint=update_session, methods=["POST"]), 301 | ], 302 | middleware=[ 303 | Middleware( 304 | SessionMiddleware, 305 | jwt_alg=jwt_alg, 306 | secret_key=secret_key, 307 | domain=".example.com", 308 | ) 309 | ], 310 | ) 311 | client = TestClient(app, base_url="https://testserver") 312 | 313 | response = client.post("/update_session", json={"some": "data"}) 314 | assert response.json() == {"session": {"some": "data"}} 315 | 316 | # check cookie max-age 317 | set_cookie = response.headers["set-cookie"] 318 | assert "domain=.example.com" in set_cookie 319 | 320 | client.cookies.delete("session") 321 | response = client.get("/view_session") 322 | assert response.json() == {"session": {}} 323 | --------------------------------------------------------------------------------