├── .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 |
7 |
8 |
9 |
10 |
11 | 
12 | 
13 |
14 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
15 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
16 |
17 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
18 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
19 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
20 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
21 |
22 | [](https://discord.gg/X3FJqy8d2j)
23 | [](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 |
5 |
6 |
7 |
8 |
9 | 
10 | 
11 |
12 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
13 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
14 |
15 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
16 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
17 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
18 | [](https://sonarcloud.io/summary/new_code?id=starlite-api_jwt-auth)
19 |
20 | [](https://discord.gg/X3FJqy8d2j)
21 | [](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 |
--------------------------------------------------------------------------------