├── .docker └── Dockerfile ├── .flake8 ├── .github ├── dependabot.yaml └── workflows │ ├── ci.yaml │ ├── docs.yaml │ └── publish.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yaml ├── docs ├── css │ └── extra.css ├── gen_ref_pages.py ├── images │ ├── starlite-banner.svg │ ├── starlite-favicon.ico │ └── starlite-icon@2x.png └── index.md ├── mkdocs.yml ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── requirements-lint.txt ├── sonar-project.properties ├── starlite-banner.svg ├── starlite_jwt ├── __init__.py ├── jwt_auth.py ├── middleware.py ├── py.typed └── token.py └── tests ├── conftest.py ├── test_jwt_auth.py └── test_token.py /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # this file is used to create an image for running the docs. 2 | FROM squidfunk/mkdocs-material 3 | 4 | RUN pip install --upgrade pip \ 5 | && pip install --no-cache-dir \ 6 | black \ 7 | mkdocstrings[python] \ 8 | mkdocs-gen-files \ 9 | mkdocs-literate-nav \ 10 | mkdocs-section-index 11 | 12 | ENTRYPOINT ["mkdocs"] 13 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | max-complexity = 12 4 | type-checking-pydantic-enabled = true 5 | ignore = E501, PT006 6 | classmethod-decorators = 7 | classmethod 8 | validator 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | env: 11 | SETUPTOOLS_USE_DISTUTILS: stdlib 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | - uses: pre-commit/action@v3.0.0 18 | test: 19 | needs: validate 20 | runs-on: ubuntu-latest 21 | strategy: 22 | fail-fast: true 23 | matrix: 24 | python-version: ["3.8", "3.9", "3.10"] 25 | steps: 26 | - name: Check out repository 27 | uses: actions/checkout@v3 28 | - name: Set up python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install Poetry 33 | uses: snok/install-poetry@v1 34 | with: 35 | virtualenvs-create: true 36 | virtualenvs-in-project: true 37 | installer-parallel: true 38 | - name: Load cached venv 39 | id: cached-poetry-dependencies 40 | uses: actions/cache@v3 41 | with: 42 | path: .venv 43 | key: v1-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 44 | restore-keys: | 45 | v1-venv-${{ runner.os }}-${{ matrix.python-version }} 46 | v1-venv-${{ runner.os }} 47 | - name: Install dependencies 48 | run: poetry install --no-interaction --no-root 49 | - name: Set pythonpath 50 | run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV 51 | - name: Test 52 | if: matrix.python-version != '3.10' 53 | run: poetry run pytest 54 | - name: Test with Coverage 55 | if: matrix.python-version == '3.10' 56 | run: poetry run pytest tests --cov=starlite_jwt--cov-report=xml 57 | - uses: actions/upload-artifact@v3 58 | if: matrix.python-version == '3.10' 59 | with: 60 | name: coverage-xml 61 | path: coverage.xml 62 | sonar: 63 | needs: 64 | - test 65 | if: github.event.pull_request.head.repo.fork == false 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Check out repository 69 | uses: actions/checkout@v3 70 | - name: Download Artifacts 71 | uses: actions/download-artifact@v3 72 | with: 73 | name: coverage-xml 74 | - name: Fix coverage file for sonarcloud 75 | run: sed -i "s/home\/runner\/work\/starlite-jwt\/starlite_jwt/github\/workspace/g" coverage.xml 76 | - name: SonarCloud Scan 77 | uses: sonarsource/sonarcloud-github-action@master 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 81 | snyk: 82 | needs: 83 | - test 84 | if: github.event.pull_request.head.repo.fork == false 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@master 88 | - name: Run Snyk Monitor 89 | if: ${{ github.ref == 'refs/heads/main' }} 90 | uses: snyk/actions/python-3.8@master 91 | with: 92 | command: monitor 93 | env: 94 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 95 | - name: Run Snyk Test 96 | if: ${{ github.ref != 'refs/heads/main' }} 97 | uses: snyk/actions/python-3.8@master 98 | with: 99 | command: test 100 | env: 101 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 102 | codeql: 103 | needs: 104 | - test 105 | runs-on: ubuntu-latest 106 | permissions: 107 | security-events: write 108 | steps: 109 | - name: Initialize CodeQL 110 | uses: github/codeql-action/init@v2 111 | with: 112 | languages: python 113 | - name: Checkout repository 114 | uses: actions/checkout@v3 115 | - name: Load cached venv 116 | id: cached-poetry-dependencies 117 | uses: actions/cache@v3 118 | with: 119 | path: .venv 120 | key: v1-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 121 | restore-keys: | 122 | v1-venv-${{ runner.os }}-${{ matrix.python-version }} 123 | v1-venv-${{ runner.os }} 124 | - name: Perform CodeQL Analysis 125 | uses: github/codeql-action/analyze@v2 126 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | workflow_run: 4 | workflows: ["ci"] 5 | branches: [main] 6 | types: 7 | - completed 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | - run: > 18 | pip install mkdocs-material 19 | black 20 | mkdocstrings[python] 21 | mkdocs-gen-files 22 | mkdocs-literate-nav 23 | mkdocs-section-index 24 | - run: mkdocs gh-deploy --force 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish-release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out repository 10 | uses: actions/checkout@v3 11 | - name: Set up python 3.10 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.10" 15 | - name: Install Poetry 16 | uses: snok/install-poetry@v1 17 | - name: Install dependencies 18 | run: poetry install --no-interaction --no-root --no-dev 19 | - name: publish 20 | shell: bash 21 | run: | 22 | poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} 23 | poetry publish --build --no-interaction 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .mypy_cache/ 3 | .pytest_cache/ 4 | .hypothesis/ 5 | __pycache__/ 6 | *.iml 7 | .venv 8 | .env 9 | .vscode 10 | .python-version 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: check-ast 6 | - id: check-case-conflict 7 | - id: check-merge-conflict 8 | - id: debug-statements 9 | - id: end-of-file-fixer 10 | exclude: "\\.idea/(.)*" 11 | - id: trailing-whitespace 12 | - repo: https://github.com/asottile/pyupgrade 13 | rev: v3.0.0 14 | hooks: 15 | - id: pyupgrade 16 | args: ["--py38-plus"] 17 | - repo: https://github.com/hadialqattan/pycln 18 | rev: v2.1.1 19 | hooks: 20 | - id: pycln 21 | args: [--config=pyproject.toml] 22 | - repo: https://github.com/pycqa/isort 23 | rev: 5.10.1 24 | hooks: 25 | - id: isort 26 | - repo: https://github.com/psf/black 27 | rev: 22.8.0 28 | hooks: 29 | - id: black 30 | args: [--config=./pyproject.toml] 31 | - repo: https://github.com/codespell-project/codespell 32 | rev: v2.2.1 33 | hooks: 34 | - id: codespell 35 | - repo: https://github.com/asottile/blacken-docs 36 | rev: v1.12.1 37 | hooks: 38 | - id: blacken-docs 39 | - repo: https://github.com/pre-commit/mirrors-prettier 40 | rev: "v3.0.0-alpha.0" 41 | hooks: 42 | - id: prettier 43 | - repo: https://github.com/pycqa/bandit 44 | rev: 1.7.4 45 | hooks: 46 | - id: bandit 47 | exclude: "test_*" 48 | args: ["-iii", "-ll", "-s=B308,B703"] 49 | - repo: https://github.com/igorshubovych/markdownlint-cli 50 | rev: v0.32.2 51 | hooks: 52 | - id: markdownlint 53 | args: [--disable=MD013, --disable=MD033, --disable=MD050] 54 | - repo: https://github.com/PyCQA/docformatter 55 | rev: v1.5.0 56 | hooks: 57 | - id: docformatter 58 | args: [--in-place] 59 | - repo: https://gitlab.com/pycqa/flake8 60 | rev: 3.9.2 61 | hooks: 62 | - id: flake8 63 | additional_dependencies: 64 | [ 65 | "flake8-bugbear", 66 | "flake8-comprehensions", 67 | "flake8-mutable", 68 | "flake8-print", 69 | "flake8-simplify", 70 | "flake8-type-checking", 71 | "flake8-pytest-style", 72 | "flake8-implicit-str-concat", 73 | "flake8-noqa", 74 | ] 75 | - repo: https://github.com/johnfraney/flake8-markdown 76 | rev: v0.4.0 77 | hooks: 78 | - id: flake8-markdown 79 | - repo: https://github.com/dosisod/refurb 80 | rev: v1.2.0 81 | hooks: 82 | - id: refurb 83 | additional_dependencies: [starlite, python-jose, mkdocs_gen_files] 84 | - repo: https://github.com/pycqa/pylint 85 | rev: "v2.15.3" 86 | hooks: 87 | - id: pylint 88 | exclude: "test_*|docs" 89 | args: ["--unsafe-load-any-extension=y"] 90 | additional_dependencies: [starlite, python-jose, mkdocs_gen_files] 91 | - repo: https://github.com/pre-commit/mirrors-mypy 92 | rev: "v0.982" 93 | hooks: 94 | - id: mypy 95 | additional_dependencies: 96 | [starlite, python-jose, mkdocs_gen_files, types-python-jose] 97 | - repo: https://github.com/RobertCraigie/pyright-python 98 | rev: v1.1.273 99 | hooks: 100 | - id: pyright 101 | additional_dependencies: 102 | [starlite, python-jose, mkdocs_gen_files, pytest, hypothesis] 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [1.0.0] 4 | 5 | - initial release 6 | 7 | [1.1.0] 8 | 9 | - add `cryptography` as `python-jose` backend 10 | - update `jwt-auth` to have `openapi_components` and `security_requirements` properties that can be used for OpenAPI 3.1 docs generation. 11 | - update to `Token` model. 12 | 13 | [1.1.1] 14 | 15 | - update dependencies and adjust `authenticate_request` to Starlite `1.6.0+` 16 | 17 | [1.2.0] 18 | 19 | - update implementation for Starlite `1.16.0+` compatibility. 20 | 21 | [1.3.0] 22 | 23 | - add `JWTCookieAuth` as an additional JWT backend. 24 | - add `OAuth2PasswordBearerAuth` as a pre-configured JWT backend. 25 | - update implementation for Starlite `1.20.0+` compatibility. 26 | 27 | [1.4.0] 28 | 29 | - add Python `3.11` support. 30 | - require Starlite `>=1.24.0`. 31 | - update `RetrieveUserHandler` to support accepting the `connection` as an arg. 32 | 33 | [1.4.1] 34 | 35 | - updated authentication header and cookie to include the security scheme prefixed to the JWT token 36 | 37 | [1.5.0] 38 | 39 | - Updates references to starlette to use starlite 40 | - remove Python `3.7` support 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socioeconomic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from 118 | the [Contributor Covenant version 2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 121 | 122 | For answers to common questions about this code of conduct, see [the FAQ](https://www.contributor-covenant.org/faq) 123 | and [translations](https://www.contributor-covenant.org/translations). 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines 2 | 3 | To contribute code changes or update the documentation, please follow these steps: 4 | 5 | 1. Fork the upstream repository and clone the fork locally. 6 | 2. Install [poetry](https://python-poetry.org/), and install the project's dependencies with `poetry install`. 7 | 3. Install [pre-commit](https://pre-commit.com/) and install the hooks by running `pre-commit install` in the 8 | repository's hook. 9 | 4. Make whatever changes and additions you wish and commit these - please try to keep your commit history clean. 10 | 5. Create a pull request to the main repository with an explanation of your changes. The PR should detail the 11 | contribution and link to any related issues - if existing. 12 | 13 | ## Docs 14 | 15 | ### Docs Theme and Appearance 16 | 17 | We welcome contributions that enhance / improve the appearance and usability of the docs, as well as any images, icons 18 | etc. 19 | 20 | We use the excellent [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme, which comes with a lot 21 | of options out of the box. If you wish to contribute to the docs style / setup, or static site generation, you should 22 | consult the theme docs as a first step. 23 | 24 | ### Running the Docs Locally 25 | 26 | To run the docs locally, simply use the `docker-compose` configuration in place by executing `docker compose up`. 27 | On the first run it will pull and build the image, but afterwards this should be quite fast. 28 | 29 | Note: if you want your terminal back use `docker compose up --detach` but then you will need to bring the docs down 30 | with `docker compose down` rather than ctrl+C. 31 | 32 | ### Writing and Editing Docs 33 | 34 | We welcome contributions that enhance / improve the content of the docs. Feel free to add examples, clarify text, 35 | restructure the docs etc. But make sure to follow these emphases: 36 | 37 | - the docs should be as simple and easy to grasp as possible. 38 | - the docs should be written in good idiomatic english. 39 | - examples should be simple and clear. 40 | - provide links where applicable. 41 | - provide diagrams where applicable and possible. 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Starlite-API 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starlite JWT 2 | 3 | DEPRECATED: from version `1.43.0` Starlite includes this functionality under `starlite.contrib.jwt` 4 | 5 | 6 | Starlite logo 7 | 8 | 9 |
10 | 11 | ![PyPI - License](https://img.shields.io/pypi/l/starlite-jwt?color=blue) 12 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/starlite-jwt) 13 | 14 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 15 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=coverage)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 16 | 17 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 18 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 19 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 20 | [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 21 | 22 | [![Discord](https://img.shields.io/discord/919193495116337154?color=blue&label=chat%20on%20discord&logo=discord)](https://discord.gg/X3FJqy8d2j) 23 | [![Matrix](https://img.shields.io/badge/%5Bm%5D%20chat%20on%20Matrix-bridged-blue)](https://matrix.to/#/#starlitespace:matrix.org) 24 | 25 |
26 | 27 | This library offers simple JWT authentication for [Starlite](https://github.com/starlite-api/starlite). 28 | 29 | Checkout [the docs 📚](https://starlite-api.github.io/starlite-jwt/). 30 | 31 | ## Installation 32 | 33 | ```shell 34 | pip install starlite-jwt 35 | ``` 36 | 37 | This library uses the excellent [python-jose](https://github.com/mpdavis/python-jose) library, which supports multiple 38 | cryptographic backends. You can install either [pyca/cryptography](http://cryptography.io/) 39 | or [pycryptodome](https://pycryptodome.readthedocs.io/en/latest/), and it will be used as the backend automatically. Note 40 | that if you want to use a certificate based encryption scheme, you must install one of these backends - please refer to 41 | the [python-jose](https://github.com/mpdavis/python-jose) readme for more details. 42 | 43 | ## Example 44 | 45 | ```python 46 | import os 47 | from typing import Any, Optional 48 | from uuid import UUID, uuid4 49 | 50 | from pydantic import BaseModel, EmailStr 51 | from starlite import OpenAPIConfig, Request, Response, ASGIConnection, Starlite, get 52 | 53 | from starlite_jwt import JWTAuth, Token 54 | 55 | 56 | # Let's assume we have a User model that is a pydantic model. 57 | # This though is not required - we need some sort of user class - 58 | # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. 59 | class User(BaseModel): 60 | id: UUID 61 | name: str 62 | email: EmailStr 63 | 64 | 65 | # The JWTAuth package requires a handler callable that takes a unique identifier, and returns the 'User' 66 | # instance correlating to it. 67 | # 68 | # The identifier is the 'sub' key of the JWT, and it usually correlates to a user ID. 69 | # It can be though any arbitrary value you decide upon - as long as the handler function provided 70 | # can receive this value and return the model instance for it. 71 | # 72 | # Note: The callable can be either sync or async - both will work. 73 | async def retrieve_user_handler( 74 | unique_identifier: str, connection: ASGIConnection[Any, Any, Any] 75 | ) -> Optional[User]: 76 | # logic here to retrieve the user instance 77 | ... 78 | 79 | 80 | # The minimal configuration required for the library is the callable for the 'retrieve_user_handler' key, and a string 81 | # value for the token secret. 82 | # 83 | # Important: secrets should never be hardcoded. Its best practice to pass the secret using ENV. 84 | # 85 | # Tip: It's also a good idea to use the pydantic settings management functionality 86 | jwt_auth = JWTAuth( 87 | retrieve_user_handler=retrieve_user_handler, 88 | token_secret=os.environ.get("JWT_SECRET", "abcd123"), 89 | # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint 90 | # and our openAPI docs. 91 | exclude=["/login", "/schema"], 92 | ) 93 | 94 | 95 | # Given an instance of 'JWTAuth' we can create a login handler function: 96 | @get("/login") 97 | def login_handler() -> Response[User]: 98 | # we have a user instance - probably by retrieving it from persistence using another lib. 99 | # what's important for our purposes is to have an identifier: 100 | user = User(name="Moishe Zuchmir", email="zuchmir@moishe.com", id=uuid4()) 101 | 102 | response = jwt_auth.login(identifier=str(user.id), response_body=user) 103 | 104 | # you can do whatever you want to update the response instance here 105 | # e.g. response.set_cookie(...) 106 | 107 | return response 108 | 109 | 110 | # We also have some other routes, for example: 111 | @get("/some-path") 112 | def some_route_handler(request: Request[User, Token]) -> Any: 113 | # request.user is set to the instance of user returned by the middleware 114 | assert isinstance(request.user, User) 115 | # request.auth is the instance of 'starlite_jwt.Token' created from the data encoded in the auth header 116 | assert isinstance(request.auth, Token) 117 | # do stuff ... 118 | 119 | 120 | # We add the jwt security schema to the OpenAPI config. 121 | openapi_config = OpenAPIConfig( 122 | title="My API", 123 | version="1.0.0", 124 | components=[jwt_auth.openapi_components], 125 | security=[jwt_auth.security_requirement], 126 | ) 127 | 128 | # We initialize the app instance, passing to it the 'jwt_auth.middleware' and the 'openapi_config'. 129 | app = Starlite( 130 | route_handlers=[login_handler, some_route_handler], 131 | middleware=[jwt_auth.middleware], 132 | openapi_config=openapi_config, 133 | ) 134 | ``` 135 | 136 | ## Customization 137 | 138 | This integrates with the OpenAPI configuration of Starlite, and it uses the `SecurityScheme` configuration to format the header and/or cookie value. 139 | 140 | The default implementation follows the `Bearer {encoded_token}` format, but you may optionally override this configuration by modifying the openapi_component attribute of your `JWTAuth` instance. 141 | 142 | If you wanted your authentication header to be `Token {encoded_token}`, you could use the following as your security scheme configuration: 143 | 144 | ```python 145 | from pydantic_openapi_schema.v3_1_0 import Components, SecurityScheme 146 | from starlite_jwt import JWTAuth 147 | 148 | 149 | class CustomJWTAuth(JWTAuth): 150 | @property 151 | def openapi_components(self) -> Components: 152 | """Creates OpenAPI documentation for the JWT auth schema used. 153 | 154 | Returns: 155 | An [Components][pydantic_schema_pydantic.v3_1_0.components.Components] instance. 156 | """ 157 | return Components( 158 | securitySchemes={ 159 | self.openapi_security_scheme_name: SecurityScheme( 160 | type="http", 161 | scheme="Token", 162 | name=self.auth_header, 163 | bearerFormat="JWT", 164 | description="JWT api-key authentication and authorization.", 165 | ) 166 | } 167 | ) 168 | ``` 169 | 170 | ## Contributing 171 | 172 | Starlite and all its official libraries is open to contributions big and small. 173 | 174 | You can always [join our discord](https://discord.gg/X3FJqy8d2j) server 175 | or [join our Matrix](https://matrix.to/#/#starlitespace:matrix.org) space to discuss contributions and project 176 | maintenance. For guidelines on how to contribute to this library, please see [the contribution guide](CONTRIBUTING.md). 177 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a vulnerability either create an issue on GitHub or tag "@maintainer" on our discord server 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | docs: 4 | build: 5 | dockerfile: .docker/Dockerfile 6 | context: . 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - .:/docs 11 | command: 12 | - "serve" 13 | - "--dev-addr=0.0.0.0:8000" 14 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | :root, 2 | body { 3 | --md-text-font-family: Tahoma, Geneva, Verdana, sans-serif; 4 | } 5 | 6 | .md-content__button { 7 | display: none; 8 | } 9 | .md-typeset .md-content__button + h1 { 10 | margin-top: 0; 11 | } 12 | 13 | .md-typeset h1 { 14 | font-size: calc(1rem * 1.35 * 1.35 * 1.35); 15 | margin: 1.35rem 0 1rem; 16 | font-weight: 400; 17 | } 18 | 19 | /* #starlite-banner .cls-1 { 20 | fill: transparent; 21 | } 22 | #starlite-banner #banner-text path { 23 | fill: var(--md-default-fg-color); 24 | } */ 25 | #starlite-banner { 26 | border-radius: 0.5rem; 27 | overflow: hidden; 28 | } 29 | [data-md-color-scheme="mirage-light"] #starlite-banner .cls-1 { 30 | fill: var(--md-default-fg-color--light); 31 | } 32 | 33 | .md-search__form { 34 | background-color: var(--md-default-bg-color) !important; 35 | } 36 | 37 | .md-typeset h2 { 38 | font-size: calc(1rem * 1.35 * 1.35); 39 | margin: 1.35rem 0 1rem; 40 | font-weight: 400; 41 | padding-top: 1rem; 42 | border-top: 1px solid var(--md-default-bg-color--lightest); 43 | border-color: transparent; 44 | } 45 | 46 | .md-typeset h3 { 47 | font-size: calc(1rem * 1.35); 48 | margin: 1.25rem 0 1rem; 49 | font-weight: 400; 50 | padding-top: 1rem; 51 | border-top: 1px solid var(--md-default-bg-color--lightest); 52 | border-color: transparent; 53 | } 54 | 55 | .highlight span.filename { 56 | margin-top: 1rem; 57 | } 58 | .highlight .filename + pre { 59 | margin-bottom: 2rem; 60 | } 61 | 62 | @media screen and (max-width: 76.1875em) { 63 | .md-nav--primary .md-nav__title[for="__drawer"] { 64 | color: var(--md-accent-fg-color); 65 | background-color: var(--md-primary-bg-color); 66 | } 67 | 68 | .md-nav__source { 69 | background-color: var(--md-accent-fg-color); 70 | } 71 | 72 | .md-nav--primary .md-nav__item { 73 | border-top: none; 74 | } 75 | } 76 | 77 | [data-md-color-scheme="mirage"] { 78 | /* Default color shades */ 79 | --md-default-fg-color: #d6dbe1; 80 | --md-default-fg-color--light: hsl(213, 15%, 76%); 81 | --md-default-fg-color--lighter: hsl(213, 15%, 66%); 82 | --md-default-fg-color--lightest: hsl(213, 15%, 56%); 83 | --md-default-bg-color: #1d2433; 84 | --md-default-bg-color--light: hsl(221, 28%, 26%); 85 | --md-default-bg-color--lighter: hsl(221, 28%, 36%); 86 | --md-default-bg-color--lightest: hsl(221, 28%, 46%); 87 | 88 | /* Primary color shades */ 89 | --md-primary-fg-color: #ffae57; 90 | --md-primary-fg-color--light: #ffd580; 91 | --md-primary-fg-color--dark: hsl(31, 90%, 60%); 92 | --md-primary-bg-color: #2f3b54; 93 | --md-primary-bg-color--light: hsl(221, 28%, 33%); 94 | 95 | /* Accent color shades */ 96 | --md-accent-fg-color: #ffd580; 97 | --md-accent-fg-color--transparent: #ffd58033; 98 | --md-accent-bg-color: #171c28; 99 | --md-accent-bg-color--light: hsl(222, 27%, 20%); 100 | 101 | /* Misc */ 102 | --md-footer-fg-color: var(--md-accent-fg-color); 103 | --md-footer-bg-color: var(--md-accent-bg-color); 104 | /* --md-footer-bg-color--dark: var(--md-default-bg-color); */ 105 | 106 | /* Code color shades */ 107 | --md-code-fg-color: var(--md-default-fg-color); 108 | --md-code-bg-color: var(--md-accent-bg-color--light); 109 | 110 | /* Code highlighting color shades */ 111 | --md-code-hl-color: #bae67e99; 112 | --md-code-hl-number-color: #ffd580; 113 | --md-code-hl-special-color: #ef6b73; 114 | --md-code-hl-function-color: #5ccfe6; 115 | --md-code-hl-constant-color: #c3a6ff; 116 | --md-code-hl-keyword-color: #ef6b73; 117 | --md-code-hl-string-color: #bae67e; 118 | --md-code-hl-name-color: var(--md-code-fg-color); 119 | --md-code-hl-operator-color: var(--md-code-hl-number-color); 120 | --md-code-hl-punctuation-color: var(--md-code-hl-number-color); 121 | --md-code-hl-comment-color: var(--md-default-bg-color--lightest); 122 | --md-code-hl-generic-color: var(--md-code-hl-number-color); 123 | --md-code-hl-variable-color: var(--md-code-hl-number-color); 124 | 125 | /* Typeset color shades */ 126 | --md-typeset-color: var(--md-default-fg-color); 127 | 128 | /* Typeset `a` color shades */ 129 | --md-typeset-a-color: var(--md-primary-fg-color); 130 | 131 | /* Typeset `mark` color shades */ 132 | --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); 133 | 134 | /* Typeset `del` and `ins` color shades */ 135 | --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); 136 | --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); 137 | 138 | /* Typeset `kbd` color shades */ 139 | --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); 140 | --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); 141 | --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); 142 | 143 | /* Typeset `table` color shades */ 144 | --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); 145 | 146 | /* Admonition color shades * 147 | --md-admonition-fg-color: var(--md-default-fg-color); 148 | --md-admonition-bg-color: var(--md-default-bg-color); 149 | 150 | /* Footer color shades * 151 | --md-footer-fg-color: hsla(0, 0%, 100%, 1); 152 | --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); 153 | --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3); 154 | --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); 155 | --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); 156 | 157 | /* Shadow depth 1 * 158 | --md-shadow-z1: 159 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.05), 160 | 0 0 .06125rem hsla(0, 0%, 0%, 0.1); 161 | 162 | /* Shadow depth 2 * 163 | --md-shadow-z2: 164 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.1), 165 | 0 0 .06125rem hsla(0, 0%, 0%, 0.25); 166 | 167 | /* Shadow depth 3 * 168 | --md-shadow-z3: 169 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.2), 170 | 0 0 .06125rem hsla(0, 0%, 0%, 0.35); */ 171 | } 172 | 173 | [data-md-color-scheme="mirage"] .md-header { 174 | color: var(--md-default-fg-color); 175 | background-color: var(--md-primary-bg-color); 176 | } 177 | 178 | [data-md-color-scheme="mirage-light"] { 179 | /* Default color shades */ 180 | --md-default-fg-color: #1d2433; 181 | --md-default-fg-color--light: hsl(221, 28%, 26%); 182 | --md-default-fg-color--lighter: hsl(221, 28%, 36%); 183 | --md-default-fg-color--lightest: hsl(221, 28%, 46%); 184 | --md-default-bg-color: #fff; 185 | --md-default-bg-color--light: var(--md-default-bg-color); 186 | --md-default-bg-color--lighter: var(--md-default-bg-color); 187 | --md-default-bg-color--lightest: var(--md-default-bg-color); 188 | 189 | /* Primary color shades */ 190 | --md-primary-fg-color: #ffae57; 191 | --md-primary-fg-color--light: #ffd580; 192 | --md-primary-fg-color--dark: hsl(31, 90%, 60%); 193 | --md-primary-bg-color: #2f3b54; 194 | --md-primary-bg-color--light: hsl(221, 28%, 33%); 195 | 196 | /* Accent color shades */ 197 | --md-accent-bg-color: #171c28; 198 | --md-accent-bg-color--light: hsl(222, 27%, 20%); 199 | --md-accent-fg-color: #ffd580; 200 | --md-accent-fg-color--transparent: #ffd58033; 201 | 202 | /* Misc */ 203 | --md-footer-fg-color: var(--md-accent-fg-color); 204 | --md-footer-bg-color: var(--md-primary-bg-color); 205 | /* --md-footer-bg-color--dark: var(--md-default-bg-color); */ 206 | --md-typeset-a-color: var(--md-primary-fg-color--dark); 207 | 208 | /* Code color shades */ 209 | --md-code-fg-color: var(--md-default-bg-color); 210 | --md-code-bg-color: var(--md-primary-bg-color); 211 | 212 | /* Code highlighting color shades */ 213 | --md-code-hl-color: #bae67e99; 214 | --md-code-hl-number-color: #ffd580; 215 | --md-code-hl-special-color: #ef6b73; 216 | --md-code-hl-function-color: #5ccfe6; 217 | --md-code-hl-constant-color: #c3a6ff; 218 | --md-code-hl-keyword-color: #ef6b73; 219 | --md-code-hl-string-color: #bae67e; 220 | --md-code-hl-name-color: var(--md-default-bg-color); 221 | --md-code-hl-operator-color: var(--md-code-hl-number-color); 222 | --md-code-hl-punctuation-color: var(--md-code-hl-number-color); 223 | --md-code-hl-comment-color: var(--md-default-fg-color--lightest); 224 | --md-code-hl-generic-color: var(--md-code-hl-number-color); 225 | --md-code-hl-variable-color: var(--md-code-hl-number-color); 226 | 227 | /* Typeset color shades */ 228 | --md-typeset-color: var(--md-default-fg-color); 229 | 230 | /* Typeset `a` color shades */ 231 | --md-typeset-a-color: var(--md-primary-fg-color); 232 | 233 | /* Typeset `mark` color shades */ 234 | --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); 235 | 236 | /* Typeset `del` and `ins` color shades */ 237 | --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); 238 | --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); 239 | 240 | /* Typeset `kbd` color shades */ 241 | --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); 242 | --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); 243 | --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); 244 | 245 | /* Typeset `table` color shades */ 246 | --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); 247 | 248 | /* Admonition color shades * 249 | --md-admonition-fg-color: var(--md-default-fg-color); 250 | --md-admonition-bg-color: var(--md-default-bg-color); 251 | 252 | /* Footer color shades * 253 | --md-footer-fg-color: hsla(0, 0%, 100%, 1); 254 | --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); 255 | --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3); 256 | --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); 257 | --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); 258 | 259 | /* Shadow depth 1 * 260 | --md-shadow-z1: 261 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.05), 262 | 0 0 .06125rem hsla(0, 0%, 0%, 0.1); 263 | 264 | /* Shadow depth 2 * 265 | --md-shadow-z2: 266 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.1), 267 | 0 0 .06125rem hsla(0, 0%, 0%, 0.25); 268 | 269 | /* Shadow depth 3 * 270 | --md-shadow-z3: 271 | 0 .25rem .66rem hsla(0, 0%, 0%, 0.2), 272 | 0 0 .06125rem hsla(0, 0%, 0%, 0.35); */ 273 | } 274 | [data-md-color-scheme="mirage-light"] .md-typeset a:focus, 275 | .md-typeset a:hover { 276 | color: var(--md-primary-fg-color); 277 | } 278 | 279 | [data-md-color-scheme="mirage-light"] .md-header { 280 | color: var(--md-default-bg-color); 281 | background-color: var(--md-primary-bg-color); 282 | } 283 | 284 | [data-md-color-scheme="mirage-light"] .highlight span.filename { 285 | color: var(--md-default-bg-color); 286 | } 287 | -------------------------------------------------------------------------------- /docs/gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | """Generate the code reference pages.""" 2 | 3 | from pathlib import Path 4 | 5 | import mkdocs_gen_files 6 | 7 | nav = mkdocs_gen_files.Nav() # type: ignore[attr-defined] 8 | 9 | for path in sorted(Path("starlite_jwt").rglob("*.py")): # 10 | module_path = Path("starlite_jwt").with_suffix("") 11 | doc_path = Path("starlite_jwt").with_suffix(".md") 12 | full_doc_path = Path("reference", doc_path) 13 | 14 | parts = module_path.parts 15 | 16 | if parts[-1] == "__main__": 17 | continue 18 | if parts[-1] == "__init__": 19 | parts = parts[:-1] 20 | doc_path = doc_path.with_name("index.md") 21 | full_doc_path = full_doc_path.with_name("index.md") 22 | 23 | nav[parts] = doc_path.as_posix() 24 | 25 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 26 | identifier = ".".join(parts) 27 | fd.write(f"::: {identifier}") 28 | 29 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 30 | 31 | with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: 32 | nav_file.writelines(nav.build_literate_nav()) 33 | -------------------------------------------------------------------------------- /docs/images/starlite-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/starlite-favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/starlite-jwt/c00fafc6ef3f82cddecfbc1cd66a0e5fd618466f/docs/images/starlite-favicon.ico -------------------------------------------------------------------------------- /docs/images/starlite-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/starlite-jwt/c00fafc6ef3f82cddecfbc1cd66a0e5fd618466f/docs/images/starlite-icon@2x.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Starlite JWT 2 | 3 | 4 | Starlite logo 5 | 6 | 7 |
8 | 9 | ![PyPI - License](https://img.shields.io/pypi/l/starlite-jwt?color=blue) 10 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/starlite-jwt) 11 | 12 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 13 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=coverage)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 14 | 15 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 16 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 17 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 18 | [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=starlite-api_jwt-auth&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth) 19 | 20 | [![Discord](https://img.shields.io/discord/919193495116337154?color=blue&label=chat%20on%20discord&logo=discord)](https://discord.gg/X3FJqy8d2j) 21 | [![Matrix](https://img.shields.io/badge/%5Bm%5D%20chat%20on%20Matrix-bridged-blue)](https://matrix.to/#/#starlitespace:matrix.org) 22 | 23 |
24 | 25 | This library offers simple JWT authentication for [Starlite](https://github.com/starlite-api/starlite). 26 | 27 | ## Installation 28 | 29 | ```shell 30 | pip install starlite-jwt 31 | ``` 32 | 33 | This library uses the excellent [python-jose](https://github.com/mpdavis/python-jose) library, which supports multiple 34 | cryptographic backends. You can install either [pyca/cryptography](http://cryptography.io/) 35 | or [pycryptodome](https://pycryptodome.readthedocs.io/en/latest/), and it will be used as the backend automatically. 36 | Note 37 | that if you want to use a certificate based encryption scheme, you must install one of these backends - please refer to 38 | the [python-jose](https://github.com/mpdavis/python-jose) readme for more details. 39 | 40 | ## Example 41 | 42 | ```python 43 | import os 44 | from typing import Any, Optional 45 | from uuid import UUID, uuid4 46 | 47 | from pydantic import BaseModel, EmailStr 48 | from starlite import OpenAPIConfig, Request, Response, Starlite, ASGIConnection, get 49 | 50 | from starlite_jwt import JWTAuth, Token 51 | 52 | 53 | # Let's assume we have a User model that is a pydantic model. 54 | # This though is not required - we need some sort of user class - 55 | # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. 56 | class User(BaseModel): 57 | id: UUID 58 | name: str 59 | email: EmailStr 60 | 61 | 62 | # The JWTAuth package requires a handler callable that takes a unique identifier, and returns the 'User' 63 | # instance correlating to it. 64 | # 65 | # The identifier is the 'sub' key of the JWT, and it usually correlates to a user ID. 66 | # It can be though any arbitrary value you decide upon - as long as the handler function provided 67 | # can receive this value and return the model instance for it. 68 | # 69 | # Note: The callable can be either sync or async - both will work. 70 | async def retrieve_user_handler( 71 | unique_identifier: str, connection: ASGIConnection[Any, Any, Any] 72 | ) -> Optional[User]: 73 | # logic here to retrieve the user instance 74 | ... 75 | 76 | 77 | # The minimal configuration required for the library is the callable for the 'retrieve_user_handler' key, and a string 78 | # value for the token secret. 79 | # 80 | # Important: secrets should never be hardcoded. Its best practice to pass the secret using ENV. 81 | # 82 | # Tip: It's also a good idea to use the pydantic settings management functionality 83 | jwt_auth = JWTAuth( 84 | retrieve_user_handler=retrieve_user_handler, 85 | token_secret=os.environ.get("JWT_SECRET", "abcd123"), 86 | # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint 87 | # and our openAPI docs. 88 | exclude=["/login", "/schema"], 89 | ) 90 | 91 | 92 | # Given an instance of 'JWTAuth' we can create a login handler function: 93 | @get("/login") 94 | def login_handler() -> Response[User]: 95 | # we have a user instance - probably by retrieving it from persistence using another lib. 96 | # what's important for our purposes is to have an identifier: 97 | user = User(name="Moishe Zuchmir", email="zuchmir@moishe.com", id=uuid4()) 98 | 99 | response = jwt_auth.login(identifier=str(user.id), response_body=user) 100 | 101 | # you can do whatever you want to update the response instance here 102 | # e.g. response.set_cookie(...) 103 | 104 | return response 105 | 106 | 107 | # We also have some other routes, for example: 108 | @get("/some-path") 109 | def some_route_handler(request: Request[User, Token]) -> Any: 110 | # request.user is set to the instance of user returned by the middleware 111 | assert isinstance(request.user, User) 112 | # request.auth is the instance of 'starlite_jwt.Token' created from the data encoded in the auth header 113 | assert isinstance(request.auth, Token) 114 | # do stuff ... 115 | 116 | 117 | # We add the jwt security schema to the OpenAPI config. 118 | openapi_config = OpenAPIConfig( 119 | title="My API", 120 | version="1.0.0", 121 | components=[jwt_auth.openapi_components], 122 | security=[jwt_auth.security_requirement], 123 | ) 124 | 125 | # We initialize the app instance, passing to it the 'jwt_auth.middleware' and the 'openapi_config'. 126 | app = Starlite( 127 | route_handlers=[login_handler, some_route_handler], 128 | middleware=[jwt_auth.middleware], 129 | openapi_config=openapi_config, 130 | ) 131 | ``` 132 | 133 | ## JWT Cookie authentication 134 | 135 | If you'd like to additionally enable JWT auth using HTTP only cookies, you can configure the built in middleware. 136 | 137 | ```python 138 | import os 139 | 140 | from typing import Optional, Any 141 | from uuid import UUID 142 | 143 | from pydantic import BaseModel, EmailStr 144 | from starlite import ASGIConnection 145 | from starlite_jwt import JWTCookieAuth 146 | 147 | 148 | # Let's assume we have a User model that is a pydantic model. 149 | # This though is not required - we need some sort of user class - 150 | # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. 151 | class User(BaseModel): 152 | id: UUID 153 | name: str 154 | email: EmailStr 155 | 156 | 157 | # The JWTAuth package requires a handler callable that takes a unique identifier, and returns the 'User' 158 | # instance correlating to it. 159 | # 160 | # The identifier is the 'sub' key of the JWT, and it usually correlates to a user ID. 161 | # It can be though any arbitrary value you decide upon - as long as the handler function provided 162 | # can receive this value and return the model instance for it. 163 | # 164 | # Note: The callable can be either sync or async - both will work. 165 | async def retrieve_user_handler( 166 | unique_identifier: str, connection: ASGIConnection[Any, Any, Any] 167 | ) -> Optional[User]: 168 | # logic here to retrieve the user instance 169 | ... 170 | 171 | 172 | # The minimal configuration required for the JWT cookie configuration is the callable for the 'retrieve_user_handler' key, and a string 173 | # value for the token secret. 174 | # 175 | # Important: secrets should never be hardcoded. Its best practice to pass the secret using ENV. 176 | # 177 | # Tip: It's also a good idea to use the pydantic settings management functionality 178 | jwt_auth = JWTCookieAuth( 179 | retrieve_user_handler=retrieve_user_handler, 180 | token_secret=os.environ.get("JWT_SECRET", "abcd123"), 181 | # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint 182 | # and our openAPI docs. 183 | exclude=["/login", "/schema"], 184 | # Tip: We can optionally supply cookie options to the configuration. Here is an example of enabling the secure cookie option 185 | # auth_cookie_options=CookieOptions(secure=True), 186 | ) 187 | ``` 188 | 189 | ## OAUTH2 Password Bearer Flow 190 | 191 | It is also possible to configure an OAUTH2 Password Bearer login flow with the included `OAuth2PasswordBearerAuth` class. 192 | 193 | ```python 194 | import os 195 | 196 | from typing import Optional, Any 197 | from uuid import UUID, uuid4 198 | 199 | from pydantic import BaseModel, EmailStr 200 | from starlite import ( 201 | ASGIConnection, 202 | Body, 203 | NotAuthorizedException, 204 | RequestEncodingType, 205 | Response, 206 | get, 207 | ) 208 | from starlite_jwt import OAuth2PasswordBearerAuth 209 | 210 | 211 | # Let's assume we have a User model that is a pydantic model. 212 | # This though is not required - we need some sort of user class - 213 | # but it can be any arbitrary value, e.g. an SQLAlchemy model, a representation of a MongoDB etc. 214 | class User(BaseModel): 215 | id: UUID 216 | name: str 217 | email: EmailStr 218 | password: str 219 | 220 | 221 | # The JWTAuth package requires a handler callable that takes a unique identifier, and returns the 'User' 222 | # instance correlating to it. 223 | # 224 | # The identifier is the 'sub' key of the JWT, and it usually correlates to a user ID. 225 | # It can be though any arbitrary value you decide upon - as long as the handler function provided 226 | # can receive this value and return the model instance for it. 227 | # 228 | # Note: The callable can be either sync or async - both will work. 229 | async def retrieve_user_handler( 230 | unique_identifier: str, connection: ASGIConnection[Any, Any, Any] 231 | ) -> Optional[User]: 232 | # logic here to retrieve the user instance 233 | ... 234 | 235 | 236 | # The minimal configuration required for the JWT cookie configuration is the callable for the 'retrieve_user_handler' key, a string 237 | # value for the token secret, and a `token_url` for retrieving an access token. 238 | # 239 | # Important: secrets should never be hardcoded. Its best practice to pass the secret using ENV. 240 | # 241 | # Tip: It's also a good idea to use the pydantic settings management functionality 242 | oauth2_auth = OAuth2PasswordBearerAuth( 243 | retrieve_user_handler=retrieve_user_handler, 244 | token_secret=os.environ.get("JWT_SECRET", "abcd123"), 245 | # we are specifying the URL for retrieving a JWT access token 246 | token_url="/login", 247 | # we are specifying which endpoints should be excluded from authentication. In this case the login endpoint 248 | # and our openAPI docs. 249 | exclude=["/login", "/schema"], 250 | ) 251 | 252 | 253 | # A basic login form could look like this: 254 | class UserLogin(BaseModel): 255 | """minimum properties required to log in""" 256 | 257 | username: str 258 | password: str 259 | 260 | 261 | async def authenticate(username: str, password: str) -> User: 262 | """Authenticates a user 263 | 264 | Args: 265 | username: User email 266 | password: password 267 | Returns: 268 | User object 269 | """ 270 | user = User( 271 | name="Moishe Zuchmir", 272 | email="zuchmir@moishe.com", 273 | id=uuid4(), 274 | password="insecure", 275 | ) 276 | if username == user.email and password == user.password: 277 | return user 278 | raise NotAuthorizedException 279 | 280 | 281 | @get("/login") 282 | def login_handler( 283 | data: UserLogin = Body(media_type=RequestEncodingType.URL_ENCODED), 284 | ) -> Response[User]: 285 | # we have a user instance - probably by retrieving it from persistence using another lib. 286 | # what's important for our purposes is to have an identifier: 287 | user = await authenticate(data.username, data.password) 288 | 289 | response = oauth2_auth.login(identifier=str(user.id), response_body=user) 290 | 291 | # you can do whatever you want to update the response instance here 292 | # e.g. response.set_cookie(...) 293 | 294 | return response 295 | ``` 296 | 297 | ## Customization 298 | 299 | This integrates with the OpenAPI configuration of Starlite, and it uses the `SecurityScheme` configuration to format the header and/or cookie value. 300 | 301 | The default implementation follows the `Bearer {encoded_token}` format, but you may optionally override this configuration by modifying the openapi_component attribute of your `JWTAuth` instance. 302 | 303 | If you wanted your authentication header to be `Token {encoded_token}`, you could use the following to customize the JWT implementation: 304 | 305 | ```python 306 | from pydantic_openapi_schema.v3_1_0 import Components, SecurityScheme 307 | from starlite_jwt import JWTAuth 308 | 309 | 310 | class CustomJWTAuth(JWTAuth): 311 | @property 312 | def openapi_components(self) -> Components: 313 | """Creates OpenAPI documentation for the JWT auth schema used. 314 | 315 | Returns: 316 | An [Components][pydantic_schema_pydantic.v3_1_0.components.Components] instance. 317 | """ 318 | return Components( 319 | securitySchemes={ 320 | self.openapi_security_scheme_name: SecurityScheme( 321 | type="http", 322 | scheme="Token", 323 | name=self.auth_header, 324 | bearerFormat="JWT", 325 | description="JWT api-key authentication and authorization.", 326 | ) 327 | } 328 | ) 329 | ``` 330 | 331 | ## Contributing 332 | 333 | Starlite and all its official libraries is open to contributions big and small. 334 | 335 | You can always [join our discord](https://discord.gg/X3FJqy8d2j) server 336 | or [join our Matrix](https://matrix.to/#/#starlitespace:matrix.org) space to discuss contributions and project 337 | maintenance. For guidelines on how to contribute to this library, please see the contribution guide in the repository's 338 | root. 339 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: starlite-jwt 2 | repo_url: https://github.com/starlite-api/starlite-jwt 3 | repo_name: starlite-api/starlite-jwt 4 | site_url: https://starlite-api.github.io/starlite-jwt 5 | nav: 6 | - index.md 7 | - Reference: reference/ 8 | watch: 9 | - starlite_jwt 10 | theme: 11 | name: material 12 | palette: 13 | - media: "(prefers-color-scheme: dark)" 14 | scheme: mirage 15 | toggle: 16 | icon: material/toggle-switch 17 | name: Switch to light mode 18 | - media: "(prefers-color-scheme: light)" 19 | scheme: mirage-light 20 | toggle: 21 | icon: material/toggle-switch-off-outline 22 | name: Switch to dark mode 23 | favicon: images/starlite-favicon.ico 24 | logo: images/starlite-icon@2x.png 25 | icon: 26 | repo: fontawesome/brands/github 27 | features: 28 | - navigation.instant 29 | - navigation.tracking 30 | - navigation.tabs 31 | - navigation.top 32 | - navigation.expand 33 | - toc.integrate 34 | - search.suggest 35 | - search.highlight 36 | - search.share 37 | markdown_extensions: 38 | - admonition 39 | - meta 40 | - toc: 41 | permalink: true 42 | - pymdownx.details 43 | - pymdownx.snippets 44 | - pymdownx.superfences 45 | extra: 46 | social: 47 | - icon: fontawesome/brands/discord 48 | link: https://discord.gg/X3FJqy8d2j 49 | - icon: fontawesome/brands/github 50 | link: https://github.com/starlite-api/starlite-jwt 51 | extra_css: 52 | - css/extra.css 53 | plugins: 54 | - search: 55 | lang: en 56 | - gen-files: 57 | scripts: 58 | - docs/gen_ref_pages.py 59 | - literate-nav: 60 | nav_file: SUMMARY.md 61 | - section-index 62 | - mkdocstrings: 63 | handlers: 64 | python: 65 | paths: [src] 66 | import: 67 | - https://docs.python.org/3/objects.inv 68 | - https://starlite-api.github.io/starlite/objects.inv 69 | - https://starlite-api.github.io/pydantic-openapi-schema/objects.inv 70 | options: 71 | docstring_style: google 72 | docstring_options: 73 | ignore_init_summary: yes 74 | line_length: 100 75 | merge_init_into_class: yes 76 | separate_signature: yes 77 | show_source: no 78 | show_root_heading: no 79 | show_root_full_path: no 80 | show_root_toc_entry: no 81 | - autorefs 82 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins = pydantic.mypy 3 | 4 | warn_unused_ignores = True 5 | warn_redundant_casts = True 6 | warn_unused_configs = True 7 | warn_unreachable = True 8 | warn_return_any = True 9 | strict = True 10 | disallow_untyped_decorators = False 11 | implicit_reexport = False 12 | show_error_codes = True 13 | 14 | [pydantic-mypy] 15 | init_forbid_extra = True 16 | init_typed = True 17 | warn_required_dynamic_aliases = True 18 | warn_untyped_fields = True 19 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "anyio" 3 | version = "3.6.2" 4 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | idna = ">=2.8" 11 | sniffio = ">=1.1" 12 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 13 | 14 | [package.extras] 15 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 16 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 17 | trio = ["trio (>=0.16,<0.22)"] 18 | 19 | [[package]] 20 | name = "attrs" 21 | version = "22.1.0" 22 | description = "Classes Without Boilerplate" 23 | category = "dev" 24 | optional = false 25 | python-versions = ">=3.5" 26 | 27 | [package.extras] 28 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] 29 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 30 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] 31 | tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] 32 | 33 | [[package]] 34 | name = "certifi" 35 | version = "2022.9.24" 36 | description = "Python package for providing Mozilla's CA Bundle." 37 | category = "main" 38 | optional = false 39 | python-versions = ">=3.6" 40 | 41 | [[package]] 42 | name = "cffi" 43 | version = "1.15.1" 44 | description = "Foreign Function Interface for Python calling C code." 45 | category = "main" 46 | optional = false 47 | python-versions = "*" 48 | 49 | [package.dependencies] 50 | pycparser = "*" 51 | 52 | [[package]] 53 | name = "cfgv" 54 | version = "3.3.1" 55 | description = "Validate configuration and produce human readable error messages." 56 | category = "dev" 57 | optional = false 58 | python-versions = ">=3.6.1" 59 | 60 | [[package]] 61 | name = "click" 62 | version = "8.1.3" 63 | description = "Composable command line interface toolkit" 64 | category = "dev" 65 | optional = false 66 | python-versions = ">=3.7" 67 | 68 | [package.dependencies] 69 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 70 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 71 | 72 | [[package]] 73 | name = "colorama" 74 | version = "0.4.6" 75 | description = "Cross-platform colored terminal text." 76 | category = "dev" 77 | optional = false 78 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 79 | 80 | [[package]] 81 | name = "coverage" 82 | version = "6.5.0" 83 | description = "Code coverage measurement for Python" 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=3.7" 87 | 88 | [package.dependencies] 89 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 90 | 91 | [package.extras] 92 | toml = ["tomli"] 93 | 94 | [[package]] 95 | name = "cryptography" 96 | version = "38.0.3" 97 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 98 | category = "main" 99 | optional = false 100 | python-versions = ">=3.6" 101 | 102 | [package.dependencies] 103 | cffi = ">=1.12" 104 | 105 | [package.extras] 106 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 107 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 108 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 109 | sdist = ["setuptools-rust (>=0.11.4)"] 110 | ssh = ["bcrypt (>=3.1.5)"] 111 | test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] 112 | 113 | [[package]] 114 | name = "distlib" 115 | version = "0.3.6" 116 | description = "Distribution utilities" 117 | category = "dev" 118 | optional = false 119 | python-versions = "*" 120 | 121 | [[package]] 122 | name = "dnspython" 123 | version = "2.2.1" 124 | description = "DNS toolkit" 125 | category = "main" 126 | optional = false 127 | python-versions = ">=3.6,<4.0" 128 | 129 | [package.extras] 130 | curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] 131 | dnssec = ["cryptography (>=2.6,<37.0)"] 132 | doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] 133 | idna = ["idna (>=2.1,<4.0)"] 134 | trio = ["trio (>=0.14,<0.20)"] 135 | wmi = ["wmi (>=1.5.1,<2.0.0)"] 136 | 137 | [[package]] 138 | name = "ecdsa" 139 | version = "0.18.0" 140 | description = "ECDSA cryptographic signature library (pure python)" 141 | category = "main" 142 | optional = false 143 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 144 | 145 | [package.dependencies] 146 | six = ">=1.9.0" 147 | 148 | [package.extras] 149 | gmpy = ["gmpy"] 150 | gmpy2 = ["gmpy2"] 151 | 152 | [[package]] 153 | name = "email-validator" 154 | version = "1.3.0" 155 | description = "A robust email address syntax and deliverability validation library." 156 | category = "main" 157 | optional = false 158 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 159 | 160 | [package.dependencies] 161 | dnspython = ">=1.15.0" 162 | idna = ">=2.0.0" 163 | 164 | [[package]] 165 | name = "exceptiongroup" 166 | version = "1.0.1" 167 | description = "Backport of PEP 654 (exception groups)" 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=3.7" 171 | 172 | [package.extras] 173 | test = ["pytest (>=6)"] 174 | 175 | [[package]] 176 | name = "faker" 177 | version = "15.3.1" 178 | description = "Faker is a Python package that generates fake data for you." 179 | category = "main" 180 | optional = false 181 | python-versions = ">=3.7" 182 | 183 | [package.dependencies] 184 | python-dateutil = ">=2.4" 185 | typing-extensions = {version = ">=3.10.0.1", markers = "python_version < \"3.8\""} 186 | 187 | [[package]] 188 | name = "filelock" 189 | version = "3.8.0" 190 | description = "A platform independent file lock." 191 | category = "dev" 192 | optional = false 193 | python-versions = ">=3.7" 194 | 195 | [package.extras] 196 | docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] 197 | testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] 198 | 199 | [[package]] 200 | name = "ghp-import" 201 | version = "2.1.0" 202 | description = "Copy your docs directly to the gh-pages branch." 203 | category = "dev" 204 | optional = false 205 | python-versions = "*" 206 | 207 | [package.dependencies] 208 | python-dateutil = ">=2.8.1" 209 | 210 | [package.extras] 211 | dev = ["flake8", "markdown", "twine", "wheel"] 212 | 213 | [[package]] 214 | name = "h11" 215 | version = "0.12.0" 216 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 217 | category = "main" 218 | optional = false 219 | python-versions = ">=3.6" 220 | 221 | [[package]] 222 | name = "httpcore" 223 | version = "0.15.0" 224 | description = "A minimal low-level HTTP client." 225 | category = "main" 226 | optional = false 227 | python-versions = ">=3.7" 228 | 229 | [package.dependencies] 230 | anyio = ">=3.0.0,<4.0.0" 231 | certifi = "*" 232 | h11 = ">=0.11,<0.13" 233 | sniffio = ">=1.0.0,<2.0.0" 234 | 235 | [package.extras] 236 | http2 = ["h2 (>=3,<5)"] 237 | socks = ["socksio (>=1.0.0,<2.0.0)"] 238 | 239 | [[package]] 240 | name = "httpx" 241 | version = "0.23.0" 242 | description = "The next generation HTTP client." 243 | category = "main" 244 | optional = false 245 | python-versions = ">=3.7" 246 | 247 | [package.dependencies] 248 | certifi = "*" 249 | httpcore = ">=0.15.0,<0.16.0" 250 | rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} 251 | sniffio = "*" 252 | 253 | [package.extras] 254 | brotli = ["brotli", "brotlicffi"] 255 | cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] 256 | http2 = ["h2 (>=3,<5)"] 257 | socks = ["socksio (>=1.0.0,<2.0.0)"] 258 | 259 | [[package]] 260 | name = "hypothesis" 261 | version = "6.56.4" 262 | description = "A library for property-based testing" 263 | category = "dev" 264 | optional = false 265 | python-versions = ">=3.7" 266 | 267 | [package.dependencies] 268 | attrs = ">=19.2.0" 269 | exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 270 | sortedcontainers = ">=2.1.0,<3.0.0" 271 | 272 | [package.extras] 273 | all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.5)"] 274 | cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] 275 | codemods = ["libcst (>=0.3.16)"] 276 | dateutil = ["python-dateutil (>=1.4)"] 277 | django = ["django (>=3.2)"] 278 | dpcontracts = ["dpcontracts (>=0.4)"] 279 | ghostwriter = ["black (>=19.10b0)"] 280 | lark = ["lark-parser (>=0.6.5)"] 281 | numpy = ["numpy (>=1.9.0)"] 282 | pandas = ["pandas (>=1.0)"] 283 | pytest = ["pytest (>=4.6)"] 284 | pytz = ["pytz (>=2014.1)"] 285 | redis = ["redis (>=3.0.0)"] 286 | zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.5)"] 287 | 288 | [[package]] 289 | name = "identify" 290 | version = "2.5.8" 291 | description = "File identification library for Python" 292 | category = "dev" 293 | optional = false 294 | python-versions = ">=3.7" 295 | 296 | [package.extras] 297 | license = ["ukkonen"] 298 | 299 | [[package]] 300 | name = "idna" 301 | version = "3.4" 302 | description = "Internationalized Domain Names in Applications (IDNA)" 303 | category = "main" 304 | optional = false 305 | python-versions = ">=3.5" 306 | 307 | [[package]] 308 | name = "importlib-metadata" 309 | version = "5.0.0" 310 | description = "Read metadata from Python packages" 311 | category = "dev" 312 | optional = false 313 | python-versions = ">=3.7" 314 | 315 | [package.dependencies] 316 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 317 | zipp = ">=0.5" 318 | 319 | [package.extras] 320 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 321 | perf = ["ipython"] 322 | testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] 323 | 324 | [[package]] 325 | name = "iniconfig" 326 | version = "1.1.1" 327 | description = "iniconfig: brain-dead simple config-ini parsing" 328 | category = "dev" 329 | optional = false 330 | python-versions = "*" 331 | 332 | [[package]] 333 | name = "jinja2" 334 | version = "3.1.2" 335 | description = "A very fast and expressive template engine." 336 | category = "dev" 337 | optional = false 338 | python-versions = ">=3.7" 339 | 340 | [package.dependencies] 341 | MarkupSafe = ">=2.0" 342 | 343 | [package.extras] 344 | i18n = ["Babel (>=2.7)"] 345 | 346 | [[package]] 347 | name = "markdown" 348 | version = "3.3.7" 349 | description = "Python implementation of Markdown." 350 | category = "dev" 351 | optional = false 352 | python-versions = ">=3.6" 353 | 354 | [package.dependencies] 355 | importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} 356 | 357 | [package.extras] 358 | testing = ["coverage", "pyyaml"] 359 | 360 | [[package]] 361 | name = "markupsafe" 362 | version = "2.1.1" 363 | description = "Safely add untrusted strings to HTML/XML markup." 364 | category = "dev" 365 | optional = false 366 | python-versions = ">=3.7" 367 | 368 | [[package]] 369 | name = "mergedeep" 370 | version = "1.3.4" 371 | description = "A deep merge function for 🐍." 372 | category = "dev" 373 | optional = false 374 | python-versions = ">=3.6" 375 | 376 | [[package]] 377 | name = "mkdocs" 378 | version = "1.4.2" 379 | description = "Project documentation with Markdown." 380 | category = "dev" 381 | optional = false 382 | python-versions = ">=3.7" 383 | 384 | [package.dependencies] 385 | click = ">=7.0" 386 | colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} 387 | ghp-import = ">=1.0" 388 | importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} 389 | jinja2 = ">=2.11.1" 390 | markdown = ">=3.2.1,<3.4" 391 | mergedeep = ">=1.3.4" 392 | packaging = ">=20.5" 393 | pyyaml = ">=5.1" 394 | pyyaml-env-tag = ">=0.1" 395 | typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} 396 | watchdog = ">=2.0" 397 | 398 | [package.extras] 399 | i18n = ["babel (>=2.9.0)"] 400 | min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] 401 | 402 | [[package]] 403 | name = "mkdocs-gen-files" 404 | version = "0.4.0" 405 | description = "MkDocs plugin to programmatically generate documentation pages during the build" 406 | category = "dev" 407 | optional = false 408 | python-versions = ">=3.7,<4.0" 409 | 410 | [package.dependencies] 411 | mkdocs = ">=1.0.3,<2.0.0" 412 | 413 | [[package]] 414 | name = "multidict" 415 | version = "6.0.2" 416 | description = "multidict implementation" 417 | category = "main" 418 | optional = false 419 | python-versions = ">=3.7" 420 | 421 | [[package]] 422 | name = "nodeenv" 423 | version = "1.7.0" 424 | description = "Node.js virtual environment builder" 425 | category = "dev" 426 | optional = false 427 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 428 | 429 | [package.dependencies] 430 | setuptools = "*" 431 | 432 | [[package]] 433 | name = "orjson" 434 | version = "3.8.1" 435 | description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" 436 | category = "main" 437 | optional = false 438 | python-versions = ">=3.7" 439 | 440 | [[package]] 441 | name = "packaging" 442 | version = "21.3" 443 | description = "Core utilities for Python packages" 444 | category = "dev" 445 | optional = false 446 | python-versions = ">=3.6" 447 | 448 | [package.dependencies] 449 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 450 | 451 | [[package]] 452 | name = "platformdirs" 453 | version = "2.5.3" 454 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 455 | category = "dev" 456 | optional = false 457 | python-versions = ">=3.7" 458 | 459 | [package.extras] 460 | docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] 461 | test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] 462 | 463 | [[package]] 464 | name = "pluggy" 465 | version = "1.0.0" 466 | description = "plugin and hook calling mechanisms for python" 467 | category = "dev" 468 | optional = false 469 | python-versions = ">=3.6" 470 | 471 | [package.dependencies] 472 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 473 | 474 | [package.extras] 475 | dev = ["pre-commit", "tox"] 476 | testing = ["pytest", "pytest-benchmark"] 477 | 478 | [[package]] 479 | name = "pre-commit" 480 | version = "2.20.0" 481 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 482 | category = "dev" 483 | optional = false 484 | python-versions = ">=3.7" 485 | 486 | [package.dependencies] 487 | cfgv = ">=2.0.0" 488 | identify = ">=1.0.0" 489 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 490 | nodeenv = ">=0.11.1" 491 | pyyaml = ">=5.1" 492 | toml = "*" 493 | virtualenv = ">=20.0.8" 494 | 495 | [[package]] 496 | name = "pyasn1" 497 | version = "0.4.8" 498 | description = "ASN.1 types and codecs" 499 | category = "main" 500 | optional = false 501 | python-versions = "*" 502 | 503 | [[package]] 504 | name = "pycparser" 505 | version = "2.21" 506 | description = "C parser in Python" 507 | category = "main" 508 | optional = false 509 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 510 | 511 | [[package]] 512 | name = "pydantic" 513 | version = "1.10.2" 514 | description = "Data validation and settings management using python type hints" 515 | category = "main" 516 | optional = false 517 | python-versions = ">=3.7" 518 | 519 | [package.dependencies] 520 | typing-extensions = ">=4.1.0" 521 | 522 | [package.extras] 523 | dotenv = ["python-dotenv (>=0.10.4)"] 524 | email = ["email-validator (>=1.0.3)"] 525 | 526 | [[package]] 527 | name = "pydantic-factories" 528 | version = "1.15.0" 529 | description = "Mock data generation for pydantic based models and python dataclasses" 530 | category = "main" 531 | optional = false 532 | python-versions = ">=3.7,<4.0" 533 | 534 | [package.dependencies] 535 | faker = "*" 536 | pydantic = ">=1.10.0" 537 | typing-extensions = "*" 538 | 539 | [[package]] 540 | name = "pydantic-openapi-schema" 541 | version = "1.3.0" 542 | description = "OpenAPI Schema using pydantic. Forked for Starlite-API from 'openapi-schema-pydantic'." 543 | category = "main" 544 | optional = false 545 | python-versions = ">=3.7" 546 | 547 | [package.dependencies] 548 | email-validator = "*" 549 | pydantic = ">=1.10.0" 550 | 551 | [[package]] 552 | name = "pyparsing" 553 | version = "3.0.9" 554 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 555 | category = "dev" 556 | optional = false 557 | python-versions = ">=3.6.8" 558 | 559 | [package.extras] 560 | diagrams = ["jinja2", "railroad-diagrams"] 561 | 562 | [[package]] 563 | name = "pytest" 564 | version = "7.2.0" 565 | description = "pytest: simple powerful testing with Python" 566 | category = "dev" 567 | optional = false 568 | python-versions = ">=3.7" 569 | 570 | [package.dependencies] 571 | attrs = ">=19.2.0" 572 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 573 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 574 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 575 | iniconfig = "*" 576 | packaging = "*" 577 | pluggy = ">=0.12,<2.0" 578 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 579 | 580 | [package.extras] 581 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 582 | 583 | [[package]] 584 | name = "pytest-asyncio" 585 | version = "0.20.2" 586 | description = "Pytest support for asyncio" 587 | category = "dev" 588 | optional = false 589 | python-versions = ">=3.7" 590 | 591 | [package.dependencies] 592 | pytest = ">=6.1.0" 593 | typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} 594 | 595 | [package.extras] 596 | testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] 597 | 598 | [[package]] 599 | name = "pytest-cov" 600 | version = "4.0.0" 601 | description = "Pytest plugin for measuring coverage." 602 | category = "dev" 603 | optional = false 604 | python-versions = ">=3.6" 605 | 606 | [package.dependencies] 607 | coverage = {version = ">=5.2.1", extras = ["toml"]} 608 | pytest = ">=4.6" 609 | 610 | [package.extras] 611 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 612 | 613 | [[package]] 614 | name = "python-dateutil" 615 | version = "2.8.2" 616 | description = "Extensions to the standard Python datetime module" 617 | category = "main" 618 | optional = false 619 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 620 | 621 | [package.dependencies] 622 | six = ">=1.5" 623 | 624 | [[package]] 625 | name = "python-jose" 626 | version = "3.3.0" 627 | description = "JOSE implementation in Python" 628 | category = "main" 629 | optional = false 630 | python-versions = "*" 631 | 632 | [package.dependencies] 633 | ecdsa = "!=0.15" 634 | pyasn1 = "*" 635 | rsa = "*" 636 | 637 | [package.extras] 638 | cryptography = ["cryptography (>=3.4.0)"] 639 | pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] 640 | pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] 641 | 642 | [[package]] 643 | name = "pyyaml" 644 | version = "6.0" 645 | description = "YAML parser and emitter for Python" 646 | category = "main" 647 | optional = false 648 | python-versions = ">=3.6" 649 | 650 | [[package]] 651 | name = "pyyaml-env-tag" 652 | version = "0.1" 653 | description = "A custom YAML tag for referencing environment variables in YAML files. " 654 | category = "dev" 655 | optional = false 656 | python-versions = ">=3.6" 657 | 658 | [package.dependencies] 659 | pyyaml = "*" 660 | 661 | [[package]] 662 | name = "rfc3986" 663 | version = "1.5.0" 664 | description = "Validating URI References per RFC 3986" 665 | category = "main" 666 | optional = false 667 | python-versions = "*" 668 | 669 | [package.dependencies] 670 | idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} 671 | 672 | [package.extras] 673 | idna2008 = ["idna"] 674 | 675 | [[package]] 676 | name = "rsa" 677 | version = "4.9" 678 | description = "Pure-Python RSA implementation" 679 | category = "main" 680 | optional = false 681 | python-versions = ">=3.6,<4" 682 | 683 | [package.dependencies] 684 | pyasn1 = ">=0.1.3" 685 | 686 | [[package]] 687 | name = "setuptools" 688 | version = "65.5.1" 689 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 690 | category = "dev" 691 | optional = false 692 | python-versions = ">=3.7" 693 | 694 | [package.extras] 695 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 696 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 697 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 698 | 699 | [[package]] 700 | name = "six" 701 | version = "1.16.0" 702 | description = "Python 2 and 3 compatibility utilities" 703 | category = "main" 704 | optional = false 705 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 706 | 707 | [[package]] 708 | name = "sniffio" 709 | version = "1.3.0" 710 | description = "Sniff out which async library your code is running under" 711 | category = "main" 712 | optional = false 713 | python-versions = ">=3.7" 714 | 715 | [[package]] 716 | name = "sortedcontainers" 717 | version = "2.4.0" 718 | description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" 719 | category = "dev" 720 | optional = false 721 | python-versions = "*" 722 | 723 | [[package]] 724 | name = "starlette" 725 | version = "0.21.0" 726 | description = "The little ASGI library that shines." 727 | category = "main" 728 | optional = false 729 | python-versions = ">=3.7" 730 | 731 | [package.dependencies] 732 | anyio = ">=3.4.0,<5" 733 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 734 | 735 | [package.extras] 736 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] 737 | 738 | [[package]] 739 | name = "starlite" 740 | version = "1.38.0" 741 | description = "Light-weight and flexible ASGI API Framework" 742 | category = "main" 743 | optional = false 744 | python-versions = ">=3.7,<4.0" 745 | 746 | [package.dependencies] 747 | anyio = ">=3" 748 | httpx = ">=0.22" 749 | multidict = ">=6.0.2" 750 | orjson = "*" 751 | pydantic = "*" 752 | pydantic-factories = "*" 753 | pydantic-openapi-schema = "*" 754 | pyyaml = "*" 755 | starlette = ">=0.21" 756 | starlite-multipart = ">=1.2.0" 757 | typing-extensions = "*" 758 | 759 | [package.extras] 760 | brotli = ["brotli"] 761 | cryptography = ["cryptography"] 762 | full = ["brotli", "cryptography", "picologging", "structlog"] 763 | memcached = ["aiomcache"] 764 | picologging = ["picologging"] 765 | redis = ["redis[hiredis]"] 766 | structlog = ["structlog"] 767 | 768 | [[package]] 769 | name = "starlite-multipart" 770 | version = "1.2.0" 771 | description = "Toolkit for working with multipart/formdata messages." 772 | category = "main" 773 | optional = false 774 | python-versions = ">=3.7,<4.0" 775 | 776 | [package.dependencies] 777 | anyio = "*" 778 | 779 | [[package]] 780 | name = "toml" 781 | version = "0.10.2" 782 | description = "Python Library for Tom's Obvious, Minimal Language" 783 | category = "dev" 784 | optional = false 785 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 786 | 787 | [[package]] 788 | name = "tomli" 789 | version = "2.0.1" 790 | description = "A lil' TOML parser" 791 | category = "dev" 792 | optional = false 793 | python-versions = ">=3.7" 794 | 795 | [[package]] 796 | name = "typing-extensions" 797 | version = "4.4.0" 798 | description = "Backported and Experimental Type Hints for Python 3.7+" 799 | category = "main" 800 | optional = false 801 | python-versions = ">=3.7" 802 | 803 | [[package]] 804 | name = "virtualenv" 805 | version = "20.16.6" 806 | description = "Virtual Python Environment builder" 807 | category = "dev" 808 | optional = false 809 | python-versions = ">=3.6" 810 | 811 | [package.dependencies] 812 | distlib = ">=0.3.6,<1" 813 | filelock = ">=3.4.1,<4" 814 | importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} 815 | platformdirs = ">=2.4,<3" 816 | 817 | [package.extras] 818 | docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] 819 | testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] 820 | 821 | [[package]] 822 | name = "watchdog" 823 | version = "2.1.9" 824 | description = "Filesystem events monitoring" 825 | category = "dev" 826 | optional = false 827 | python-versions = ">=3.6" 828 | 829 | [package.extras] 830 | watchmedo = ["PyYAML (>=3.10)"] 831 | 832 | [[package]] 833 | name = "zipp" 834 | version = "3.10.0" 835 | description = "Backport of pathlib-compatible object wrapper for zip files" 836 | category = "dev" 837 | optional = false 838 | python-versions = ">=3.7" 839 | 840 | [package.extras] 841 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] 842 | testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 843 | 844 | [metadata] 845 | lock-version = "1.1" 846 | python-versions = ">=3.7,<4.0" 847 | content-hash = "9a24c18dc56295fa7e4a745ac53ba2fa4a2214aea61d2ad2e2e8580969d9bbd4" 848 | 849 | [metadata.files] 850 | anyio = [ 851 | {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, 852 | {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, 853 | ] 854 | attrs = [ 855 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 856 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 857 | ] 858 | certifi = [ 859 | {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, 860 | {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, 861 | ] 862 | cffi = [ 863 | {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, 864 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, 865 | {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, 866 | {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, 867 | {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, 868 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, 869 | {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, 870 | {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, 871 | {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, 872 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, 873 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, 874 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, 875 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, 876 | {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, 877 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, 878 | {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, 879 | {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, 880 | {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, 881 | {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, 882 | {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, 883 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, 884 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, 885 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, 886 | {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, 887 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, 888 | {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, 889 | {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, 890 | {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, 891 | {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, 892 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, 893 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, 894 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, 895 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, 896 | {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, 897 | {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, 898 | {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, 899 | {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, 900 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, 901 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, 902 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, 903 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, 904 | {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, 905 | {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, 906 | {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, 907 | {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, 908 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, 909 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, 910 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, 911 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, 912 | {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, 913 | {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, 914 | {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, 915 | {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, 916 | {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, 917 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, 918 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, 919 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, 920 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, 921 | {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, 922 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, 923 | {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, 924 | {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, 925 | {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, 926 | {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, 927 | ] 928 | cfgv = [ 929 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 930 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 931 | ] 932 | click = [ 933 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 934 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 935 | ] 936 | colorama = [ 937 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 938 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 939 | ] 940 | coverage = [ 941 | {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, 942 | {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, 943 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, 944 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, 945 | {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, 946 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, 947 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, 948 | {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, 949 | {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, 950 | {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, 951 | {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, 952 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, 953 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, 954 | {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, 955 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, 956 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, 957 | {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, 958 | {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, 959 | {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, 960 | {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, 961 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, 962 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, 963 | {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, 964 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, 965 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, 966 | {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, 967 | {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, 968 | {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, 969 | {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, 970 | {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, 971 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, 972 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, 973 | {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, 974 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, 975 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, 976 | {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, 977 | {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, 978 | {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, 979 | {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, 980 | {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, 981 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, 982 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, 983 | {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, 984 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, 985 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, 986 | {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, 987 | {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, 988 | {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, 989 | {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, 990 | {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, 991 | ] 992 | cryptography = [ 993 | {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"}, 994 | {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"}, 995 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"}, 996 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"}, 997 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"}, 998 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"}, 999 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"}, 1000 | {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"}, 1001 | {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"}, 1002 | {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"}, 1003 | {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"}, 1004 | {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"}, 1005 | {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"}, 1006 | {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"}, 1007 | {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"}, 1008 | {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"}, 1009 | {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"}, 1010 | {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"}, 1011 | {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"}, 1012 | {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"}, 1013 | {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"}, 1014 | {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"}, 1015 | {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"}, 1016 | {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"}, 1017 | {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"}, 1018 | {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"}, 1019 | ] 1020 | distlib = [ 1021 | {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, 1022 | {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, 1023 | ] 1024 | dnspython = [ 1025 | {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, 1026 | {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, 1027 | ] 1028 | ecdsa = [ 1029 | {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, 1030 | {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, 1031 | ] 1032 | email-validator = [ 1033 | {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, 1034 | {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, 1035 | ] 1036 | exceptiongroup = [ 1037 | {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"}, 1038 | {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"}, 1039 | ] 1040 | faker = [ 1041 | {file = "Faker-15.3.1-py3-none-any.whl", hash = "sha256:4a3465624515a6807e8aa7e8eeb85bdd86a2fa53de4e258892dd6be95362462e"}, 1042 | {file = "Faker-15.3.1.tar.gz", hash = "sha256:b9dd2fd9a9ac68a4e0c5040cd9e9bfaa099fa8dd15bae5f01f224a45431818d5"}, 1043 | ] 1044 | filelock = [ 1045 | {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, 1046 | {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, 1047 | ] 1048 | ghp-import = [ 1049 | {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, 1050 | {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, 1051 | ] 1052 | h11 = [ 1053 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 1054 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 1055 | ] 1056 | httpcore = [ 1057 | {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, 1058 | {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, 1059 | ] 1060 | httpx = [ 1061 | {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, 1062 | {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, 1063 | ] 1064 | hypothesis = [ 1065 | {file = "hypothesis-6.56.4-py3-none-any.whl", hash = "sha256:67950103ee930c66673494b3671474a083ea71f1ebe8f0ff849ba8ad5317772d"}, 1066 | {file = "hypothesis-6.56.4.tar.gz", hash = "sha256:313bc1c0f377ec6c98815d3237a69add7558eadee4effe4ed613d0ba36513a52"}, 1067 | ] 1068 | identify = [ 1069 | {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, 1070 | {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, 1071 | ] 1072 | idna = [ 1073 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 1074 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 1075 | ] 1076 | importlib-metadata = [ 1077 | {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, 1078 | {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, 1079 | ] 1080 | iniconfig = [ 1081 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 1082 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 1083 | ] 1084 | jinja2 = [ 1085 | {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, 1086 | {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, 1087 | ] 1088 | markdown = [ 1089 | {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, 1090 | {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, 1091 | ] 1092 | markupsafe = [ 1093 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, 1094 | {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, 1095 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, 1096 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, 1097 | {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, 1098 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, 1099 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, 1100 | {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, 1101 | {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, 1102 | {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, 1103 | {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, 1104 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, 1105 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, 1106 | {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, 1107 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, 1108 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, 1109 | {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, 1110 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, 1111 | {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, 1112 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, 1113 | {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, 1114 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, 1115 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, 1116 | {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, 1117 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, 1118 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, 1119 | {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, 1120 | {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, 1121 | {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, 1122 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, 1123 | {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, 1124 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, 1125 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, 1126 | {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, 1127 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, 1128 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, 1129 | {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, 1130 | {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, 1131 | {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, 1132 | {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, 1133 | ] 1134 | mergedeep = [ 1135 | {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, 1136 | {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, 1137 | ] 1138 | mkdocs = [ 1139 | {file = "mkdocs-1.4.2-py3-none-any.whl", hash = "sha256:c8856a832c1e56702577023cd64cc5f84948280c1c0fcc6af4cd39006ea6aa8c"}, 1140 | {file = "mkdocs-1.4.2.tar.gz", hash = "sha256:8947af423a6d0facf41ea1195b8e1e8c85ad94ac95ae307fe11232e0424b11c5"}, 1141 | ] 1142 | mkdocs-gen-files = [ 1143 | {file = "mkdocs-gen-files-0.4.0.tar.gz", hash = "sha256:377bff8ee8e93515916689f483d971643f83a94eed7e92318854da8f344f0163"}, 1144 | {file = "mkdocs_gen_files-0.4.0-py3-none-any.whl", hash = "sha256:3241a4c947ecd11763ca77cc645015305bf71a0e1b9b886801c114fcf9971e71"}, 1145 | ] 1146 | multidict = [ 1147 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, 1148 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, 1149 | {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, 1150 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, 1151 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, 1152 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, 1153 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, 1154 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, 1155 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, 1156 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, 1157 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, 1158 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, 1159 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, 1160 | {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, 1161 | {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, 1162 | {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, 1163 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, 1164 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, 1165 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, 1166 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, 1167 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, 1168 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, 1169 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, 1170 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, 1171 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, 1172 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, 1173 | {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, 1174 | {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, 1175 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, 1176 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, 1177 | {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, 1178 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, 1179 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, 1180 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, 1181 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, 1182 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, 1183 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, 1184 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, 1185 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, 1186 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, 1187 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, 1188 | {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, 1189 | {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, 1190 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, 1191 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, 1192 | {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, 1193 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, 1194 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, 1195 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, 1196 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, 1197 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, 1198 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, 1199 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, 1200 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, 1201 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, 1202 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, 1203 | {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, 1204 | {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, 1205 | {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, 1206 | ] 1207 | nodeenv = [ 1208 | {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, 1209 | {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, 1210 | ] 1211 | orjson = [ 1212 | {file = "orjson-3.8.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:a70aaa2e56356e58c6e1b49f7b7f069df5b15e55db002a74db3ff3f7af67c7ff"}, 1213 | {file = "orjson-3.8.1-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d45db052d01d0ab7579470141d5c3592f4402d43cfacb67f023bc1210a67b7bc"}, 1214 | {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2aae92398c0023ac26a6cd026375f765ef5afe127eccabf563c78af7b572d59"}, 1215 | {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0bd5b4e539db8a9635776bdf9a25c3db84e37165e65d45c8ca90437adc46d6d8"}, 1216 | {file = "orjson-3.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21efb87b168066201a120b0f54a2381f6f51ff3727e07b3908993732412b314a"}, 1217 | {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:e073338e422f518c1d4d80efc713cd17f3ed6d37c8c7459af04a95459f3206d1"}, 1218 | {file = "orjson-3.8.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8f672f3987f6424f60ab2e86ea7ed76dd2806b8e9b506a373fc8499aed85ddb5"}, 1219 | {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:231c30958ed99c23128a21993c5ac0a70e1e568e6a898a47f70d5d37461ca47c"}, 1220 | {file = "orjson-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59b4baf71c9f39125d7e535974b146cc180926462969f6d8821b4c5e975e11b3"}, 1221 | {file = "orjson-3.8.1-cp310-none-win_amd64.whl", hash = "sha256:fe25f50dc3d45364428baa0dbe3f613a5171c64eb0286eb775136b74e61ba58a"}, 1222 | {file = "orjson-3.8.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:6802edf98f6918e89df355f56be6e7db369b31eed64ff2496324febb8b0aa43b"}, 1223 | {file = "orjson-3.8.1-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a4244f4199a160717f0027e434abb886e322093ceadb2f790ff0c73ed3e17662"}, 1224 | {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6956cf7a1ac97523e96f75b11534ff851df99a6474a561ad836b6e82004acbb8"}, 1225 | {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b4e3857dd2416b479f700e9bdf4fcec8c690d2716622397d2b7e848f9833e50"}, 1226 | {file = "orjson-3.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8873e490dea0f9cd975d66f84618b6fb57b1ba45ecb218313707a71173d764f"}, 1227 | {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:124207d2cd04e845eaf2a6171933cde40aebcb8c2d7d3b081e01be066d3014b6"}, 1228 | {file = "orjson-3.8.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d8ed77098c2e22181fce971f49a34204c38b79ca91c01d515d07015339ae8165"}, 1229 | {file = "orjson-3.8.1-cp311-none-win_amd64.whl", hash = "sha256:8623ac25fa0850a44ac845e9333c4da9ae5707b7cec8ac87cbe9d4e41137180f"}, 1230 | {file = "orjson-3.8.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:d67a0bd0283a3b17ac43c5ab8e4a7e9d3aa758d6ec5d51c232343c408825a5ad"}, 1231 | {file = "orjson-3.8.1-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d89ef8a4444d83e0a5171d14f2ab4895936ab1773165b020f97d29cf289a2d88"}, 1232 | {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97839a6abbebb06099294e6057d5b3061721ada08b76ae792e7041b6cb54c97f"}, 1233 | {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6071bcf51f0ae4d53b9d3e9164f7138164df4291c484a7b14562075aaa7a2b7b"}, 1234 | {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15e7d691cee75b5192fc1fa8487bf541d463246dc25c926b9b40f5b6ab56770"}, 1235 | {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:b9abc49c014def1b832fcd53bdc670474b6fe41f373d16f40409882c0d0eccba"}, 1236 | {file = "orjson-3.8.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:3fd5472020042482d7da4c26a0ee65dbd931f691e1c838c6cf4232823179ecc1"}, 1237 | {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e399ed1b0d6f8089b9b6ff2cb3e71ba63a56d8ea88e1d95467949795cc74adfd"}, 1238 | {file = "orjson-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5e3db6496463c3000d15b7a712da5a9601c6c43682f23f81862fe1d2a338f295"}, 1239 | {file = "orjson-3.8.1-cp37-none-win_amd64.whl", hash = "sha256:0f21eed14697083c01f7e00a87e21056fc8fb5851e8a7bca98345189abcdb4d4"}, 1240 | {file = "orjson-3.8.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:5a9e324213220578d324e0858baeab47808a13d3c3fbc6ba55a3f4f069d757cf"}, 1241 | {file = "orjson-3.8.1-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69097c50c3ccbcc61292192b045927f1688ca57ce80525dc5d120e0b91e19bb0"}, 1242 | {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7822cba140f7ca48ed0256229f422dbae69e3a3475176185db0c0538cfadb57"}, 1243 | {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03389e3750c521a7f3d4837de23cfd21a7f24574b4b3985c9498f440d21adb03"}, 1244 | {file = "orjson-3.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f9d9b5c6692097de07dd0b2d5ff20fd135bacd1b2fb7ea383ee717a4150c93"}, 1245 | {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:c2c9ef10b6344465fd5ac002be2d34f818211274dd79b44c75b2c14a979f84f3"}, 1246 | {file = "orjson-3.8.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7adaac93678ac61f5dc070f615b18639d16ee66f6a946d5221dbf315e8b74bec"}, 1247 | {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0c1750f73658906b82cabbf4be2f74300644c17cb037fbc8b48d746c3b90c76"}, 1248 | {file = "orjson-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:da6306e1f03e7085fe0db61d4a3377f70c6fd865118d0afe17f80ae9a8f6f124"}, 1249 | {file = "orjson-3.8.1-cp38-none-win_amd64.whl", hash = "sha256:f532c2cbe8c140faffaebcfb34d43c9946599ea8138971f181a399bec7d6b123"}, 1250 | {file = "orjson-3.8.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:6a7b76d4b44bca418f7797b1e157907b56b7d31caa9091db4e99ebee51c16933"}, 1251 | {file = "orjson-3.8.1-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f850489d89ea12be486492e68f0fd63e402fa28e426d4f0b5fc1eec0595e6109"}, 1252 | {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4449e70b98f3ad3e43958360e4be1189c549865c0a128e8629ec96ce92d251c3"}, 1253 | {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:45357eea9114bd41ef19280066591e9069bb4f6f5bffd533e9bfc12a439d735f"}, 1254 | {file = "orjson-3.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5a9bc5bc4d730153529cb0584c63ff286d50663ccd48c9435423660b1bb12d"}, 1255 | {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:a806aca6b80fa1d996aa16593e4995a71126a085ee1a59fff19ccad29a4e47fd"}, 1256 | {file = "orjson-3.8.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:395d02fd6be45f960da014372e7ecefc9e5f8df57a0558b7111a5fa8423c0669"}, 1257 | {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:caff3c1e964cfee044a03a46244ecf6373f3c56142ad16458a1446ac6d69824a"}, 1258 | {file = "orjson-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ded261268d5dfd307078fe3370295e5eb15bdde838bbb882acf8538e061c451"}, 1259 | {file = "orjson-3.8.1-cp39-none-win_amd64.whl", hash = "sha256:45c1914795ffedb2970bfcd3ed83daf49124c7c37943ed0a7368971c6ea5e278"}, 1260 | {file = "orjson-3.8.1.tar.gz", hash = "sha256:07c42de52dfef56cdcaf2278f58e837b26f5b5af5f1fd133a68c4af203851fc7"}, 1261 | ] 1262 | packaging = [ 1263 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 1264 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 1265 | ] 1266 | platformdirs = [ 1267 | {file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"}, 1268 | {file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"}, 1269 | ] 1270 | pluggy = [ 1271 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 1272 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 1273 | ] 1274 | pre-commit = [ 1275 | {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, 1276 | {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, 1277 | ] 1278 | pyasn1 = [ 1279 | {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, 1280 | {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, 1281 | {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, 1282 | {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, 1283 | {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, 1284 | {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, 1285 | {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, 1286 | {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, 1287 | {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, 1288 | {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, 1289 | {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, 1290 | {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, 1291 | {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, 1292 | ] 1293 | pycparser = [ 1294 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 1295 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 1296 | ] 1297 | pydantic = [ 1298 | {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, 1299 | {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, 1300 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, 1301 | {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, 1302 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, 1303 | {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, 1304 | {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, 1305 | {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, 1306 | {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, 1307 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, 1308 | {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, 1309 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, 1310 | {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, 1311 | {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, 1312 | {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, 1313 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, 1314 | {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, 1315 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, 1316 | {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, 1317 | {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, 1318 | {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, 1319 | {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, 1320 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, 1321 | {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, 1322 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, 1323 | {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, 1324 | {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, 1325 | {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, 1326 | {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, 1327 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, 1328 | {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, 1329 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, 1330 | {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, 1331 | {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, 1332 | {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, 1333 | {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, 1334 | ] 1335 | pydantic-factories = [ 1336 | {file = "pydantic_factories-1.15.0-py3-none-any.whl", hash = "sha256:04e1f125cb28cbe745f2fece4bd2b419786395dc7efabac74fe037dc5397490a"}, 1337 | {file = "pydantic_factories-1.15.0.tar.gz", hash = "sha256:5c0636a2c5f357390d0a528e70754b139b431dd675109c481c06c494b3b26432"}, 1338 | ] 1339 | pydantic-openapi-schema = [ 1340 | {file = "pydantic-openapi-schema-1.3.0.tar.gz", hash = "sha256:2aed6913080f1dae94234e00d0905504c6aab65ab6afe246ed7aa98da989f69e"}, 1341 | {file = "pydantic_openapi_schema-1.3.0-py3-none-any.whl", hash = "sha256:37a264596a3cd0c2627cf98e22fbf1231e76c19cd83c536d847fa17a2e80c211"}, 1342 | ] 1343 | pyparsing = [ 1344 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 1345 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 1346 | ] 1347 | pytest = [ 1348 | {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, 1349 | {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, 1350 | ] 1351 | pytest-asyncio = [ 1352 | {file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"}, 1353 | {file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"}, 1354 | ] 1355 | pytest-cov = [ 1356 | {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, 1357 | {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, 1358 | ] 1359 | python-dateutil = [ 1360 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 1361 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 1362 | ] 1363 | python-jose = [ 1364 | {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, 1365 | {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, 1366 | ] 1367 | pyyaml = [ 1368 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 1369 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 1370 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 1371 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 1372 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 1373 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 1374 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 1375 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 1376 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 1377 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 1378 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 1379 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 1380 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 1381 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 1382 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 1383 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 1384 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 1385 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 1386 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 1387 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 1388 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 1389 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 1390 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 1391 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 1392 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 1393 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 1394 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 1395 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 1396 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 1397 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 1398 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 1399 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 1400 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 1401 | ] 1402 | pyyaml-env-tag = [ 1403 | {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, 1404 | {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, 1405 | ] 1406 | rfc3986 = [ 1407 | {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, 1408 | {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, 1409 | ] 1410 | rsa = [ 1411 | {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, 1412 | {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, 1413 | ] 1414 | setuptools = [ 1415 | {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"}, 1416 | {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"}, 1417 | ] 1418 | six = [ 1419 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1420 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1421 | ] 1422 | sniffio = [ 1423 | {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, 1424 | {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, 1425 | ] 1426 | sortedcontainers = [ 1427 | {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, 1428 | {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, 1429 | ] 1430 | starlette = [ 1431 | {file = "starlette-0.21.0-py3-none-any.whl", hash = "sha256:0efc058261bbcddeca93cad577efd36d0c8a317e44376bcfc0e097a2b3dc24a7"}, 1432 | {file = "starlette-0.21.0.tar.gz", hash = "sha256:b1b52305ee8f7cfc48cde383496f7c11ab897cd7112b33d998b1317dc8ef9027"}, 1433 | ] 1434 | starlite = [ 1435 | {file = "starlite-1.38.0-py3-none-any.whl", hash = "sha256:cae00ff06f00eff779a5bf6f75b238b07886e2ec287f8a0b9ef4dc088f561370"}, 1436 | {file = "starlite-1.38.0.tar.gz", hash = "sha256:28b8ebe5a1bae98ab943cb022b11ebccffd8f5d91b066d8da48791733b5e4e1a"}, 1437 | ] 1438 | starlite-multipart = [ 1439 | {file = "starlite-multipart-1.2.0.tar.gz", hash = "sha256:9ba2108cfd47de07240dc90e55397080ec9ca0c6992ec1097a02dd32ba4ca593"}, 1440 | {file = "starlite_multipart-1.2.0-py3-none-any.whl", hash = "sha256:ceb5667d6c58bcbc305d5922ead791f0b50f0b9f658747144577a3dccffc5335"}, 1441 | ] 1442 | toml = [ 1443 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1444 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1445 | ] 1446 | tomli = [ 1447 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 1448 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 1449 | ] 1450 | typing-extensions = [ 1451 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 1452 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 1453 | ] 1454 | virtualenv = [ 1455 | {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, 1456 | {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, 1457 | ] 1458 | watchdog = [ 1459 | {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"}, 1460 | {file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b17d302850c8d412784d9246cfe8d7e3af6bcd45f958abb2d08a6f8bedf695d"}, 1461 | {file = "watchdog-2.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee3e38a6cc050a8830089f79cbec8a3878ec2fe5160cdb2dc8ccb6def8552658"}, 1462 | {file = "watchdog-2.1.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64a27aed691408a6abd83394b38503e8176f69031ca25d64131d8d640a307591"}, 1463 | {file = "watchdog-2.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:195fc70c6e41237362ba720e9aaf394f8178bfc7fa68207f112d108edef1af33"}, 1464 | {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bfc4d351e6348d6ec51df007432e6fe80adb53fd41183716017026af03427846"}, 1465 | {file = "watchdog-2.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8250546a98388cbc00c3ee3cc5cf96799b5a595270dfcfa855491a64b86ef8c3"}, 1466 | {file = "watchdog-2.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:117ffc6ec261639a0209a3252546b12800670d4bf5f84fbd355957a0595fe654"}, 1467 | {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:97f9752208f5154e9e7b76acc8c4f5a58801b338de2af14e7e181ee3b28a5d39"}, 1468 | {file = "watchdog-2.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:247dcf1df956daa24828bfea5a138d0e7a7c98b1a47cf1fa5b0c3c16241fcbb7"}, 1469 | {file = "watchdog-2.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:226b3c6c468ce72051a4c15a4cc2ef317c32590d82ba0b330403cafd98a62cfd"}, 1470 | {file = "watchdog-2.1.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d9820fe47c20c13e3c9dd544d3706a2a26c02b2b43c993b62fcd8011bcc0adb3"}, 1471 | {file = "watchdog-2.1.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:70af927aa1613ded6a68089a9262a009fbdf819f46d09c1a908d4b36e1ba2b2d"}, 1472 | {file = "watchdog-2.1.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed80a1628cee19f5cfc6bb74e173f1b4189eb532e705e2a13e3250312a62e0c9"}, 1473 | {file = "watchdog-2.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9f05a5f7c12452f6a27203f76779ae3f46fa30f1dd833037ea8cbc2887c60213"}, 1474 | {file = "watchdog-2.1.9-py3-none-manylinux2014_armv7l.whl", hash = "sha256:255bb5758f7e89b1a13c05a5bceccec2219f8995a3a4c4d6968fe1de6a3b2892"}, 1475 | {file = "watchdog-2.1.9-py3-none-manylinux2014_i686.whl", hash = "sha256:d3dda00aca282b26194bdd0adec21e4c21e916956d972369359ba63ade616153"}, 1476 | {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64.whl", hash = "sha256:186f6c55abc5e03872ae14c2f294a153ec7292f807af99f57611acc8caa75306"}, 1477 | {file = "watchdog-2.1.9-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:083171652584e1b8829581f965b9b7723ca5f9a2cd7e20271edf264cfd7c1412"}, 1478 | {file = "watchdog-2.1.9-py3-none-manylinux2014_s390x.whl", hash = "sha256:b530ae007a5f5d50b7fbba96634c7ee21abec70dc3e7f0233339c81943848dc1"}, 1479 | {file = "watchdog-2.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4f4e1c4aa54fb86316a62a87b3378c025e228178d55481d30d857c6c438897d6"}, 1480 | {file = "watchdog-2.1.9-py3-none-win32.whl", hash = "sha256:5952135968519e2447a01875a6f5fc8c03190b24d14ee52b0f4b1682259520b1"}, 1481 | {file = "watchdog-2.1.9-py3-none-win_amd64.whl", hash = "sha256:7a833211f49143c3d336729b0020ffd1274078e94b0ae42e22f596999f50279c"}, 1482 | {file = "watchdog-2.1.9-py3-none-win_ia64.whl", hash = "sha256:ad576a565260d8f99d97f2e64b0f97a48228317095908568a9d5c786c829d428"}, 1483 | {file = "watchdog-2.1.9.tar.gz", hash = "sha256:43ce20ebb36a51f21fa376f76d1d4692452b2527ccd601950d69ed36b9e21609"}, 1484 | ] 1485 | zipp = [ 1486 | {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, 1487 | {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, 1488 | ] 1489 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "starlite-jwt" 3 | version = "1.5.0" 4 | description = "A JWT auth toolkit for Starlite" 5 | authors = ["Na'aman Hirschfeld "] 6 | maintainers = [ 7 | "Na'aman Hirschfeld ", 8 | "Peter Schutt ", 9 | "Cody Fincher ", 10 | "Janek Nouvertné ", 11 | "Konstantin Mikhailov " 12 | ] 13 | license = "MIT" 14 | readme = "README.md" 15 | homepage = "https://github.com/starlite-api/jwt-auth" 16 | repository = "https://github.com/starlite-api/jwt-auth" 17 | documentation = "https://github.com/starlite-api/jwt-auth" 18 | keywords = ["starlite", "jwt", "jwt-token", "jwt-auth", "jwt-authentication", "auth", "authentication", "middleware", "token", "apitoken"] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Web Environment", 22 | "License :: OSI Approved :: MIT License", 23 | "Natural Language :: English", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python", 31 | "Topic :: Internet :: WWW/HTTP", 32 | "Topic :: Software Development :: Libraries", 33 | "Topic :: Software Development", 34 | "Typing :: Typed", 35 | ] 36 | include = ["CHANGELOG.md"] 37 | packages = [ 38 | { include = "starlite_jwt" }, 39 | ] 40 | 41 | [tool.poetry.dependencies] 42 | python = ">=3.8,<4.0" 43 | starlite = ">=1.24.0" 44 | python-jose = "*" 45 | cryptography = "*" 46 | 47 | [tool.poetry.dev-dependencies] 48 | pytest = "*" 49 | pytest-asyncio = "*" 50 | pytest-cov = "*" 51 | httpx = "*" 52 | pre-commit = "*" 53 | hypothesis = "*" 54 | mkdocs-gen-files = "*" 55 | 56 | [build-system] 57 | requires = ["poetry-core>=1.0.0"] 58 | build-backend = "poetry.core.masonry.api" 59 | 60 | [tool.black] 61 | line-length = 120 62 | include = '\.pyi?$' 63 | 64 | [tool.isort] 65 | profile = "black" 66 | multi_line_output = 3 67 | 68 | [tool.pylint.DESIGN] 69 | max-args = 7 70 | 71 | [tool.pylint.MESSAGE_CONTROL] 72 | disable = [ 73 | "line-too-long", 74 | "missing-class-docstring", 75 | "missing-module-docstring", 76 | "too-few-public-methods", 77 | "too-many-arguments" 78 | ] 79 | enable = "useless-suppression" 80 | extension-pkg-allow-list = ["pydantic"] 81 | 82 | [tool.pylint.REPORTS] 83 | reports = "no" 84 | 85 | [tool.pylint.FORMAT] 86 | max-line-length = "120" 87 | 88 | [tool.pylint.VARIABLES] 89 | ignored-argument-names = "args|kwargs|_|__" 90 | 91 | [tool.pylint.BASIC] 92 | good-names = "_,__,i,e" 93 | no-docstring-rgx="(__.*__|main|test.*|.*test|.*Test|^_.*)$" 94 | 95 | [tool.coverage.run] 96 | omit = ["*/tests/*"] 97 | 98 | [tool.pytest.ini_options] 99 | asyncio_mode = "auto" 100 | 101 | [tool.pycln] 102 | all = true 103 | 104 | [tool.pyright] 105 | include = ["starlite_jwt", "tests"] 106 | exclude = [] 107 | -------------------------------------------------------------------------------- /requirements-lint.txt: -------------------------------------------------------------------------------- 1 | black 2 | mkdocs-material 3 | coverage[toml] 4 | mypy 5 | pre-commit 6 | pylint 7 | pytest 8 | pytest-asyncio 9 | pytest-cov 10 | pytest-dotenv 11 | types-redis 12 | types-pyyaml 13 | types-requests 14 | types-python-jose 15 | flake8 16 | flake8-type-checking 17 | pyupgrade 18 | blacken-docs 19 | bandit 20 | flake8-bugbear 21 | flake8-comprehensions 22 | flake8-mutable 23 | flake8-print 24 | flake8-simplify 25 | slotscheck 26 | types-freezegun 27 | Jinja2 28 | freezegun 29 | pytest-mock 30 | tox 31 | mkdocs 32 | mkdocstrings 33 | mkdocstrings-python 34 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=starlite-api_jwt-auth 2 | sonar.organization=starlite-api 3 | sonar.python.coverage.reportPaths=coverage.xml 4 | sonar.test.inclusions=tests/test_*.py 5 | sonar.sources=starlite_jwt 6 | sonar.sourceEncoding=UTF-8 7 | sonar.python.version=3.7, 3.8, 3.9, 3.10 8 | -------------------------------------------------------------------------------- /starlite-banner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /starlite_jwt/__init__.py: -------------------------------------------------------------------------------- 1 | from starlite_jwt.jwt_auth import JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth 2 | from starlite_jwt.token import Token 3 | 4 | __all__ = ["JWTAuth", "Token", "OAuth2PasswordBearerAuth", "JWTCookieAuth"] 5 | -------------------------------------------------------------------------------- /starlite_jwt/jwt_auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | from typing import Any, Awaitable, Callable, Dict, List, Optional, Union 3 | 4 | from pydantic import BaseConfig, BaseModel, validator 5 | from pydantic_openapi_schema.v3_1_0 import ( 6 | Components, 7 | OAuthFlow, 8 | OAuthFlows, 9 | SecurityRequirement, 10 | SecurityScheme, 11 | ) 12 | from starlite import ASGIConnection, Cookie, DefineMiddleware, Response 13 | from starlite.enums import MediaType 14 | from starlite.status_codes import HTTP_201_CREATED 15 | from starlite.utils import AsyncCallable 16 | 17 | from starlite_jwt.middleware import ( 18 | CookieOptions, 19 | JWTAuthenticationMiddleware, 20 | JWTCookieAuthenticationMiddleware, 21 | ) 22 | from starlite_jwt.token import Token 23 | 24 | RetrieveUserHandler = Union[ 25 | Callable[[str], Any], 26 | Callable[[str], Awaitable[Any]], 27 | Callable[[str, ASGIConnection], Any], 28 | Callable[[str, ASGIConnection], Awaitable[Any]], 29 | ] 30 | 31 | 32 | class JWTAuth(BaseModel): 33 | """JWT Authentication Configuration. 34 | 35 | This class is the main entry point to the library, and it includes 36 | methods to create the middleware, provide login functionality, and 37 | create OpenAPI documentation. 38 | """ 39 | 40 | class Config(BaseConfig): 41 | arbitrary_types_allowed = True 42 | 43 | algorithm: str = "HS256" 44 | """ 45 | Algorithm to use for JWT hashing. 46 | """ 47 | auth_header: str = "Authorization" 48 | """ 49 | Request header key from which to retrieve the token. E.g. 'Authorization' or 'X-Api-Key'. 50 | """ 51 | default_token_expiration: timedelta = timedelta(days=1) 52 | """ 53 | The default value for token expiration. 54 | """ 55 | retrieve_user_handler: RetrieveUserHandler 56 | """ 57 | Callable that receives the 'sub' value of a token, which represents the 'subject' of the token (usually a user ID 58 | or equivalent value) and returns a 'user' value. 59 | 60 | Notes: 61 | - User can be any arbitrary value, 62 | - The callable can be sync or async. 63 | """ 64 | token_secret: str 65 | """ 66 | Key with which to generate the token hash. 67 | 68 | Notes: 69 | - This value should be kept as a secret and the standard practice is to inject it into the environment. 70 | """ 71 | exclude: Optional[Union[str, List[str]]] = None 72 | """ 73 | A pattern or list of patterns to skip in the authentication middleware. 74 | """ 75 | openapi_security_scheme_name: str = "BearerToken" 76 | """ 77 | The value to use for the OpenAPI security scheme and security requirements 78 | """ 79 | 80 | @validator("retrieve_user_handler") 81 | def validate_retrieve_user_handler(cls, value: RetrieveUserHandler) -> Any: # pylint: disable=no-self-argument 82 | """This validator ensures that the passed in value does not get bound. 83 | 84 | Args: 85 | value: A callable fulfilling the RetrieveUserHandler type. 86 | 87 | Returns: 88 | An instance of AsyncCallable wrapping the callable. 89 | """ 90 | return AsyncCallable(value) # type: ignore[arg-type] 91 | 92 | @property 93 | def openapi_components(self) -> Components: 94 | """Creates OpenAPI documentation for the JWT auth schema used. 95 | 96 | Returns: 97 | An [Components][pydantic_schema_pydantic.v3_1_0.components.Components] instance. 98 | """ 99 | return Components( 100 | securitySchemes={ 101 | self.openapi_security_scheme_name: SecurityScheme( 102 | type="http", 103 | scheme="Bearer", 104 | name=self.auth_header, 105 | bearerFormat="JWT", 106 | description="JWT api-key authentication and authorization.", 107 | ) 108 | } 109 | ) 110 | 111 | @property 112 | def security_requirement(self) -> SecurityRequirement: 113 | """ 114 | Returns: 115 | An OpenAPI 3.1 [SecurityRequirement][pydantic_schema_pydantic.v3_1_0.security_requirement.SecurityRequirement] dictionary. 116 | """ 117 | return {self.openapi_security_scheme_name: []} 118 | 119 | @property 120 | def middleware(self) -> DefineMiddleware: 121 | """Creates `JWTAuthenticationMiddleware` wrapped in Starlite's 122 | `DefineMiddleware`. 123 | 124 | Returns: 125 | An instance of [DefineMiddleware][starlite.middleware.base.DefineMiddleware]. 126 | """ 127 | return DefineMiddleware( 128 | JWTAuthenticationMiddleware, 129 | algorithm=self.algorithm, 130 | auth_header=self.auth_header, 131 | retrieve_user_handler=self.retrieve_user_handler, 132 | token_secret=self.token_secret, 133 | exclude=self.exclude, 134 | ) 135 | 136 | def login( 137 | self, 138 | identifier: str, 139 | *, 140 | response_body: Optional[Any] = None, 141 | response_media_type: Union[str, MediaType] = MediaType.JSON, 142 | response_status_code: int = HTTP_201_CREATED, 143 | token_expiration: Optional[timedelta] = None, 144 | token_issuer: Optional[str] = None, 145 | token_audience: Optional[str] = None, 146 | token_unique_jwt_id: Optional[str] = None, 147 | ) -> Response[Any]: 148 | """Create a response with a JWT header. Calls the 149 | 'JWTAuth.store_token_handler' to persist the token 'sub'. 150 | 151 | Args: 152 | identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. 153 | response_body: An optional response body to send. 154 | response_media_type: An optional 'Content-Type'. Defaults to 'application/json'. 155 | response_status_code: An optional status code for the response. Defaults to '201 Created'. 156 | token_expiration: An optional timedelta for the token expiration. 157 | token_issuer: An optional value of the token 'iss' field. 158 | token_audience: An optional value for the token 'aud' field. 159 | token_unique_jwt_id: An optional value for the token 'jti' field. 160 | 161 | Returns: 162 | A [Response][starlite.response.Response] instance. 163 | """ 164 | encoded_token = self.create_token( 165 | identifier=identifier, 166 | token_expiration=token_expiration, 167 | token_issuer=token_issuer, 168 | token_audience=token_audience, 169 | token_unique_jwt_id=token_unique_jwt_id, 170 | ) 171 | return Response( 172 | content=response_body, 173 | headers={self.auth_header: self.format_auth_header(encoded_token)}, 174 | media_type=response_media_type, 175 | status_code=response_status_code, 176 | ) 177 | 178 | def create_token( 179 | self, 180 | identifier: str, 181 | token_expiration: Optional[timedelta] = None, 182 | token_issuer: Optional[str] = None, 183 | token_audience: Optional[str] = None, 184 | token_unique_jwt_id: Optional[str] = None, 185 | ) -> str: 186 | """Creates a Token instance from the passed in parameters, persists and 187 | returns it. 188 | 189 | Args: 190 | identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. 191 | token_expiration: An optional timedelta for the token expiration. 192 | token_issuer: An optional value of the token 'iss' field. 193 | token_audience: An optional value for the token 'aud' field. 194 | token_unique_jwt_id: An optional value for the token 'jti' field. 195 | 196 | Returns: 197 | The created token. 198 | """ 199 | token = Token( 200 | sub=identifier, 201 | exp=(datetime.now(timezone.utc) + (token_expiration or self.default_token_expiration)), 202 | iss=token_issuer, 203 | aud=token_audience, 204 | jti=token_unique_jwt_id, 205 | ) 206 | encoded_token = token.encode(secret=self.token_secret, algorithm=self.algorithm) 207 | 208 | return encoded_token 209 | 210 | def format_auth_header(self, encoded_token: str) -> str: 211 | """Formats a token according to the specified OpenAPI scheme. 212 | 213 | Args: 214 | token: An encoded JWT token 215 | 216 | Returns: 217 | The encoded token formatted for the HTTP headers 218 | """ 219 | if self.openapi_components.securitySchemes: 220 | security = self.openapi_components.securitySchemes.get(self.openapi_security_scheme_name, None) 221 | if isinstance(security, SecurityScheme): 222 | return f"{security.scheme} {encoded_token}" 223 | return encoded_token 224 | 225 | 226 | class JWTCookieAuth(JWTAuth): 227 | """JWT Cookie Authentication Configuration. 228 | 229 | This class is an alternate entry point to the library, and it 230 | includes all of the functionality of the `JWTAuth` class and adds 231 | support for passing JWT tokens `HttpOnly` cookies. 232 | """ 233 | 234 | auth_cookie: str = "token" 235 | """ 236 | Cookie name from which to retrieve the token. E.g. 'access-token' or 'refresh-token'. 237 | """ 238 | auth_cookie_options: "CookieOptions" = CookieOptions(domain=None, secure=False, samesite="lax") 239 | """ 240 | Cookie configuration options to use when creating cookies for requests 241 | """ 242 | 243 | @property 244 | def openapi_components(self) -> Components: 245 | """Creates OpenAPI documentation for the JWT Cookie auth scheme. 246 | 247 | Returns: 248 | An [Components][pydantic_schema_pydantic.v3_1_0.components.Components] instance. 249 | """ 250 | return Components( 251 | securitySchemes={ 252 | self.openapi_security_scheme_name: SecurityScheme( 253 | type="http", 254 | scheme="Bearer", 255 | name=self.auth_cookie, 256 | security_scheme_in="cookie", 257 | bearerFormat="JWT", 258 | description="JWT cookie-based authentication and authorization.", 259 | ) 260 | } 261 | ) 262 | 263 | @property 264 | def middleware(self) -> DefineMiddleware: 265 | """Creates `JWTCookieAuthenticationMiddleware` wrapped in Starlite's 266 | `DefineMiddleware`. 267 | 268 | Returns: 269 | An instance of [DefineMiddleware][starlite.middleware.base.DefineMiddleware]. 270 | """ 271 | return DefineMiddleware( 272 | JWTCookieAuthenticationMiddleware, 273 | algorithm=self.algorithm, 274 | auth_header=self.auth_header, 275 | retrieve_user_handler=self.retrieve_user_handler, 276 | token_secret=self.token_secret, 277 | exclude=self.exclude, 278 | auth_cookie=self.auth_cookie, 279 | auth_cookie_options=self.auth_cookie_options, 280 | ) 281 | 282 | def login( 283 | self, 284 | identifier: str, 285 | *, 286 | response_body: Optional[Any] = None, 287 | response_media_type: Union[str, MediaType] = MediaType.JSON, 288 | response_status_code: int = HTTP_201_CREATED, 289 | token_expiration: Optional[timedelta] = None, 290 | token_issuer: Optional[str] = None, 291 | token_audience: Optional[str] = None, 292 | token_unique_jwt_id: Optional[str] = None, 293 | ) -> Response[Any]: 294 | """Create a response with a JWT header. Calls the 295 | 'JWTAuth.store_token_handler' to persist the token 'sub'. 296 | 297 | Args: 298 | identifier: Unique identifier of the token subject. Usually this is a user ID or equivalent kind of value. 299 | response_body: An optional response body to send. 300 | response_media_type: An optional 'Content-Type'. Defaults to 'application/json'. 301 | response_status_code: An optional status code for the response. Defaults to '201 Created'. 302 | token_expiration: An optional timedelta for the token expiration. 303 | token_issuer: An optional value of the token 'iss' field. 304 | token_audience: An optional value for the token 'aud' field. 305 | token_unique_jwt_id: An optional value for the token 'jti' field. 306 | 307 | Returns: 308 | A [Response][starlite.response.Response] instance. 309 | """ 310 | encoded_token = self.create_token( 311 | identifier=identifier, 312 | token_expiration=token_expiration, 313 | token_issuer=token_issuer, 314 | token_audience=token_audience, 315 | token_unique_jwt_id=token_unique_jwt_id, 316 | ) 317 | return Response( 318 | content=response_body, 319 | headers={self.auth_header: self.format_auth_header(encoded_token)}, 320 | cookies=[ 321 | Cookie( 322 | key=self.auth_cookie, 323 | value=self.format_auth_header(encoded_token), 324 | httponly=True, 325 | expires=int( 326 | (datetime.now(timezone.utc) + (token_expiration or self.default_token_expiration)).timestamp() 327 | ), 328 | **self.auth_cookie_options.dict(exclude_none=True), 329 | ) 330 | ], 331 | media_type=response_media_type, 332 | status_code=response_status_code, 333 | ) 334 | 335 | 336 | class OAuth2PasswordBearerAuth(JWTCookieAuth): 337 | """OAUTH2 Schema for Password Bearer Authentication. 338 | 339 | This class implements an OAUTH2 authentication flow entry point to the library, and it 340 | includes all of the functionality of the `JWTAuth` class and adds 341 | support for passing JWT tokens `HttpOnly` cookies. 342 | 343 | `token_url` is the only additional required argument and should point your login route 344 | """ 345 | 346 | token_url: str 347 | """ 348 | The URL for retrieving a new token. 349 | """ 350 | scopes: Optional[Dict[str, str]] = {} 351 | """Scopes available for the token.""" 352 | 353 | @property 354 | def oauth_flow(self) -> OAuthFlow: 355 | """Creates an OpenAPI OAuth2 flow for the password bearer 356 | authentication scheme. 357 | 358 | Returns: 359 | An [OAuthFlow][pydantic_schema_pydantic.v3_1_0.oauth_flow.OAuthFlow] instance. 360 | """ 361 | return OAuthFlow( 362 | tokenUrl=self.token_url, 363 | scopes=self.scopes, 364 | ) 365 | 366 | @property 367 | def openapi_components(self) -> Components: 368 | """Creates OpenAPI documentation for the OAUTH2 Password bearer auth 369 | scheme. 370 | 371 | Returns: 372 | An [Components][pydantic_schema_pydantic.v3_1_0.components.Components] instance. 373 | """ 374 | return Components( 375 | securitySchemes={ 376 | self.openapi_security_scheme_name: SecurityScheme( 377 | type="oauth2", 378 | scheme="Bearer", 379 | name=self.auth_header, 380 | security_scheme_in="header", 381 | flows=OAuthFlows(password=self.oauth_flow), # pyright: reportGeneralTypeIssues=false 382 | bearerFormat="JWT", 383 | description="OAUTH2 password bearer authentication and authorization.", 384 | ) 385 | } 386 | ) 387 | -------------------------------------------------------------------------------- /starlite_jwt/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Awaitable, List, Literal, Optional, Union 2 | 3 | from pydantic import BaseModel 4 | from starlite import ( 5 | AbstractAuthenticationMiddleware, 6 | AuthenticationResult, 7 | NotAuthorizedException, 8 | ) 9 | from starlite.connection import ASGIConnection 10 | 11 | from starlite_jwt.token import Token 12 | 13 | if TYPE_CHECKING: # pragma: no cover 14 | from typing import Any 15 | 16 | from starlite.types import ASGIApp 17 | from starlite.utils import AsyncCallable 18 | 19 | 20 | class JWTAuthenticationMiddleware(AbstractAuthenticationMiddleware): 21 | def __init__( 22 | self, 23 | app: "ASGIApp", 24 | exclude: Optional[Union[str, List[str]]], 25 | algorithm: str, 26 | auth_header: str, 27 | retrieve_user_handler: Union[ 28 | "AsyncCallable[[Any, ASGIConnection[Any, Any, Any]], Awaitable[Any]]", 29 | "AsyncCallable[[Any], Awaitable[Any]]", 30 | ], 31 | token_secret: str, 32 | ): 33 | """This Class is a Starlite compatible JWT authentication middleware. 34 | 35 | It checks incoming requests for an encoded token in the auth header specified, 36 | and if present retrieves the user from persistence using the provided function. 37 | 38 | Args: 39 | app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. 40 | retrieve_user_handler: A function that receives an instance of 'Token' and returns a user, which can be 41 | any arbitrary value. 42 | token_secret: Secret for decoding the JWT token. This value should be equivalent to the secret used to encode it. 43 | auth_header: Request header key from which to retrieve the token. E.g. 'Authorization' or 'X-Api-Key'. 44 | algorithm: JWT hashing algorithm to use. 45 | exclude: A pattern or list of patterns to skip. 46 | """ 47 | super().__init__(app=app, exclude=exclude) 48 | self.algorithm = algorithm 49 | self.auth_header = auth_header 50 | self.retrieve_user_handler = retrieve_user_handler 51 | self.token_secret = token_secret 52 | 53 | async def authenticate_request(self, connection: "ASGIConnection[Any,Any,Any]") -> AuthenticationResult: 54 | """Given an HTTP Connection, parse the JWT api key stored in the header 55 | and retrieve the user correlating to the token from the DB. 56 | 57 | Args: 58 | connection: An Starlite HTTPConnection instance. 59 | 60 | Returns: 61 | AuthenticationResult 62 | 63 | Raises: 64 | [NotAuthorizedException][starlite.exceptions.NotAuthorizedException]: If token is invalid or user is not found. 65 | """ 66 | auth_header = connection.headers.get(self.auth_header) 67 | if not auth_header: 68 | raise NotAuthorizedException("No JWT token found in request header") 69 | _, _, encoded_token = auth_header.partition(" ") 70 | return await self.authenticate_token(encoded_token=encoded_token, connection=connection) 71 | 72 | async def authenticate_token( 73 | self, encoded_token: "Any", connection: "ASGIConnection[Any, Any, Any]" 74 | ) -> AuthenticationResult: 75 | """Given an encoded JWT token, parse, validate and look up sub within 76 | token. 77 | 78 | Args: 79 | encoded_token (Any): _description_ 80 | 81 | Raises: 82 | [NotAuthorizedException][starlite.exceptions.NotAuthorizedException]: If token is invalid or user is not found. 83 | 84 | Returns: 85 | AuthenticationResult: _description_ 86 | """ 87 | token = Token.decode( 88 | encoded_token=encoded_token, 89 | secret=self.token_secret, 90 | algorithm=self.algorithm, 91 | ) 92 | if self.retrieve_user_handler.num_expected_args == 2: 93 | user = await self.retrieve_user_handler(token.sub, connection) # type: ignore[call-arg] 94 | else: 95 | user = await self.retrieve_user_handler(token.sub) # type: ignore[call-arg] 96 | 97 | if not user: 98 | raise NotAuthorizedException() 99 | 100 | return AuthenticationResult(user=user, auth=token) 101 | 102 | 103 | class CookieOptions(BaseModel): 104 | path: str = "/" 105 | """Path fragment that must exist in the request url for the cookie to be valid. Defaults to '/'.""" 106 | domain: Optional[str] = None 107 | """Domain for which the cookie is valid.""" 108 | secure: bool = False 109 | """Https is required for the cookie.""" 110 | samesite: Literal["lax", "strict", "none"] = "lax" 111 | """Controls whether or not a cookie is sent with cross-site requests. Defaults to 'lax'.""" 112 | description: Optional[str] = None 113 | """Description of the response cookie header for OpenAPI documentation""" 114 | 115 | 116 | class JWTCookieAuthenticationMiddleware(JWTAuthenticationMiddleware): 117 | def __init__( 118 | self, 119 | app: "ASGIApp", 120 | exclude: Optional[Union[str, List[str]]], 121 | algorithm: str, 122 | auth_header: str, 123 | auth_cookie: str, 124 | auth_cookie_options: CookieOptions, 125 | retrieve_user_handler: Union[ 126 | "AsyncCallable[[Any, ASGIConnection[Any, Any, Any]], Awaitable[Any]]", 127 | "AsyncCallable[[Any], Awaitable[Any]]", 128 | ], 129 | token_secret: str, 130 | ): 131 | """This Class is a Starlite compatible JWT authentication middleware 132 | with cookie support. 133 | 134 | It checks incoming requests for an encoded token in the auth header or cookie name specified, 135 | and if present retrieves the user from persistence using the provided function. 136 | 137 | Args: 138 | app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack. 139 | retrieve_user_handler: A function that receives an instance of 'Token' and returns a user, which can be 140 | any arbitrary value. 141 | token_secret: Secret for decoding the JWT token. This value should be equivalent to the secret used to encode it. 142 | auth_header: Request header key from which to retrieve the token. E.g. 'Authorization' or 'X-Api-Key'. 143 | auth_cookie: Cookie name from which to retrieve the token. E.g. 'token' or 'accessToken'. 144 | auth_cookie_options: Cookie configuration options to use when creating cookies for requests. 145 | algorithm: JWT hashing algorithm to use. 146 | exclude: A pattern or list of patterns to skip. 147 | """ 148 | super().__init__( 149 | algorithm=algorithm, 150 | app=app, 151 | auth_header=auth_header, 152 | retrieve_user_handler=retrieve_user_handler, 153 | token_secret=token_secret, 154 | exclude=exclude, 155 | ) 156 | 157 | self.auth_cookie = auth_cookie 158 | self.auth_cookie_options = auth_cookie_options or CookieOptions() 159 | 160 | async def authenticate_request(self, connection: "ASGIConnection[Any,Any,Any]") -> AuthenticationResult: 161 | """Given an HTTP Connection, parse the JWT api key stored in the header 162 | and retrieve the user correlating to the token from the DB. 163 | 164 | Args: 165 | connection: An Starlite HTTPConnection instance. 166 | 167 | Returns: 168 | AuthenticationResult 169 | 170 | Raises: 171 | [NotAuthorizedException][starlite.exceptions.NotAuthorizedException]: If token is invalid or user is not found. 172 | """ 173 | auth_header = connection.headers.get(self.auth_header) or connection.cookies.get(self.auth_cookie) 174 | if not auth_header: 175 | raise NotAuthorizedException("No JWT token found in request header or cookies") 176 | _, _, encoded_token = auth_header.partition(" ") 177 | return await self.authenticate_token(encoded_token=encoded_token, connection=connection) 178 | -------------------------------------------------------------------------------- /starlite_jwt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/litestar-org/starlite-jwt/c00fafc6ef3f82cddecfbc1cd66a0e5fd618466f/starlite_jwt/py.typed -------------------------------------------------------------------------------- /starlite_jwt/token.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Dict, Optional, Union 3 | 4 | from jose import JWSError, JWTError, jwt 5 | from pydantic import ( 6 | BaseConfig, 7 | BaseModel, 8 | Extra, 9 | Field, 10 | ValidationError, 11 | constr, 12 | validator, 13 | ) 14 | from starlite import ImproperlyConfiguredException 15 | from starlite.exceptions import NotAuthorizedException 16 | 17 | 18 | def _normalize_datetime(value: datetime) -> datetime: 19 | """Converts the given value into UTC and strips microseconds. 20 | 21 | Args: 22 | value: A datetime instance 23 | 24 | Returns: 25 | A datetime instance 26 | """ 27 | if value.tzinfo is not None: 28 | value.astimezone(timezone.utc) 29 | return value.replace(microsecond=0) 30 | 31 | 32 | class Token(BaseModel): 33 | """This class represents a JWT token.""" 34 | 35 | class Config(BaseConfig): 36 | extra = Extra.allow 37 | 38 | exp: datetime 39 | """Expiration - datetime for token expiration.""" 40 | iat: datetime = Field(default_factory=lambda: _normalize_datetime(datetime.now(timezone.utc))) 41 | """Issued at - should always be current now.""" 42 | sub: constr(min_length=1) # type: ignore[valid-type] 43 | """Subject - usually a unique identifier of the user or equivalent entity.""" 44 | iss: Optional[str] = None 45 | """Issuer - optional unique identifier for the issuer.""" 46 | aud: Optional[str] = None 47 | """Audience - intended audience.""" 48 | jti: Optional[str] = None 49 | """JWT ID - a unique identifier of the JWT between different issuers.""" 50 | 51 | @validator("exp", always=True) 52 | def validate_exp(cls, value: datetime) -> datetime: # pylint: disable=no-self-argument 53 | """Ensures that 'exp' value is a future datetime. 54 | 55 | Args: 56 | value: A datetime instance. 57 | 58 | Raises: 59 | ValueError: if value is not a future datetime instance. 60 | 61 | Returns: 62 | The validated datetime. 63 | """ 64 | 65 | value = _normalize_datetime(value) 66 | if value.timestamp() >= _normalize_datetime(datetime.now(timezone.utc)).timestamp(): 67 | return value 68 | raise ValueError("exp value must be a datetime in the future") 69 | 70 | @validator("iat", always=True) 71 | def validate_iat(cls, value: datetime) -> datetime: # pylint: disable=no-self-argument 72 | """Ensures that 'iat' value is a past or current datetime. 73 | 74 | Args: 75 | value: A datetime instance. 76 | 77 | Raises: 78 | ValueError: if value is not a past or current datetime instance. 79 | 80 | Returns: 81 | The validated datetime. 82 | """ 83 | value = _normalize_datetime(value) 84 | if value.timestamp() <= _normalize_datetime(datetime.now(timezone.utc)).timestamp(): 85 | return value 86 | raise ValueError("iat must be a current or past time") 87 | 88 | @staticmethod 89 | def decode(encoded_token: str, secret: Union[str, Dict[str, str]], algorithm: str) -> "Token": 90 | """Decodes a passed in token string and returns a Token instance. 91 | 92 | Args: 93 | encoded_token: A base64 string containing an encoded JWT. 94 | secret: The secret with which the JWT is encoded. It may optionally 95 | be an individual JWK or JWS set dict 96 | algorithm: The algorithm used to encode the JWT. 97 | 98 | Returns: 99 | A decoded Token instance. 100 | 101 | Raises: 102 | [NotAuthorizedException][starlite.exceptions.NotAuthorizedException]: If token is invalid. 103 | """ 104 | try: 105 | payload = jwt.decode(token=encoded_token, key=secret, algorithms=[algorithm], options={"verify_aud": False}) 106 | return Token(**payload) 107 | except (JWTError, ValidationError) as e: 108 | raise NotAuthorizedException("Invalid token") from e 109 | 110 | def encode(self, secret: str, algorithm: str) -> str: 111 | """Encodes the token instance into a string. 112 | 113 | Args: 114 | secret: The secret with which the JWT is encoded. 115 | algorithm: The algorithm used to encode the JWT. 116 | 117 | Returns: 118 | An encoded token string. 119 | 120 | Raises: 121 | [ImproperlyConfiguredException][starlite.exceptions.ImproperlyConfiguredException]: If encoding fails. 122 | """ 123 | try: 124 | return jwt.encode(claims=self.dict(exclude_none=True), key=secret, algorithm=algorithm) 125 | except (JWTError, JWSError) as e: 126 | raise ImproperlyConfiguredException("Failed to encode token") from e 127 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | import pytest 4 | from pydantic import BaseModel 5 | from pydantic_factories import ModelFactory 6 | from starlite.cache.simple_cache_backend import SimpleCacheBackend 7 | 8 | 9 | class User(BaseModel): 10 | name: str 11 | id: UUID 12 | 13 | 14 | class UserFactory(ModelFactory[User]): 15 | __model__ = User 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def mock_db() -> SimpleCacheBackend: 20 | return SimpleCacheBackend() 21 | -------------------------------------------------------------------------------- /tests/test_jwt_auth.py: -------------------------------------------------------------------------------- 1 | import string 2 | from datetime import datetime, timedelta, timezone 3 | from typing import TYPE_CHECKING, Any, Dict, Optional 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from hypothesis import given 8 | from hypothesis.strategies import integers, none, one_of, sampled_from, text, timedeltas 9 | from starlite import OpenAPIConfig, Request, Response, Starlite, get 10 | from starlite.status_codes import HTTP_200_OK, HTTP_401_UNAUTHORIZED 11 | from starlite.testing import create_test_client 12 | 13 | from starlite_jwt import JWTAuth, JWTCookieAuth, OAuth2PasswordBearerAuth, Token 14 | from tests.conftest import User, UserFactory 15 | 16 | if TYPE_CHECKING: 17 | 18 | from starlite.cache import SimpleCacheBackend 19 | 20 | 21 | @pytest.mark.asyncio() 22 | @given( 23 | algorithm=sampled_from( 24 | [ 25 | "HS256", 26 | "HS384", 27 | "HS512", 28 | ] 29 | ), 30 | auth_header=sampled_from(["Authorization", "X-API-Key"]), 31 | default_token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), 32 | token_secret=text(min_size=10), 33 | response_status_code=integers(min_value=200, max_value=201), 34 | token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), 35 | token_issuer=one_of(none(), text(max_size=256)), 36 | token_audience=one_of(none(), text(max_size=256, alphabet=string.ascii_letters)), 37 | token_unique_jwt_id=one_of(none(), text(max_size=256)), 38 | ) 39 | async def test_jwt_auth( 40 | mock_db: "SimpleCacheBackend", 41 | algorithm: str, 42 | auth_header: str, 43 | default_token_expiration: timedelta, 44 | token_secret: str, 45 | response_status_code: int, 46 | token_expiration: timedelta, 47 | token_issuer: Optional[str], 48 | token_audience: Optional[str], 49 | token_unique_jwt_id: Optional[str], 50 | ) -> None: 51 | user = UserFactory.build() 52 | 53 | await mock_db.set(str(user.id), user, 120) 54 | 55 | async def retrieve_user_handler(sub: str) -> Optional["User"]: 56 | stored_user = await mock_db.get(sub) 57 | return stored_user 58 | 59 | jwt_auth = JWTAuth( 60 | algorithm=algorithm, 61 | auth_header=auth_header, 62 | default_token_expiration=default_token_expiration, 63 | token_secret=token_secret, 64 | retrieve_user_handler=retrieve_user_handler, 65 | ) 66 | 67 | @get("/my-endpoint", middleware=[jwt_auth.middleware]) 68 | def my_handler(request: Request["User", Token]) -> None: 69 | assert request.user 70 | assert request.user.dict() == user.dict() 71 | assert request.auth.sub == str(user.id) 72 | 73 | @get("/login") 74 | def login_handler() -> Response["User"]: 75 | return jwt_auth.login( 76 | identifier=str(user.id), 77 | response_body=user, 78 | response_status_code=response_status_code, 79 | token_expiration=token_expiration, 80 | token_issuer=token_issuer, 81 | token_audience=token_audience, 82 | token_unique_jwt_id=token_unique_jwt_id, 83 | ) 84 | 85 | with create_test_client(route_handlers=[my_handler, login_handler]) as client: 86 | response = client.get("/login") 87 | assert response.status_code == response_status_code 88 | _, _, encoded_token = response.headers.get(auth_header).partition(" ") 89 | assert encoded_token 90 | decoded_token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) 91 | assert decoded_token.sub == str(user.id) 92 | assert decoded_token.iss == token_issuer 93 | assert decoded_token.aud == token_audience 94 | assert decoded_token.jti == token_unique_jwt_id 95 | 96 | response = client.get("/my-endpoint") 97 | assert response.status_code == HTTP_401_UNAUTHORIZED 98 | 99 | response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) 100 | assert response.status_code == HTTP_200_OK 101 | 102 | response = client.get("/my-endpoint", headers={auth_header: encoded_token}) 103 | assert response.status_code == HTTP_401_UNAUTHORIZED 104 | 105 | response = client.get("/my-endpoint", headers={auth_header: uuid4().hex}) 106 | assert response.status_code == HTTP_401_UNAUTHORIZED 107 | 108 | fake_token = Token( 109 | sub=uuid4().hex, 110 | iss=token_issuer, 111 | aud=token_audience, 112 | jti=token_unique_jwt_id, 113 | exp=(datetime.now(timezone.utc) + token_expiration), 114 | ).encode(secret=token_secret, algorithm=algorithm) 115 | 116 | response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(fake_token)}) 117 | assert response.status_code == HTTP_401_UNAUTHORIZED 118 | 119 | 120 | @pytest.mark.asyncio() 121 | @given( 122 | algorithm=sampled_from( 123 | [ 124 | "HS256", 125 | "HS384", 126 | "HS512", 127 | ] 128 | ), 129 | auth_header=sampled_from(["Authorization", "X-API-Key"]), 130 | auth_cookie=sampled_from(["token", "accessToken"]), 131 | default_token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), 132 | token_secret=text(min_size=10), 133 | response_status_code=integers(min_value=200, max_value=201), 134 | token_expiration=timedeltas(min_value=timedelta(seconds=30), max_value=timedelta(weeks=1)), 135 | token_issuer=one_of(none(), text(max_size=256)), 136 | token_audience=one_of(none(), text(max_size=256, alphabet=string.ascii_letters)), 137 | token_unique_jwt_id=one_of(none(), text(max_size=256)), 138 | ) 139 | async def test_jwt_cookie_auth( 140 | mock_db: "SimpleCacheBackend", 141 | algorithm: str, 142 | auth_header: str, 143 | auth_cookie: str, 144 | default_token_expiration: timedelta, 145 | token_secret: str, 146 | response_status_code: int, 147 | token_expiration: timedelta, 148 | token_issuer: Optional[str], 149 | token_audience: Optional[str], 150 | token_unique_jwt_id: Optional[str], 151 | ) -> None: 152 | user = UserFactory.build() 153 | 154 | await mock_db.set(str(user.id), user, 120) 155 | 156 | async def retrieve_user_handler(sub: str, connection: Any) -> Optional["User"]: 157 | stored_user = await mock_db.get(sub) 158 | assert connection 159 | return stored_user 160 | 161 | jwt_auth = JWTCookieAuth( 162 | algorithm=algorithm, 163 | auth_header=auth_header, 164 | token_secret=token_secret, 165 | retrieve_user_handler=retrieve_user_handler, 166 | auth_cookie=auth_cookie, 167 | default_token_expiration=default_token_expiration, 168 | ) 169 | 170 | @get("/my-endpoint", middleware=[jwt_auth.middleware]) 171 | def my_handler(request: Request["User", Token]) -> None: 172 | assert request.user 173 | assert request.user.dict() == user.dict() 174 | assert request.auth.sub == str(user.id) 175 | 176 | @get("/login") 177 | def login_handler() -> Response["User"]: 178 | return jwt_auth.login( 179 | identifier=str(user.id), 180 | response_body=user, 181 | response_status_code=response_status_code, 182 | token_expiration=token_expiration, 183 | token_issuer=token_issuer, 184 | token_audience=token_audience, 185 | token_unique_jwt_id=token_unique_jwt_id, 186 | ) 187 | 188 | with create_test_client(route_handlers=[my_handler, login_handler]) as client: 189 | response = client.get("/login") 190 | assert response.status_code == response_status_code 191 | _, _, encoded_token = response.headers.get(auth_header).partition(" ") 192 | assert encoded_token 193 | decoded_token = Token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) 194 | assert decoded_token.sub == str(user.id) 195 | assert decoded_token.iss == token_issuer 196 | assert decoded_token.aud == token_audience 197 | assert decoded_token.jti == token_unique_jwt_id 198 | 199 | client.cookies.clear() 200 | response = client.get("/my-endpoint") 201 | assert response.status_code == HTTP_401_UNAUTHORIZED 202 | 203 | client.cookies.clear() 204 | response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(encoded_token)}) 205 | assert response.status_code == HTTP_200_OK 206 | 207 | client.cookies.clear() 208 | response = client.get("/my-endpoint", cookies={auth_cookie: jwt_auth.format_auth_header(encoded_token)}) 209 | assert response.status_code == HTTP_200_OK 210 | 211 | client.cookies.clear() 212 | response = client.get("/my-endpoint", headers={auth_header: encoded_token}) 213 | assert response.status_code == HTTP_401_UNAUTHORIZED 214 | 215 | client.cookies.clear() 216 | response = client.get("/my-endpoint", headers={auth_cookie: encoded_token}) 217 | assert response.status_code == HTTP_401_UNAUTHORIZED 218 | 219 | client.cookies.clear() 220 | response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(uuid4().hex)}) 221 | assert response.status_code == HTTP_401_UNAUTHORIZED 222 | 223 | client.cookies.clear() 224 | response = client.get("/my-endpoint", cookies={auth_cookie: jwt_auth.format_auth_header(uuid4().hex)}) 225 | assert response.status_code == HTTP_401_UNAUTHORIZED 226 | 227 | client.cookies.clear() 228 | response = client.get("/my-endpoint", cookies={auth_cookie: uuid4().hex}) 229 | assert response.status_code == HTTP_401_UNAUTHORIZED 230 | fake_token = Token( 231 | sub=uuid4().hex, 232 | iss=token_issuer, 233 | aud=token_audience, 234 | jti=token_unique_jwt_id, 235 | exp=(datetime.now(timezone.utc) + token_expiration), 236 | ).encode(secret=token_secret, algorithm=algorithm) 237 | 238 | client.cookies.clear() 239 | response = client.get("/my-endpoint", headers={auth_header: jwt_auth.format_auth_header(fake_token)}) 240 | assert response.status_code == HTTP_401_UNAUTHORIZED 241 | 242 | client.cookies.clear() 243 | response = client.get("/my-endpoint", cookies={auth_cookie: jwt_auth.format_auth_header(fake_token)}) 244 | assert response.status_code == HTTP_401_UNAUTHORIZED 245 | 246 | 247 | async def test_path_exclusion() -> None: 248 | async def retrieve_user_handler(_: str) -> None: 249 | return None 250 | 251 | jwt_auth = JWTAuth( 252 | token_secret="abc123", 253 | retrieve_user_handler=retrieve_user_handler, 254 | exclude=["north", "south"], 255 | ) 256 | 257 | @get("/north/{value:int}") 258 | def north_handler(value: int) -> Dict[str, int]: 259 | return {"value": value} 260 | 261 | @get("/south") 262 | def south_handler() -> None: 263 | return None 264 | 265 | @get("/west") 266 | def west_handler() -> None: 267 | return None 268 | 269 | with create_test_client( 270 | route_handlers=[north_handler, south_handler, west_handler], middleware=[jwt_auth.middleware] 271 | ) as client: 272 | response = client.get("/north/1") 273 | assert response.status_code == HTTP_200_OK 274 | 275 | response = client.get("/south") 276 | assert response.status_code == HTTP_200_OK 277 | 278 | response = client.get("/west") 279 | assert response.status_code == HTTP_401_UNAUTHORIZED 280 | 281 | 282 | def test_openapi() -> None: 283 | jwt_auth = JWTAuth(token_secret="abc123", retrieve_user_handler=lambda _: None) 284 | assert jwt_auth.openapi_components.dict(exclude_none=True) == { 285 | "securitySchemes": { 286 | "BearerToken": { 287 | "type": "http", 288 | "description": "JWT api-key authentication and authorization.", 289 | "name": "Authorization", 290 | "scheme": "Bearer", 291 | "bearerFormat": "JWT", 292 | } 293 | } 294 | } 295 | assert jwt_auth.security_requirement == {"BearerToken": []} 296 | 297 | openapi_config = OpenAPIConfig( 298 | title="my api", 299 | version="1.0.0", 300 | components=[jwt_auth.openapi_components], 301 | security=[jwt_auth.security_requirement], 302 | ) 303 | app = Starlite(route_handlers=[], openapi_config=openapi_config) 304 | assert app.openapi_schema.dict(exclude_none=True) == { # type: ignore 305 | "openapi": "3.1.0", 306 | "info": {"title": "my api", "version": "1.0.0"}, 307 | "servers": [{"url": "/"}], 308 | "paths": {}, 309 | "components": { 310 | "securitySchemes": { 311 | "BearerToken": { 312 | "type": "http", 313 | "description": "JWT api-key authentication and authorization.", 314 | "name": "Authorization", 315 | "scheme": "Bearer", 316 | "bearerFormat": "JWT", 317 | } 318 | } 319 | }, 320 | "security": [{"BearerToken": []}], 321 | } 322 | 323 | 324 | def test_oauth2_password_bearer() -> None: 325 | jwt_auth = OAuth2PasswordBearerAuth( # nosec 326 | token_url="/login", token_secret="abc123", retrieve_user_handler=lambda _: None 327 | ) 328 | assert jwt_auth.openapi_components.dict(exclude_none=True) == { 329 | "securitySchemes": { 330 | "BearerToken": { 331 | "type": "oauth2", 332 | "description": "OAUTH2 password bearer authentication and authorization.", 333 | "name": "Authorization", 334 | "scheme": "Bearer", 335 | "bearerFormat": "JWT", 336 | "security_scheme_in": "header", 337 | "flows": {"password": {"tokenUrl": "/login", "scopes": {}}}, 338 | } 339 | } 340 | } 341 | assert jwt_auth.security_requirement == {"BearerToken": []} 342 | 343 | openapi_config = OpenAPIConfig( 344 | title="my api", 345 | version="1.0.0", 346 | components=[jwt_auth.openapi_components], 347 | security=[jwt_auth.security_requirement], 348 | ) 349 | app = Starlite(route_handlers=[], openapi_config=openapi_config) 350 | assert app.openapi_schema.dict(exclude_none=True) == { # type: ignore 351 | "openapi": "3.1.0", 352 | "info": {"title": "my api", "version": "1.0.0"}, 353 | "servers": [{"url": "/"}], 354 | "paths": {}, 355 | "components": { 356 | "securitySchemes": { 357 | "BearerToken": { 358 | "type": "oauth2", 359 | "description": "OAUTH2 password bearer authentication and authorization.", 360 | "name": "Authorization", 361 | "scheme": "Bearer", 362 | "bearerFormat": "JWT", 363 | "security_scheme_in": "header", 364 | "flows": {"password": {"tokenUrl": "/login", "scopes": {}}}, 365 | } 366 | } 367 | }, 368 | "security": [{"BearerToken": []}], 369 | } 370 | -------------------------------------------------------------------------------- /tests/test_token.py: -------------------------------------------------------------------------------- 1 | import string 2 | from datetime import datetime, timedelta, timezone 3 | from typing import Optional 4 | from uuid import uuid4 5 | 6 | import pytest 7 | from hypothesis import given 8 | from hypothesis.strategies import datetimes, none, one_of, sampled_from, text 9 | from starlite import ImproperlyConfiguredException, NotAuthorizedException 10 | 11 | from starlite_jwt import Token 12 | 13 | 14 | @given( 15 | algorithm=sampled_from( 16 | [ 17 | "HS256", 18 | "HS384", 19 | "HS512", 20 | ] 21 | ), 22 | token_sub=text(min_size=1), 23 | token_secret=text(min_size=10), 24 | token_issuer=one_of(none(), text(max_size=256)), 25 | token_audience=one_of(none(), text(max_size=256, alphabet=string.ascii_letters)), 26 | token_unique_jwt_id=one_of(none(), text(max_size=256)), 27 | ) 28 | def test_token( 29 | algorithm: str, 30 | token_sub: str, 31 | token_secret: str, 32 | token_issuer: Optional[str], 33 | token_audience: Optional[str], 34 | token_unique_jwt_id: Optional[str], 35 | ) -> None: 36 | token = Token( 37 | sub=token_sub, 38 | exp=(datetime.now(timezone.utc) + timedelta(seconds=30)), 39 | aud=token_audience, 40 | iss=token_issuer, 41 | jti=token_unique_jwt_id, 42 | ) 43 | encoded_token = token.encode(secret=token_secret, algorithm=algorithm) 44 | decoded_token = token.decode(encoded_token=encoded_token, secret=token_secret, algorithm=algorithm) 45 | assert token.dict() == decoded_token.dict() 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "algorithm, secret", 50 | [ 51 | ( 52 | "nope", 53 | "1", 54 | ), 55 | ( 56 | "HS256", 57 | "", 58 | ), 59 | ( 60 | None, 61 | None, 62 | ), 63 | ( 64 | "HS256", 65 | None, 66 | ), 67 | ( 68 | "", 69 | None, 70 | ), 71 | ( 72 | "", 73 | "", 74 | ), 75 | ( 76 | "", 77 | "1", 78 | ), 79 | ], 80 | ) 81 | def test_encode_validation(algorithm: str, secret: str) -> None: 82 | with pytest.raises(ImproperlyConfiguredException): 83 | Token( 84 | sub="123", 85 | exp=(datetime.now(timezone.utc) + timedelta(seconds=30)), 86 | ).encode(algorithm="nope", secret=secret) 87 | 88 | 89 | def test_decode_validation() -> None: 90 | token = Token( 91 | sub="123", 92 | exp=(datetime.now(timezone.utc) + timedelta(seconds=30)), 93 | ) 94 | algorithm = "HS256" 95 | secret = uuid4().hex 96 | encoded_token = token.encode(algorithm=algorithm, secret=secret) 97 | 98 | token.decode(encoded_token=encoded_token, algorithm=algorithm, secret=secret) 99 | 100 | with pytest.raises(NotAuthorizedException): 101 | token.decode(encoded_token=secret, algorithm=algorithm, secret=secret) 102 | 103 | with pytest.raises(NotAuthorizedException): 104 | token.decode(encoded_token=encoded_token, algorithm="nope", secret=secret) 105 | 106 | with pytest.raises(NotAuthorizedException): 107 | token.decode(encoded_token=encoded_token, algorithm=algorithm, secret=uuid4().hex) 108 | 109 | 110 | @given(exp=datetimes(max_value=datetime.now() - timedelta(seconds=1))) 111 | def test_exp_validation(exp: datetime) -> None: 112 | with pytest.raises(ValueError): # noqa: PT011 113 | Token( 114 | sub="123", 115 | exp=exp, 116 | iat=(datetime.now() - timedelta(seconds=30)), 117 | ) 118 | 119 | 120 | @given(iat=datetimes(min_value=datetime.now() + timedelta(days=1))) 121 | def test_iat_validation(iat: datetime) -> None: 122 | with pytest.raises(ValueError): # noqa: PT011 123 | Token( 124 | sub="123", 125 | iat=iat, 126 | exp=(iat + timedelta(seconds=120)), 127 | ) 128 | --------------------------------------------------------------------------------