Python updoots (#1081)

This commit is contained in:
James Hodgkinson 2022-09-29 10:08:15 +10:00 committed by GitHub
parent 446e06d5f6
commit f0caec57a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1107 additions and 2188 deletions

View file

@ -11,10 +11,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Set up Python 3.9 - name: Set up Python 3.10
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.10'
- name: Running mypy - name: Running mypy
run: | run: |
cd pykanidm cd pykanidm

View file

@ -11,10 +11,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Set up Python 3.9 - name: Set up Python 3.10
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: '3.9' python-version: '3.10'
- name: Running tests - name: Running tests
run: | run: |
cd pykanidm cd pykanidm

View file

@ -9,7 +9,6 @@ jobs:
strategy: strategy:
matrix: matrix:
python_version: python_version:
- "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -15,8 +15,7 @@ uri = "https://idm.example.com"
# ca_path = "/etc/kanidm/cacert.pem" # ca_path = "/etc/kanidm/cacert.pem"
# when configuring the FreeRADIUS server, set the service account details here # when configuring the FreeRADIUS server, set the service account details here
username = "radius_service_account" auth_token = "putyourtokenhere"
password = "cr4bzr0ol"
radius_cert_path = "/certs/cert.pem" # the TLS certificate radius_cert_path = "/certs/cert.pem" # the TLS certificate
radius_key_path = "/certs/key.pem" # the signing key for radius TLS radius_key_path = "/certs/key.pem" # the signing key for radius TLS

View file

@ -40,22 +40,16 @@ RUN chown -R radiusd: /etc/raddb
RUN chmod 775 /etc/raddb/certs RUN chmod 775 /etc/raddb/certs
RUN chmod 640 /etc/raddb/clients.conf RUN chmod 640 /etc/raddb/clients.conf
# install the packages
RUN mkdir -p /pkg/kanidmradius/kanidmradius/
COPY kanidm_rlm_python/kanidmradius/ /pkg/kanidmradius/kanidmradius/
COPY kanidm_rlm_python/pyproject.toml /pkg/kanidmradius/
RUN mkdir -p /pkg/pykanidm/ RUN mkdir -p /pkg/pykanidm/
COPY pykanidm/ /pkg/pykanidm/ COPY pykanidm/ /pkg/pykanidm/
# install the package and its dependencies # install the package and its dependencies
RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/pykanidm RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/pykanidm
RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/kanidmradius
# clean up after install # clean up after install
RUN rm -rf /pkg/* RUN rm -rf /pkg/*
USER radiusd USER radiusd
ENV LD_PRELOAD=/usr/lib64/libpython3.so ENV LD_PRELOAD=/usr/lib64/libpython3.so
COPY kanidm_rlm_python/entrypoint.py /entrypoint.py COPY kanidm_rlm_python/radius_entrypoint.py /radius_entrypoint.py
CMD [ "/usr/bin/python3", "/entrypoint.py" ] CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ]

View file

@ -1,20 +0,0 @@
""" utility functions """
import logging
import sys
from pathlib import Path
from typing import Dict, Any
import toml
def load_config(filename: str="/etc/kanidm/config") -> Dict[str, Any]:
""" loads the configuration file """
config_filepath = Path(filename).expanduser().resolve()
if not config_filepath.exists():
print(f"what {config_filepath}")
logging.error("Failed to find configuration file (%s), quitting!", config_filepath)
sys.exit(1)
config_data: Dict[str, Any] = toml.load(config_filepath.open(encoding="utf-8"))
return config_data

View file

@ -15,7 +15,7 @@ python3 {
# #
python_path="/usr/lib64/python3.8:/usr/lib/python3.8:/usr/lib/python3.8/site-packages:/usr/lib64/python3.8/site-packages:/usr/lib64/python3.8/lib-dynload:/usr/local/lib/python3.8/site-packages:/etc/raddb/mods-config/python3/" python_path="/usr/lib64/python3.8:/usr/lib/python3.8:/usr/lib/python3.8/site-packages:/usr/lib64/python3.8/site-packages:/usr/lib64/python3.8/lib-dynload:/usr/local/lib/python3.8/site-packages:/etc/raddb/mods-config/python3/"
module = kanidmradius module = "kanidm.radius"
# python_path = ${modconfdir}/${.:name} # python_path = ${modconfdir}/${.:name}
# Pass all VPS lists as a 6-tuple to the callbacks # Pass all VPS lists as a 6-tuple to the callbacks

View file

@ -1,957 +0,0 @@
[[package]]
name = "aiohttp"
version = "3.8.3"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
aiosignal = ">=1.1.2"
async-timeout = ">=4.0.0a3,<5.0"
attrs = ">=17.3.0"
charset-normalizer = ">=2.0,<3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
yarl = ">=1.0,<2.0"
[package.extras]
speedups = ["Brotli", "aiodns", "cchardet"]
[[package]]
name = "aiosignal"
version = "1.2.0"
description = "aiosignal: a list of registered asynchronous callbacks"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "astroid"
version = "2.12.9"
description = "An abstract syntax tree for Python with inference support."
category = "dev"
optional = false
python-versions = ">=3.7.2"
[package.dependencies]
lazy-object-proxy = ">=1.4.0"
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
wrapt = [
{version = ">=1.11,<2", markers = "python_version < \"3.11\""},
{version = ">=1.14,<2", markers = "python_version >= \"3.11\""},
]
[[package]]
name = "async-timeout"
version = "4.0.2"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"]
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"]
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"]
[[package]]
name = "certifi"
version = "2022.6.15"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "charset-normalizer"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "colorama"
version = "0.4.5"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dill"
version = "0.3.5.1"
description = "serialize all of python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]]
name = "frozenlist"
version = "1.3.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements_deprecated_finder = ["pip-api", "pipreqs"]
[[package]]
name = "lazy-object-proxy"
version = "1.7.1"
description = "A fast and thorough lazy object proxy."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "multidict"
version = "6.0.2"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mypy"
version = "0.971"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "platformdirs"
version = "2.5.2"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pylint"
version = "2.15.2"
description = "python code static checker"
category = "dev"
optional = false
python-versions = ">=3.7.2"
[package.dependencies]
astroid = ">=2.12.9,<=2.14.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = ">=0.2"
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "7.1.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.19.0"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
pytest = ">=6.1.0"
[package.extras]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]]
name = "requests"
version = "2.28.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7, <4"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = ">=2,<3"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "tomlkit"
version = "0.11.0"
description = "Style preserving TOML library"
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
[[package]]
name = "types-requests"
version = "2.28.10"
description = "Typing stubs for requests"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
types-urllib3 = "<1.27"
[[package]]
name = "types-toml"
version = "0.10.8"
description = "Typing stubs for toml"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "types-urllib3"
version = "1.26.15"
description = "Typing stubs for urllib3"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "typing-extensions"
version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "urllib3"
version = "1.26.9"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "wrapt"
version = "1.14.1"
description = "Module for decorators, wrappers and monkey patching."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "yarl"
version = "1.7.2"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "1a9398d12050b9f81addcf4db2a59a71fb865e8118c479c7114fbf8860ed4d62"
[metadata.files]
aiohttp = [
{file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"},
{file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"},
{file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"},
{file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"},
{file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"},
{file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"},
{file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"},
{file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"},
{file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"},
{file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"},
{file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"},
{file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"},
{file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"},
{file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"},
{file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"},
{file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"},
{file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"},
{file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"},
{file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"},
{file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"},
{file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"},
{file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"},
{file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"},
{file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"},
{file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"},
{file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"},
{file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"},
{file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"},
{file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"},
{file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"},
{file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"},
{file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"},
{file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"},
]
aiosignal = [
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
]
astroid = [
{file = "astroid-2.12.9-py3-none-any.whl", hash = "sha256:27a22f40e45af6d362498647a0940e8ae9c35f71cb572a1b6f8f810122a11918"},
{file = "astroid-2.12.9.tar.gz", hash = "sha256:0dafbfcf4ebdecd3c8f6d742c9d9c88508229ca823d5c98ab872d964f3321e56"},
]
async-timeout = [
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
certifi = [
{file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
{file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
dill = [
{file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"},
{file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"},
]
frozenlist = [
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"},
{file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"},
{file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"},
{file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"},
{file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"},
{file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"},
{file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"},
]
mccabe = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
multidict = [
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
]
mypy = [
{file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"},
{file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"},
{file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"},
{file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"},
{file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"},
{file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"},
{file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"},
{file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"},
{file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"},
{file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"},
{file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"},
{file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"},
{file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"},
{file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"},
{file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"},
{file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"},
{file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"},
{file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"},
{file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"},
{file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"},
{file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pylint = [
{file = "pylint-2.15.2-py3-none-any.whl", hash = "sha256:cc3da458ba810c49d330e09013dec7ced5217772dec8f043ccdd34dae648fde8"},
{file = "pylint-2.15.2.tar.gz", hash = "sha256:f63404a2547edb5247da263748771ac9a806ed1de4174cda01293c08ddbc2999"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
]
pytest-asyncio = [
{file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"},
{file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"},
]
requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
tomlkit = [
{file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"},
{file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"},
]
types-requests = [
{file = "types-requests-2.28.10.tar.gz", hash = "sha256:97d8f40aa1ffe1e58c3726c77d63c182daea9a72d9f1fa2cafdea756b2a19f2c"},
{file = "types_requests-2.28.10-py3-none-any.whl", hash = "sha256:45b485725ed58752f2b23461252f1c1ad9205b884a1e35f786bb295525a3e16a"},
]
types-toml = [
{file = "types-toml-0.10.8.tar.gz", hash = "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a"},
{file = "types_toml-0.10.8-py3-none-any.whl", hash = "sha256:8300fd093e5829eb9c1fba69cee38130347d4b74ddf32d0a7df650ae55c2b599"},
]
types-urllib3 = [
{file = "types-urllib3-1.26.15.tar.gz", hash = "sha256:c89283541ef92e344b7f59f83ea9b5a295b16366ceee3f25ecfc5593c79f794e"},
{file = "types_urllib3-1.26.15-py3-none-any.whl", hash = "sha256:6011befa13f901fc934f59bb1fd6973be6f3acf4ebfce427593a27e7f492918f"},
]
typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
]
urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
]
wrapt = [
{file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"},
{file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"},
{file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"},
{file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"},
{file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"},
{file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"},
{file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"},
{file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"},
{file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"},
{file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"},
{file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"},
{file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"},
{file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"},
{file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"},
{file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"},
{file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"},
{file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"},
{file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"},
{file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"},
{file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"},
{file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"},
{file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"},
{file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"},
{file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"},
{file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"},
{file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"},
{file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"},
{file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"},
{file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"},
{file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"},
{file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"},
{file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
]

View file

@ -1,31 +0,0 @@
[tool.poetry]
name = "kanidmradius"
version = "0.0.1"
description = "FreeRADIUS Module for Kanidm Authentication"
authors = [
"James Hodgkinson <james@terminaloutcomes.com>"
]
[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.28.1"
toml = "^0.10.2"
aiohttp = "^3.8.3"
[tool.poetry.dev-dependencies]
pylint = "^2.15.2"
mypy = "^0.971"
types-requests = "^2.28.10"
pytest = "^7.1.3"
types-toml = "^0.10.8"
pytest-asyncio = "^0.19.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.pylint.MASTER]
disable="W0511"
[tool.pytest.ini_options]
asyncio_mode = "auto"

View file

@ -19,6 +19,7 @@ echo "Starting the dev container..."
docker run --rm -it \ docker run --rm -it \
--network host \ --network host \
--name radiusd \ --name radiusd \
-v /tmp/kanidm/:/certs/ \ -v /tmp/kanidm/:/data/ \
-v /tmp/kanidm/:/tmp/kanidm/ \
-v "${CONFIG_FILE}:/data/kanidm" \ -v "${CONFIG_FILE}:/data/kanidm" \
${IMAGE} $@ ${IMAGE} $@

View file

@ -1,42 +0,0 @@
""" tests the check_vlan function """
from typing import Any
import aiohttp
import pytest
from kanidm import KanidmClient
from kanidm.types import KanidmClientConfig
from kanidmradius import check_vlan
@pytest.mark.asyncio
async def test_check_vlan(event_loop: Any) -> None:
""" test 1 """
async with aiohttp.ClientSession(loop=event_loop) as session:
testconfig = KanidmClientConfig.parse_toml("""
uri='https://kanidm.example.com'
radius_groups = [
{ name = "crabz", "vlan" = 1234 },
{ name = "hello world", "vlan" = 12345 },
]
""")
print(f"{testconfig=}")
kanidm_client = KanidmClient(
config = testconfig,
session=session,
)
print(f"{kanidm_client.config=}")
assert check_vlan(
acc=12345678,
group={'name' : 'crabz'},
kanidm_client=kanidm_client
) == 1234
assert check_vlan(
acc=12345678,
group={'name' : 'foo'},
kanidm_client=kanidm_client
) == 12345678

View file

@ -700,7 +700,7 @@ impl AuthEventStep {
mech, mech,
})), })),
None => Err(OperationError::InvalidAuthState( None => Err(OperationError::InvalidAuthState(
"session id not present in cred".to_string(), "session id not present in cred presented to 'begin' step".to_string(),
)), )),
}, },
AuthStep::Cred(cred) => match sid { AuthStep::Cred(cred) => match sid {
@ -709,7 +709,7 @@ impl AuthEventStep {
cred, cred,
})), })),
None => Err(OperationError::InvalidAuthState( None => Err(OperationError::InvalidAuthState(
"session id not present in cred".to_string(), "session id not present in cred to 'cred' step".to_string(),
)), )),
}, },
} }

View file

@ -342,6 +342,7 @@ pub async fn person_get(req: tide::Request<AppState>) -> tide::Result {
json_rest_event_get(req, filter, None).await json_rest_event_get(req, filter, None).await
} }
// expects the following fields in the attrs field of the req: [name, displayname]
pub async fn person_post(req: tide::Request<AppState>) -> tide::Result { pub async fn person_post(req: tide::Request<AppState>) -> tide::Result {
let classes = vec![ let classes = vec![
"person".to_string(), "person".to_string(),

View file

@ -14,6 +14,11 @@ python -m pip install kanidm
Documentation can be generated by [cloning the repository](https://github.com/kanidm/kanidm) and running `make docs/pykanidm/build`. The documentation will appear in `./pykanidm/site`. You'll need make and the [poetry](https://pypi.org/project/poetry/) package installed. Documentation can be generated by [cloning the repository](https://github.com/kanidm/kanidm) and running `make docs/pykanidm/build`. The documentation will appear in `./pykanidm/site`. You'll need make and the [poetry](https://pypi.org/project/poetry/) package installed.
## Testing
Set up your dev environment using `poetry` - `python -m pip install poetry && poetry install`.
Pytest it used for testing, if you don't have a live server to test against and config set up, use `poetry run pytest -m 'not network'`.
## Changelog ## Changelog

View file

@ -1,3 +1,4 @@
# kanidm.KanidmClient
::: kanidm.KanidmClient ::: kanidm.KanidmClient

View file

@ -1,3 +1,4 @@
# kanidm.types.KanidmClientConfig
::: kanidm.types.KanidmClientConfig ::: kanidm.types.KanidmClientConfig

View file

@ -1,3 +1,4 @@
# kanidm.types.RadiusClient
::: kanidm.types.RadiusClient ::: kanidm.types.RadiusClient

View file

@ -0,0 +1,3 @@
::: kanidm.tokens

View file

@ -1,14 +1,14 @@
""" Kanidm python module """ """ Kanidm python module """
from json import dumps, loads, JSONDecodeError from functools import lru_cache
import json as json_lib
import logging import logging
from pathlib import Path from pathlib import Path
import ssl import ssl
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Union
from pydantic import ValidationError
import aiohttp import aiohttp
from pydantic import ValidationError
from .exceptions import ( from .exceptions import (
AuthBeginFailed, AuthBeginFailed,
@ -28,8 +28,12 @@ from .utils import load_config
KANIDMURLS = { KANIDMURLS = {
"auth": "/v1/auth", "auth": "/v1/auth",
"person": "/v1/person",
"service_account": "/v1/person",
} }
TOKEN_PATH = Path("~/.cache/kanidm_tokens")
class KanidmClient: class KanidmClient:
"""Kanidm client module """Kanidm client module
@ -41,6 +45,7 @@ class KanidmClient:
verify_hostnames: verify the hostname is correct verify_hostnames: verify the hostname is correct
verify_certificate: verify the validity of the certificate and its CA verify_certificate: verify the validity of the certificate and its CA
ca_path: set this to a trusted CA certificate (PEM format) ca_path: set this to a trusted CA certificate (PEM format)
token: a JWS from an authentication session
""" """
# pylint: disable=too-many-instance-attributes,too-many-arguments # pylint: disable=too-many-instance-attributes,too-many-arguments
@ -49,10 +54,10 @@ class KanidmClient:
config: Optional[KanidmClientConfig] = None, config: Optional[KanidmClientConfig] = None,
config_file: Optional[Union[Path, str]] = None, config_file: Optional[Union[Path, str]] = None,
uri: Optional[str] = None, uri: Optional[str] = None,
session: Optional[aiohttp.client.ClientSession] = None,
verify_hostnames: bool = True, verify_hostnames: bool = True,
verify_certificate: bool = True, verify_certificate: bool = True,
ca_path: Optional[str] = None, ca_path: Optional[str] = None,
token: Optional[str] = None,
) -> None: ) -> None:
"""Constructor for KanidmClient""" """Constructor for KanidmClient"""
@ -65,6 +70,7 @@ class KanidmClient:
verify_hostnames=verify_hostnames, verify_hostnames=verify_hostnames,
verify_certificate=verify_certificate, verify_certificate=verify_certificate,
ca_path=ca_path, ca_path=ca_path,
auth_token=token,
) )
if config_file is not None: if config_file is not None:
@ -73,9 +79,6 @@ class KanidmClient:
config_data = load_config(config_file.expanduser().resolve()) config_data = load_config(config_file.expanduser().resolve())
self.config = self.config.parse_obj(config_data) self.config = self.config.parse_obj(config_data)
self.session = session
self.sessionid: Optional[str] = None
if self.config.uri is None: if self.config.uri is None:
raise ValueError("Please intitialize this with a server URI") raise ValueError("Please intitialize this with a server URI")
@ -87,6 +90,11 @@ class KanidmClient:
if self.config.verify_certificate is False: if self.config.verify_certificate is False:
self._ssl = False self._ssl = False
else: else:
if (
self.config.ca_path is not None
and not Path(self.config.ca_path).expanduser().resolve().exists()
):
raise FileNotFoundError(f"CA Path not found: {self.config.ca_path}")
self._ssl = ssl.create_default_context(cafile=self.config.ca_path) self._ssl = ssl.create_default_context(cafile=self.config.ca_path)
if self._ssl is not False: if self._ssl is not False:
# ignoring this for typing because mypy is being weird # ignoring this for typing because mypy is being weird
@ -102,14 +110,39 @@ class KanidmClient:
try: try:
self.config.parse_obj(config_data) self.config.parse_obj(config_data)
except ValidationError as validation_error: except ValidationError as validation_error:
# pylint: disable=raise-missing-from
raise ValueError(f"Failed to validate configuration: {validation_error}") raise ValueError(f"Failed to validate configuration: {validation_error}")
async def check_token_valid(self, token: Optional[str] = None) -> bool:
"""checks if a given token is valid, or the local one if you don't pass it"""
url = "/v1/auth/valid"
if token is not None:
headers = {
"authorization": f"Bearer {token}",
"content-type": "application/json",
}
else:
headers = None
result = await self.call_get(url, headers=headers)
logging.debug(result)
if result.status_code == 200:
return True
return False
@lru_cache()
def get_path_uri(self, path: str) -> str: def get_path_uri(self, path: str) -> str:
"""turns a path into a full URI""" """turns a path into a full URI"""
if path.startswith("/"): if path.startswith("/"):
path = path[1:] path = path[1:]
return f"{self.config.uri}{path}" return f"{self.config.uri}{path}"
@property
def _token_headers(self) -> Dict[str, str]:
"""returns an auth header with the token in it"""
if self.config.auth_token is None:
raise ValueError("Token is not set")
return {"authorization": f"Bearer {self.config.auth_token}"}
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
async def _call( async def _call(
self, self,
@ -118,46 +151,58 @@ class KanidmClient:
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None, timeout: Optional[int] = None,
json: Optional[Dict[str, str]] = None, json: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, str]] = None,
) -> ClientResponse: ) -> ClientResponse:
if timeout is None: if timeout is None:
timeout = self.config.connect_timeout timeout = self.config.connect_timeout
if self.session is None: async with aiohttp.client.ClientSession() as session:
self.session = aiohttp.client.ClientSession() # if we have a token set, we send it.
async with self.session.request( if self.config.auth_token is not None:
method=method, logging.debug("Found a token internally")
url=self.get_path_uri(path), if headers is None:
headers=headers, headers = self._token_headers
timeout=timeout, elif "authorization" not in headers:
json=json, logging.debug("Setting auth headers as Authorization not in keys")
ssl=self._ssl, headers.update(self._token_headers)
) as request: logging.debug("_call method=%s to %s", method, self.get_path_uri(path))
content = await request.content.read() async with session.request(
try: method=method,
response_json = loads(content) url=self.get_path_uri(path),
if not isinstance(response_json, dict): headers=headers,
response_json = None timeout=timeout,
except JSONDecodeError as json_error: json=json,
logging.error("Failed to JSON Decode Response: %s", json_error) params=params,
response_json = {} ssl=self._ssl,
response_input = { ) as request:
"data": response_json, content = await request.content.read()
"content": content.decode("utf-8"), try:
"headers": request.headers, response_json = json_lib.loads(content)
"status_code": request.status, if not isinstance(response_json, dict):
} response_json = None
logging.debug(dumps(response_input, default=str, indent=4)) except json_lib.JSONDecodeError as json_error:
response = ClientResponse.parse_obj(response_input) logging.error("Failed to JSON Decode Response: %s", json_error)
return response logging.error("Response data: %s", content)
response_json = {}
response_input = {
"data": response_json,
"content": content.decode("utf-8"),
"headers": request.headers,
"status_code": request.status,
}
logging.debug(json_lib.dumps(response_input, default=str, indent=4))
response = ClientResponse.parse_obj(response_input)
return response
async def call_get( async def call_get(
self, self,
path: str, path: str,
headers: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, str]] = None,
timeout: Optional[int] = None, timeout: Optional[int] = None,
) -> ClientResponse: ) -> ClientResponse:
"""does a get call to the server""" """does a get call to the server"""
return await self._call("GET", path, headers, timeout) return await self._call("GET", path, headers, timeout, params=params)
async def call_post( async def call_post(
self, self,
@ -185,7 +230,7 @@ class KanidmClient:
"Failed to authenticate, response from server: %s", "Failed to authenticate, response from server: %s",
response.content, response.content,
) )
# TODO: mock test this # TODO: mock test auth_init raises AuthInitFailed
raise AuthInitFailed(response.content) raise AuthInitFailed(response.content)
if "x-kanidm-auth-session-id" not in response.headers: if "x-kanidm-auth-session-id" not in response.headers:
@ -194,31 +239,27 @@ class KanidmClient:
raise ValueError( raise ValueError(
f"Missing x-kanidm-auth-session-id header in init auth response: {response.headers}" f"Missing x-kanidm-auth-session-id header in init auth response: {response.headers}"
) )
# TODO: setting the class-local session id, do we want this?
self.sessionid = response.headers["x-kanidm-auth-session-id"]
retval = AuthInitResponse.parse_obj(response.data) retval = AuthInitResponse.parse_obj(response.data)
retval.response = response retval.response = response
return retval return retval
async def auth_begin( async def auth_begin(self, method: str, sessionid: str) -> ClientResponse:
self,
method: str = "password", # TODO: do we want a default auth mech to be set?
) -> ClientResponse:
"""the 'begin' step""" """the 'begin' step"""
begin_auth = { begin_auth = {
"step": { "step": {
"begin": method, "begin": method,
} },
} }
headers = self.session_header(sessionid)
response = await self.call_post( response = await self.call_post(
KANIDMURLS["auth"], KANIDMURLS["auth"],
json=begin_auth, json=begin_auth,
headers=self.session_header(), headers=headers,
) )
if response.status_code != 200: if response.status_code != 200:
# TODO: write mocked test for this # TODO: mock test for auth_begin raises AuthBeginFailed
raise AuthBeginFailed(response.content) raise AuthBeginFailed(response.content)
retobject = AuthBeginResponse.parse_obj(response.data) retobject = AuthBeginResponse.parse_obj(response.data)
@ -233,6 +274,7 @@ class KanidmClient:
"""authenticates with a username and password, returns the auth token""" """authenticates with a username and password, returns the auth token"""
if username is None and password is None: if username is None and password is None:
if self.config.username is None or self.config.password is None: if self.config.username is None or self.config.password is None:
# pylint: disable=line-too-long
raise ValueError( raise ValueError(
"Need username/password to be in caller or class settings before calling authenticate_password" "Need username/password to be in caller or class settings before calling authenticate_password"
) )
@ -241,22 +283,26 @@ class KanidmClient:
if username is None or password is None: if username is None or password is None:
raise ValueError("Username and Password need to be set somewhere!") raise ValueError("Username and Password need to be set somewhere!")
auth_init = await self.auth_init(username) auth_init: AuthInitResponse = await self.auth_init(username)
if auth_init.response is None:
raise NotImplementedError("This should throw a really cool response")
sessionid = auth_init.response.headers["x-kanidm-auth-session-id"]
if len(auth_init.state.choose) == 0: if len(auth_init.state.choose) == 0:
# there's no mechanisms at all - bail # there's no mechanisms at all - bail
# TODO: write test coverage for this # TODO: write test coverage for authenticate_password raises AuthMechUnknown
raise AuthMechUnknown(f"No auth mechanisms for {username}") raise AuthMechUnknown(f"No auth mechanisms for {username}")
auth_begin = await self.auth_begin( auth_begin = await self.auth_begin(method="password", sessionid=sessionid)
method="password",
)
# does a little bit of validation # does a little bit of validation
auth_begin_object = AuthBeginResponse.parse_obj(auth_begin.data) auth_begin_object = AuthBeginResponse.parse_obj(auth_begin.data)
auth_begin_object.response = auth_begin auth_begin_object.response = auth_begin
return await self.auth_step_password(password=password) return await self.auth_step_password(password=password, sessionid=sessionid)
async def auth_step_password( async def auth_step_password(
self, self,
sessionid: str,
password: Optional[str] = None, password: Optional[str] = None,
) -> AuthStepPasswordResponse: ) -> AuthStepPasswordResponse:
"""does the password auth step""" """does the password auth step"""
@ -270,17 +316,16 @@ class KanidmClient:
cred_auth = {"step": {"cred": {"password": password}}} cred_auth = {"step": {"cred": {"password": password}}}
response = await self.call_post( response = await self.call_post(
path="/v1/auth", path="/v1/auth", json=cred_auth, headers=self.session_header(sessionid)
json=cred_auth,
) )
if response.status_code != 200: if response.status_code != 200:
# TODO: write test coverage for this # TODO: write test coverage auth_step_password raises AuthCredFailed
logging.debug("Failed to authenticate, response: %s", response.content) logging.debug("Failed to authenticate, response: %s", response.content)
raise AuthCredFailed("Failed password authentication!") raise AuthCredFailed("Failed password authentication!")
result = AuthStepPasswordResponse.parse_obj(response.data) result = AuthStepPasswordResponse.parse_obj(response.data)
result.response = response result.response = response
print(f"auth_step_password: {result.dict()}")
# pull the token out and set it # pull the token out and set it
if result.state.success is None: if result.state.success is None:
@ -291,34 +336,18 @@ class KanidmClient:
def session_header( def session_header(
self, self,
sessionid: Optional[str] = None, sessionid: str,
) -> Dict[str, str]: ) -> Dict[str, str]:
"""create a headers dict from a session id""" """create a headers dict from a session id"""
# TODO: perhaps allow session_header to take a dict and update it, too? # TODO: perhaps allow session_header to take a dict and update it, too?
return {
"X-KANIDM-AUTH-SESSION-ID": sessionid,
}
if sessionid is not None: async def get_radius_token(self, username: str) -> ClientResponse:
return {
"X-KANIDM-AUTH-SESSION-ID": sessionid,
}
if self.sessionid is not None:
return {
"X-KANIDM-AUTH-SESSION-ID": self.sessionid,
}
raise ValueError("Class doesn't have a sessionid stored and none was provided")
async def get_radius_token(
self, username: str, radius_session_id: str
) -> ClientResponse:
"""does the call to the radius token endpoint""" """does the call to the radius token endpoint"""
path = f"/v1/account/{username}/_radius/_token" path = f"/v1/account/{username}/_radius/_token"
headers = { response = await self.call_get(path)
"Authorization": f"Bearer {radius_session_id}",
}
response = await self.call_get(
path,
headers,
)
if response.status_code == 404: if response.status_code == 404:
raise NoMatchingEntries( raise NoMatchingEntries(
f"No user found: '{username}' {response.headers['x-kanidm-opid']}" f"No user found: '{username}' {response.headers['x-kanidm-opid']}"

View file

@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Union
import aiohttp import aiohttp
from kanidm import KanidmClient from kanidm import KanidmClient
from kanidm.types import AuthStepPasswordResponse from kanidm.types import AuthStepPasswordResponse, RadiusTokenGroup, RadiusTokenResponse
from kanidm.utils import load_config from kanidm.utils import load_config
from kanidm.exceptions import NoMatchingEntries from kanidm.exceptions import NoMatchingEntries
@ -21,13 +21,14 @@ from . import radiusd
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
stream=sys.stderr, stream=sys.stderr,
) )
# the list of places to try # the list of places to try
config_paths = [ config_paths = [
os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness
"~/.config/kanidm", # for a user "~/.config/kanidm", # for a user
"/etc/kanidm/kanidm", # system-wide "/etc/kanidm/kanidm", # system-wide
"../examples/kanidm", # test mode
] ]
CONFIG_PATH = None CONFIG_PATH = None
@ -37,48 +38,41 @@ for config_file_path in config_paths:
break break
if (CONFIG_PATH is None) or (not CONFIG_PATH.exists()): if (CONFIG_PATH is None) or (not CONFIG_PATH.exists()):
logging.error("Failed to find configuration file, checked (%s), quitting!", config_paths) logging.error(
"Failed to find configuration file, checked (%s), quitting!", config_paths
)
sys.exit(1) sys.exit(1)
config = load_config(str(CONFIG_PATH)) config = load_config(str(CONFIG_PATH))
COOKIE_JAR = aiohttp.CookieJar()
KANIDM_CLIENT = KanidmClient(config_file=CONFIG_PATH) KANIDM_CLIENT = KanidmClient(config_file=CONFIG_PATH)
def authenticate( def authenticate(
acct: str, acct: str,
password: str, password: str,
kanidm_client: KanidmClient = KANIDM_CLIENT, kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Union[int, AuthStepPasswordResponse]: ) -> Union[int, AuthStepPasswordResponse]:
""" authenticate the RADIUS service account to Kanidm """ """authenticate the RADIUS service account to Kanidm"""
logging.error("authenticate - %s:%s", acct, password) logging.error("authenticate - %s:%s", acct, password)
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
with aiohttp.client.ClientSession(cookie_jar=COOKIE_JAR) as session: return loop.run_until_complete(kanidm_client.check_token_valid())
kanidm_client.session = session except Exception as error_message: # pylint: disable=broad-except
return loop.run_until_complete(kanidm_client.authenticate_password(
username=acct,
password=password
))
except Exception as error_message: #pylint: disable=broad-except
logging.error("Failed to run kanidm.authenticate_password: %s", error_message) logging.error("Failed to run kanidm.authenticate_password: %s", error_message)
return radiusd.RLM_MODULE_FAIL return radiusd.RLM_MODULE_FAIL
async def _get_radius_token( async def _get_radius_token(
username: Optional[str]=None, username: Optional[str] = None,
kanidm_client: KanidmClient=KANIDM_CLIENT, kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
"""pulls the radius token for a client username"""
if username is None: if username is None:
raise ValueError("Didn't get a username for _get_radius_token") raise ValueError("Didn't get a username for _get_radius_token")
# authenticate as the radius service account # authenticate as the radius service account
logging.debug("Authenticating kanidm radius service account")
radius_auth_response = await kanidm_client.authenticate_password()
logging.debug("Getting RADIUS token for %s", username) logging.debug("Getting RADIUS token for %s", username)
response = await kanidm_client.get_radius_token( response = await kanidm_client.get_radius_token(username=username)
username=username,
radius_session_id = radius_auth_response.sessionid,
)
logging.debug("Got radius token for %s", username) logging.debug("Got radius token for %s", username)
if response.status_code != 200: if response.status_code != 200:
@ -86,17 +80,18 @@ async def _get_radius_token(
logging.error("Response content: %s", response.json()) logging.error("Response content: %s", response.json())
raise Exception("Failed to get RadiusAuthToken") raise Exception("Failed to get RadiusAuthToken")
logging.debug("Success getting RADIUS token: %s", response.json()) logging.debug("Success getting RADIUS token: %s", response.json())
print(response.data) logging.debug(response.data)
return response.data return response.data
def check_vlan( def check_vlan(
acc: int, acc: int,
group: Dict[str, str], group: RadiusTokenGroup,
kanidm_client: Optional[KanidmClient] = None, kanidm_client: Optional[KanidmClient] = None,
) -> int: ) -> int:
""" checks if a vlan is in the config, """checks if a vlan is in the config,
acc is the default vlan acc is the default vlan
""" """
logging.debug("acc=%s", acc) logging.debug("acc=%s", acc)
if kanidm_client is None: if kanidm_client is None:
@ -104,28 +99,29 @@ def check_vlan(
# raise ValueError("Need to pass this a kanidm_client") # raise ValueError("Need to pass this a kanidm_client")
for radius_group in kanidm_client.config.radius_groups: for radius_group in kanidm_client.config.radius_groups:
logging.debug("Checking '%s' radius_group against group %s", radius_group, group['name']) group_name = group.spn.split("@")[0]
if radius_group.name == group['name']: logging.debug(
"Checking '%s' radius_group against group %s", radius_group, group_name
)
if radius_group.name == group_name:
return radius_group.vlan return radius_group.vlan
#if CONFIG.has_section(f"group.{group['name']}"):
# if CONFIG.has_option(f"group.{group['name']}", "vlan"):
# vlan = CONFIG.getint(f"group.{group['name']}", "vlan")
# logging.debug("assigning vlan %s from group %s", vlan, group)
# return vlan
logging.debug("returning default vlan: %s", acc) logging.debug("returning default vlan: %s", acc)
return acc return acc
def instantiate(_: Any) -> Any: def instantiate(_: Any) -> Any:
""" start up radiusd """ """start up radiusd"""
logging.info("Starting up!") logging.info("Starting up!")
return radiusd.RLM_MODULE_OK return radiusd.RLM_MODULE_OK
# pylint: disable=too-many-locals
def authorize( def authorize(
args: Any=Dict[Any,Any], args: Any = Dict[Any, Any],
kanidm_client: KanidmClient=KANIDM_CLIENT, kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Any: ) -> Any:
""" does the kanidm authorize step """ """does the kanidm authorize step"""
logging.info('kanidm python module called') logging.info("kanidm python module called")
# args comes in like this # args comes in like this
# ( # (
# ('User-Name', '<username>'), # ('User-Name', '<username>'),
@ -138,8 +134,8 @@ def authorize(
dargs = dict(args) dargs = dict(args)
logging.error("Authorise: %s", json.dumps(dargs)) logging.error("Authorise: %s", json.dumps(dargs))
cn_uuid = dargs.get('TLS-Client-Cert-Common-Name', None) cn_uuid = dargs.get("TLS-Client-Cert-Common-Name", None)
username = dargs['User-Name'] username = dargs["User-Name"]
if cn_uuid is not None: if cn_uuid is not None:
logging.debug("Using TLS-Client-Cert-Common-Name") logging.debug("Using TLS-Client-Cert-Common-Name")
@ -151,59 +147,59 @@ def authorize(
tok = None tok = None
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
tok = loop.run_until_complete(_get_radius_token(username=user_id)) tok = RadiusTokenResponse.parse_obj(
loop.run_until_complete(_get_radius_token(username=user_id))
)
logging.debug("radius_token: %s", tok) logging.debug("radius_token: %s", tok)
except NoMatchingEntries as error_message: except NoMatchingEntries as error_message:
logging.info( logging.info(
'kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user_id %s: %s', "kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user_id %s: %s",
user_id, user_id,
error_message, error_message,
) )
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
except Exception as error_message: # pylint: disable=broad-except except Exception as error_message: # pylint: disable=broad-except
logging.error("kanidm exception: %s, %s", type(error_message), error_message) logging.error("kanidm exception: %s, %s", type(error_message), error_message)
if tok is None: if tok is None:
logging.info('kanidm RLM_MODULE_NOTFOUND due to no auth token') logging.info("kanidm RLM_MODULE_NOTFOUND due to no auth token")
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
# Get values out of the token # Get values out of the token
name = tok["name"] name = tok.name
secret = tok["secret"] secret = tok.secret
uuid = tok["uuid"] uuid = tok.uuid
# Are they in the required group? # Are they in the required group?
req_sat = False req_sat = False
for group in tok["groups"]: for group in tok.groups:
if group['name'] in kanidm_client.config.radius_required_groups: group_name = group.spn.split("@")[0]
if group_name in kanidm_client.config.radius_required_groups:
req_sat = True req_sat = True
logging.info("User %s has a required group (%s)", name, group['name']) logging.info("User %s has a required group (%s)", name, group_name)
if req_sat is not True: if req_sat is not True:
logging.info("User %s doesn't have a group from the required list.", name) logging.info("User %s doesn't have a group from the required list.", name)
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
# look up them in config for group vlan if possible. # look up them in config for group vlan if possible.
#TODO: work out the typing on this, WTF. # TODO: work out the typing on this, WTF.
uservlan: int = reduce( uservlan: int = reduce(
check_vlan, check_vlan,
tok["groups"], tok.groups,
kanidm_client.config.radius_default_vlan, kanidm_client.config.radius_default_vlan,
) )
if uservlan == int(0): if uservlan == int(0):
logging.info("Invalid uservlan of 0") logging.info("Invalid uservlan of 0")
logging.info("selected vlan %s:%s", name, uservlan) logging.info("selected vlan %s:%s", name, uservlan)
reply = ( reply = (
('User-Name', str(name)), ("User-Name", str(name)),
('Reply-Message', f"Kanidm-Uuid: {uuid}"), ("Reply-Message", f"Kanidm-Uuid: {uuid}"),
('Tunnel-Type', '13'), ("Tunnel-Type", "13"),
('Tunnel-Medium-Type', '6'), ("Tunnel-Medium-Type", "6"),
('Tunnel-Private-Group-ID', str(uservlan)), ("Tunnel-Private-Group-ID", str(uservlan)),
)
config_object = (
('Cleartext-Password', str(secret)),
) )
config_object = (("Cleartext-Password", str(secret)),)
logging.info("OK! Returning details to radius for %s ...", name) logging.info("OK! Returning details to radius for %s ...", name)
return (radiusd.RLM_MODULE_OK, reply, config_object) return (radiusd.RLM_MODULE_OK, reply, config_object)

187
pykanidm/kanidm/tokens.py Normal file
View file

@ -0,0 +1,187 @@
""" User Auth Token related widgets """
# pylint: disable=too-few-public-methods
import base64
from datetime import datetime, timedelta, timezone
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from authlib.jose import JsonWebSignature # type: ignore
from pydantic import BaseModel
from . import TOKEN_PATH
class JWSHeader(BaseModel):
"""JWS Header Parser"""
class JWSHeaderJWK(BaseModel):
"""JWS Header Sub-bit"""
kty: str
crv: str
x: str
y: str
alg: str
use: str
alg: str
typ: str
jwk: JWSHeaderJWK
class Config:
"""Configure the pydantic class"""
arbitrary_types_allowed = True
class JWSPayload(BaseModel):
"""JWS Payload parser"""
session_id: str
auth_type: str
# TODO: work out the format of the expiry
# example expiry: 2022,265,28366,802525000
expiry: List[int] # [year, day of year, something?]
uuid: str
name: str
displayname: str
spn: str
mail_primary: Optional[str]
lim_uidx: bool
lim_rmax: int
lim_pmax: int
lim_fmax: int
@property
def expiry_datetime(self) -> datetime:
"""parse the expiry and return a datetime object"""
year, day, seconds, _ = self.expiry
retval = datetime(
year=year, month=1, day=1, second=0, hour=0, tzinfo=timezone.utc
)
# day - 1 because we're already starting at day 1
retval += timedelta(days=day - 1, seconds=seconds)
return retval
class JWS:
"""JWS parser"""
def __init__(self, raw: str) -> None:
"""raw is the raw string version of the JWS"""
data = self.parse(raw)
self.header = data[0]
self.payload = data[1]
self.signature = data[2]
@classmethod
def parse(cls, raw: str) -> Tuple[JWSHeader, JWSPayload, bytes]:
"""parse a raw JWS"""
if "." not in raw:
raise ValueError("Invalid number of segments, there's no . in the raw JWS")
split_raw = raw.split(".")
if len(split_raw) != 3:
raise ValueError("Invalid number of segments")
raw_header = split_raw[0]
logging.debug("Parsing header: %s", raw_header)
padded_header = raw_header + "=" * divmod(len(raw_header), 4)[0]
decoded_header = base64.urlsafe_b64decode(padded_header)
logging.debug("decoded_header=%s", decoded_header)
header = JWSHeader.parse_obj(json.loads(decoded_header.decode("utf-8")))
logging.debug("header: %s", header)
raw_payload = split_raw[1]
logging.debug("Parsing payload: %s", raw_payload)
padded_payload = raw_payload + "=" * divmod(len(raw_payload), 4)[1]
payload = JWSPayload.parse_raw(base64.urlsafe_b64decode(padded_payload))
raw_signature = split_raw[2]
logging.debug("Parsing signature: %s", raw_signature)
padded_signature = raw_signature + "=" * divmod(len(raw_signature), 4)[1]
signature = base64.urlsafe_b64decode(padded_signature)
return header, payload, signature
class TokenStore(BaseModel):
"""Represents the user auth tokens, can load them from the user store"""
__root__: Dict[str, str] = {}
# TODO: one day work out how to type the __iter__ on TokenStore properly. It's some kind of iter() that makes mypy unhappy.
def __iter__(self) -> Any:
"""overloading the default function"""
for key in self.__root__.keys():
yield key
def __getitem__(self, item: str) -> str:
"""overloading the default function"""
return self.__root__[item]
def __delitem__(self, item: str) -> None:
"""overloading the default function"""
del self.__root__[item]
def __setitem__(self, key: str, value: str) -> None:
"""overloading the default function"""
self.__root__[key] = value
def save(self, filepath: Path = TOKEN_PATH) -> None:
"""saves the cached tokens to disk"""
data = json.dumps(self.__root__, indent=2)
with filepath.expanduser().resolve().open(
mode="w", encoding="utf-8"
) as file_handle:
file_handle.write(data)
def load(
self, overwrite: bool = True, filepath: Path = TOKEN_PATH
) -> Dict[str, str]:
"""Loads the tokens from from the store and caches them in memory - by default
from the local user's store path, but you can point it at any file path.
Will return the current cached store.
If overwrite=False, then it will add them to the existing in-memory store"""
token_path = filepath.expanduser().resolve()
if not token_path.exists():
tokens: Dict[str, str] = {}
else:
with token_path.open(encoding="utf-8") as file_handle:
tokens = json.load(file_handle)
if overwrite:
self.__root__ = tokens
else:
for user in tokens:
self.__root__[user] = tokens[user]
self.validate_tokens()
logging.debug(json.dumps(tokens, indent=4))
return self.__root__
def validate_tokens(self) -> None:
"""validates the JWS tokens for format, not their signature - PRs welcome"""
for username in self.__root__:
logging.debug("Parsing %s", username)
# TODO: Work out how to get the validation working. We probably shouldn't be worried about this since we're using it for auth...
logging.debug(
JsonWebSignature().deserialize_compact(s=self[username], key=None)
)
def token_info(self, username: str) -> Optional[JWSPayload]:
"""grabs a token and returns a complex object object"""
if username not in self:
return None
parsed_object = JsonWebSignature().deserialize_compact(
s=self[username], key=None
)
logging.debug(parsed_object)
return JWSPayload.parse_raw(parsed_object.payload)

View file

@ -1,7 +1,8 @@
""" type objects """ """ type objects """
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
# ^ disabling this because pydantic models don't have public methods
from ipaddress import IPv4Address,IPv6Address, IPv6Network, IPv4Network from ipaddress import IPv4Address, IPv6Address, IPv6Network, IPv4Network
import socket import socket
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
@ -9,14 +10,25 @@ from urllib.parse import urlparse
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
import toml import toml
class ClientResponse(BaseModel): class ClientResponse(BaseModel):
"""response from an API call""" """response from an API call, includes the following fields:
content: Optional[str]
data: Optional[Dict[str, Any]]
headers: Dict[str, Any]
status_code: int
"""
content: Optional[str] content: Optional[str]
data: Optional[Dict[str, Any]] data: Optional[Dict[str, Any]]
headers: Dict[str, Any] headers: Dict[str, Any]
status_code: int status_code: int
class Config:
"""Configuration"""
arbitrary_types_allowed = True
class AuthInitResponse(BaseModel): class AuthInitResponse(BaseModel):
"""Aelps parse the response from the Auth 'init' stage""" """Aelps parse the response from the Auth 'init' stage"""
@ -38,9 +50,7 @@ class AuthInitResponse(BaseModel):
class AuthBeginResponse(BaseModel): class AuthBeginResponse(BaseModel):
"""Helps parse the response from the Auth 'begin' stage """Helps parse the response from the Auth 'begin' stage"""
"""
class _AuthBeginState(BaseModel): class _AuthBeginState(BaseModel):
"""Helps parse the response from the Auth 'begin' stage """Helps parse the response from the Auth 'begin' stage
@ -67,6 +77,7 @@ class AuthStepPasswordResponse(BaseModel):
class _AuthStepPasswordState(BaseModel): class _AuthStepPasswordState(BaseModel):
"""subclass to help parse the response from the auth 'step password' stage""" """subclass to help parse the response from the auth 'step password' stage"""
success: Optional[str] success: Optional[str]
sessionid: str sessionid: str
@ -93,6 +104,29 @@ class RadiusGroup(BaseModel):
return value return value
class RadiusTokenGroup(BaseModel):
"""A single group"""
spn: str
uuid: str
class RadiusTokenResponse(BaseModel):
"""model capturing the groups in a response from a token request for a user"""
name: str
secret: str
displayname: Optional[str] = None
uuid: str
groups: List[RadiusTokenGroup]
class Config:
"""config for RadiusTokenGroupList"""
arbitrary_types_allowed = True
class RadiusClient(BaseModel): class RadiusClient(BaseModel):
"""Client config for Kanidm FreeRADIUS integration, """Client config for Kanidm FreeRADIUS integration,
this is a pydantic model. this is a pydantic model.
@ -110,7 +144,7 @@ class RadiusClient(BaseModel):
name: str name: str
ipaddr: str ipaddr: str
secret: str secret: str # TODO: this should probably be renamed to token
@validator("ipaddr") @validator("ipaddr")
def validate_ipaddr(cls, value: str) -> str: def validate_ipaddr(cls, value: str) -> str:
@ -125,7 +159,10 @@ class RadiusClient(BaseModel):
socket.gethostbyname(value) socket.gethostbyname(value)
return value return value
except socket.gaierror as error: except socket.gaierror as error:
raise ValueError(f"ipaddr value ({value}) wasn't an IP Address, Network or valid hostname: {error}") raise ValueError(
f"ipaddr value ({value}) wasn't an IP Address, Network or valid hostname: {error}"
)
class KanidmClientConfig(BaseModel): class KanidmClientConfig(BaseModel):
"""Configuration file definition for Kanidm client config """Configuration file definition for Kanidm client config
@ -136,6 +173,8 @@ class KanidmClientConfig(BaseModel):
uri: Optional[str] = None uri: Optional[str] = None
auth_token: Optional[str] = None
verify_hostnames: bool = True verify_hostnames: bool = True
verify_certificate: bool = True verify_certificate: bool = True
ca_path: Optional[str] = None ca_path: Optional[str] = None

View file

@ -20,6 +20,7 @@ plugins:
nav: nav:
- "Home": README.md - "Home": README.md
- "KanidmClient": kanidmclient.md - "Client": kanidmclient.md
- "KanidmClientConfig": kanidmclientconfig.md - "Client Configuration": kanidmclientconfig.md
- "RadiusClient": radiusclient.md - "RADIUS Client": radiusclient.md
- "Token Storage" : tokenstore.md

933
pykanidm/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,8 +15,6 @@ homepage = "https://kanidm.com/"
packages = [ packages = [
{include = "kanidm"}, {include = "kanidm"},
{include = "tests"}, {include = "tests"},
] ]
keywords = [ keywords = [
@ -27,16 +25,16 @@ keywords = [
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.9"
toml = "^0.10.2" toml = "^0.10.2"
pydantic = "^1.9.2" pydantic = "^1.9.2"
aiohttp = "^3.8.1" aiohttp = "^3.8.1"
Authlib = "^1.1.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pylint = "^2.15.2" pylint = "^2.15.2"
@ -54,6 +52,7 @@ mkdocs = "^1.3.1"
mkdocs-material = "^8.5.3" mkdocs-material = "^8.5.3"
mkdocstrings = "^0.19.0" mkdocstrings = "^0.19.0"
mkdocstrings-python = "^0.7.1" mkdocstrings-python = "^0.7.1"
pook = "^1.0.2"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
@ -70,7 +69,8 @@ load-plugins="pylint_pydantic,pylint_pytest"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
markers = [ markers = [
"network: Tests that require network access", "network: Tests that require network access and a working backend server",
"interactive: Requires specific config and a working backend server"
] ]
[tool.coverage.run] [tool.coverage.run]

73
pykanidm/radius_test_env.sh Executable file
View file

@ -0,0 +1,73 @@
#!/bin/bash
# This sets up a Kanidm environment for doing RADIUS testing.
read -r -n 1 -p "This script rather destructively resets the idm_admin and admin passwords and YOLO's its way through setting up a RADIUS user (test) and service account (radius_server) make sure you're not running this on an environment you care deeply about!"
PWD="$(pwd)"
cd ../kanidmd/daemon || exit 1
echo "Resetting IDM_ADMIN"
# set up idm admin account
IDM_ADMIN=$(./run_insecure_dev_server.sh recover_account idm_admin -o json 2>&1 | grep -v Running | grep recover_account | jq .result)
echo "IDM_ADMIN_PASSWORD: ${IDM_ADMIN}"
read -r -n 1 -p "Copy the idm_admin password somewhere and hit enter to continue"
# set up idm admin account
ADMIN=$(./run_insecure_dev_server.sh recover_account admin -o json 2>&1 | grep -v Running | grep recover_account | jq .result)
echo "ADMIN_PASSWORD: ${ADMIN}"
read -r -n 1 -p "Copy the admin password somewhere and hit enter to continue"
echo -n "Start the server in another terminal"
KEEP_GOING=1
while [ $KEEP_GOING -eq 1 ]; do
echo -n "."
curl -f -s -k https://localhost:8443/status && KEEP_GOING=0
sleep 1
done
cd ../../ || exit 1
echo "Logging in as admin"
cargo run --bin kanidm -- login --name admin
echo "Logging in as idm_admin"
cargo run --bin kanidm -- login --name idm_admin
echo "Creating person 'test'"
cargo run --bin kanidm -- person create test test --name idm_admin
echo "Creating group 'radius_access_allowed'"
cargo run --bin kanidm -- group create radius_access_allowed --name idm_admin
echo "Adding 'test' to group 'radius_access_allowed'"
cargo run --bin kanidm -- group add_members radius_access_allowed test --name idm_admin
echo "Creating radius secret for 'test'"
cargo run --bin kanidm -- person radius generate_secret test --name idm_admin
echo "Showing radius secret for 'test'"
cargo run --bin kanidm -- person radius show_secret test --name idm_admin
read -r -n 1 -p "Copy the RADIUS secret above then press enter to continue"
echo "Creating SA 'radius_server'"
cargo run --bin kanidm -- service-account create radius_server radius_server --name idm_admin
echo "Setting radius_server to be allowed to be a RADIUS server"
cargo run --bin kanidm group add_members --name admin idm_radius_servers radius_server
echo "Creating API Token for 'radius_server' account"
cargo run --bin kanidm -- service-account api-token generate radius_server radius --name admin
echo "Copy the API Token above to the config file"
echo "blep?"

View file

@ -3,7 +3,6 @@
import logging import logging
import os import os
import aiohttp
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -13,6 +12,7 @@ from testutils import client, client_configfile, MockResponse
from kanidm import KanidmClient from kanidm import KanidmClient
from kanidm.exceptions import AuthCredFailed, AuthInitFailed from kanidm.exceptions import AuthCredFailed, AuthInitFailed
from kanidm.types import AuthBeginResponse from kanidm.types import AuthBeginResponse
from kanidm.tokens import TokenStore
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@ -26,10 +26,8 @@ async def test_auth_init(client_configfile: KanidmClient) -> None:
print(f"Doing auth_init for {client_configfile.config.username}") print(f"Doing auth_init for {client_configfile.config.username}")
if client_configfile.config.username is None: if client_configfile.config.username is None:
raise ValueError("This path shouldn't be possible in the test!") pytest.skip("Can't run auth test without a username/password")
async with aiohttp.ClientSession() as session: result = await client_configfile.auth_init(client_configfile.config.username)
client_configfile.session = session
result = await client_configfile.auth_init(client_configfile.config.username)
print(f"{result=}") print(f"{result=}")
print(result.dict()) print(result.dict())
assert result.sessionid assert result.sessionid
@ -41,48 +39,56 @@ async def test_auth_begin(client_configfile: KanidmClient) -> None:
"""tests the auth begin step""" """tests the auth begin step"""
print(f"Doing auth_init for {client_configfile.config.username}") print(f"Doing auth_init for {client_configfile.config.username}")
async with aiohttp.ClientSession() as session: if client_configfile.config.username is None:
client_configfile.session = session pytest.skip("Can't run auth test without a username/password")
if client_configfile.config.username is None: result = await client_configfile.auth_init(client_configfile.config.username)
raise ValueError("This path shouldn't be possible in the test!") print(f"{result=}")
result = await client_configfile.auth_init(client_configfile.config.username) print("Result dict:")
print(f"{result=}") print(result.dict())
print("Result dict:") assert result.sessionid
print(result.dict())
assert result.sessionid
print(f"Doing auth_begin for {client_configfile.config.username}") print(f"Doing auth_begin for {client_configfile.config.username}")
begin_result = await client_configfile.auth_begin( if result.response is None:
# username=client.username, raise ValueError("Failed to get response")
method="password", sessionid = result.response.headers["x-kanidm-auth-session-id"]
) begin_result = await client_configfile.auth_begin(
print(f"{begin_result=}") sessionid=sessionid,
print(begin_result.data) method="password",
retval = begin_result.data )
print(f"{begin_result=}")
print(begin_result.data)
retval = begin_result.data
if retval is None: if retval is None:
raise pytest.fail("Failed to do begin_result") raise pytest.fail("Failed to do begin_result")
retval["response"] = begin_result retval["response"] = begin_result
assert AuthBeginResponse.parse_obj(retval) assert AuthBeginResponse.parse_obj(retval)
@pytest.mark.network @pytest.mark.network
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_authenticate_flow(client_configfile: KanidmClient) -> None: async def test_authenticate_flow(client_configfile: KanidmClient) -> None:
"""tests the authenticate() flow""" """tests the authenticate() flow"""
async with aiohttp.ClientSession() as session: if (
print(f"Doing client.authenticate for {client_configfile.config.username}") client_configfile.config.username is None
client_configfile.session = session or client_configfile.config.password is None
result = await client_configfile.authenticate_password() ):
pytest.skip(
"Can't run this without a username and password set in the config file"
)
client_configfile.config.auth_token = None
print(f"Doing client.authenticate for {client_configfile.config.username}")
result = await client_configfile.authenticate_password()
print(result) print(result)
@pytest.mark.network @pytest.mark.network
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_authenticate_flow_fail(client_configfile: KanidmClient) -> None: async def test_authenticate_flow_fail(client_configfile: KanidmClient) -> None:
"""tests the authenticate() flow with a valid (hopefully) usernamd and invalid password""" """tests the authenticate() flow with a valid (hopefully) username and invalid password"""
if not bool(os.getenv("RUN_SCARY_TESTS", None)): if not bool(os.getenv("RUN_SCARY_TESTS", None)):
pytest.skip(reason="Skipping because env var RUN_SCARY_TESTS isn't set") pytest.skip(reason="Skipping because env var RUN_SCARY_TESTS isn't set")
print("Starting client...") print("Starting client...")
@ -94,14 +100,14 @@ async def test_authenticate_flow_fail(client_configfile: KanidmClient) -> None:
pytest.skip("Please ensure you have a username, password and uri in the config") pytest.skip("Please ensure you have a username, password and uri in the config")
print(f"Doing client.authenticate for {client_configfile.config.username}") print(f"Doing client.authenticate for {client_configfile.config.username}")
async with aiohttp.ClientSession() as session: client_configfile.config.auth_token = None
client_configfile.session = session
with pytest.raises((AuthCredFailed, AuthInitFailed)): with pytest.raises((AuthCredFailed, AuthInitFailed)):
result = await client_configfile.authenticate_password( result = await client_configfile.authenticate_password(
username=client_configfile.config.username, username=client_configfile.config.username,
password="cheese", password="cheese",
) )
print(result) print(result)
# TODO: mock a call to auth_init when a 200 response is not returned, raises AuthInitFailed # TODO: mock a call to auth_init when a 200 response is not returned, raises AuthInitFailed
@ -122,25 +128,23 @@ async def test_authenticate_inputs_validation(
mocker.patch("aiohttp.ClientSession.post", return_value=resp) mocker.patch("aiohttp.ClientSession.post", return_value=resp)
async with aiohttp.ClientSession() as session: with pytest.raises(ValueError):
client.session = session await client.authenticate_password(username="cheese")
with pytest.raises(ValueError): with pytest.raises(ValueError):
await client.authenticate_password(username="cheese") await client.authenticate_password(password="cheese")
with pytest.raises(ValueError): client.config.password = None
await client.authenticate_password(password="cheese") client.config.username = "crabby"
client.config.password = None with pytest.raises(ValueError):
client.config.username = "crabby" await client.authenticate_password()
with pytest.raises(ValueError): client.config.password = "cR4bzR0ol"
await client.authenticate_password() client.config.username = None
client.config.password = "cR4bzR0ol" with pytest.raises(ValueError):
client.config.username = None await client.authenticate_password()
with pytest.raises(ValueError):
await client.authenticate_password()
client.config.username = None client.config.username = None
client.config.password = None client.config.password = None
with pytest.raises(ValueError): with pytest.raises(ValueError):
await client.authenticate_password() await client.authenticate_password()
@pytest.mark.network @pytest.mark.network
@ -149,6 +153,39 @@ async def test_auth_step_password(client: KanidmClient) -> None:
"""tests things""" """tests things"""
with pytest.raises(ValueError): with pytest.raises(ValueError):
async with aiohttp.ClientSession() as session: await client.auth_step_password(sessionid="asdf")
client.session = session
await client.auth_step_password()
@pytest.mark.network
@pytest.mark.asyncio
async def test_authenticate_with_token(client_configfile: KanidmClient) -> None:
"""tests auth with a token, needs to have a valid token in your local cache"""
if "KANIDM_TEST_USERNAME" in os.environ:
test_username: str = os.environ["KANIDM_TEST_USERNAME"]
print(f"Using username {test_username} from KANIDM_TEST_USERNAME env var")
else:
test_username = "idm_admin"
print(
f"Using username {test_username} by default - set KANIDM_TEST_USERNAME env var if you want to change this."
)
tokens = TokenStore()
tokens.load()
if test_username not in tokens:
print(f"Can't find {test_username} user in token store")
raise pytest.skip(f"Can't find {test_username} user in token store")
test_token: str = tokens[test_username]
if not await client_configfile.check_token_valid(test_token):
print(f"Token for {test_username} isn't valid")
pytest.skip(f"Token for {test_username} isn't valid")
else:
print("Token was noted as valid, so auth works!")
# tests the "we set a token and well it works."
client_configfile.config.auth_token = tokens[test_username]
result = await client_configfile.call_get("/v1/self")
print(result)
assert result.status_code == 200

View file

@ -4,7 +4,6 @@ import logging
from pathlib import Path from pathlib import Path
import sys import sys
import aiohttp
import pydantic import pydantic
import pytest import pytest
@ -20,11 +19,9 @@ EXAMPLE_CONFIG_FILE = "../examples/config"
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
async def client() -> KanidmClient: async def client() -> KanidmClient:
"""sets up a client with a basic thing""" """sets up a client with a basic thing"""
async with aiohttp.ClientSession() as session: return KanidmClient(
return KanidmClient( uri="https://idm.example.com",
uri="https://idm.example.com", )
session=session,
)
def test_load_config_file() -> None: def test_load_config_file() -> None:
@ -57,43 +54,25 @@ def test_parse_config_validationerror(client: KanidmClient) -> None:
client.parse_config_data(config_data=testdict) client.parse_config_data(config_data=testdict)
@pytest.mark.asyncio
async def test_parse_config_data(client: KanidmClient) -> None: async def test_parse_config_data(client: KanidmClient) -> None:
"""tests parse_config witha valid input""" """tests parse_config witha valid input"""
testdict = {
async with aiohttp.ClientSession() as session: "uri": "https://example.com",
client.session = session "username": "testuser",
testdict = { "password": "CraBzR0oL",
"uri": "https://example.com", }
"username": "testuser", client.parse_config_data(config_data=testdict)
"password": "CraBzR0oL",
}
client.parse_config_data(config_data=testdict)
@pytest.mark.asyncio
async def test_init_with_uri() -> None: async def test_init_with_uri() -> None:
"""tests the class""" """tests the class"""
async with aiohttp.ClientSession() as session: testclient = KanidmClient(
testclient = KanidmClient( uri="https://example.com",
uri="https://example.com", )
session=session,
)
assert testclient.config.uri == "https://example.com/" assert testclient.config.uri == "https://example.com/"
@pytest.mark.asyncio
async def test_init_with_session() -> None:
"""tests the class"""
async with aiohttp.ClientSession() as session:
testclient = KanidmClient(
uri="https://google.com",
session=session,
)
assert testclient.session is session
def test_config_invalid_uri() -> None: def test_config_invalid_uri() -> None:
"""tests passing an invalid uri to the config parser""" """tests passing an invalid uri to the config parser"""

View file

@ -0,0 +1,73 @@
""" Testing JWS things things """
from datetime import datetime, timezone
import pytest
from kanidm.tokens import JWS, TokenStore
# pylint: disable=line-too-long
TEST_TOKEN = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Im1KQTgtTURfeFRxQXBmSU9nbFptNXJ6RWhoQ3hDdjRxZFNpeGxjV1Q3ZmsiLCJ5IjoiNy0yVkNuY0h3NEF1WVJpYVpYT2FoVXRGMUE2SDd3eUxrUW1FekduS0pKcyIsImFsZyI6IkVTMjU2IiwidXNlIjoic2lnIn0sInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiZjExOTg2NzMtNGI5MC00NjE4LWJkZTctMTBiY2M2YzhjOGE0IiwiYXV0aF90eXBlIjoiZ2VuZXJhdGVkcGFzc3dvcmQiLCJleHBpcnkiOlsyMDIyLDI2NSwyODM2Niw4MDI1MjUwMDBdLCJ1dWlkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDE4IiwibmFtZSI6ImlkbV9hZG1pbiIsImRpc3BsYXluYW1lIjoiSURNIEFkbWluaXN0cmF0b3IiLCJzcG4iOiJpZG1fYWRtaW5AbG9jYWxob3N0IiwibWFpbF9wcmltYXJ5IjpudWxsLCJsaW1fdWlkeCI6ZmFsc2UsImxpbV9ybWF4IjoxMjgsImxpbV9wbWF4IjoyNTYsImxpbV9mbWF4IjozMn0.cln3gRV3NdgbGqYeD26mBSHFGOaFXak2UA5umvj_Xw30dMS8ECTnJU7lvLyepRTW_VzqUJHbRatPkQ1TEuK99Q"
def test_jws_parser() -> None:
"""tests the parsing"""
expected_header = {
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "mJA8-MD_xTqApfIOglZm5rzEhhCxCv4qdSixlcWT7fk",
"y": "7-2VCncHw4AuYRiaZXOahUtF1A6H7wyLkQmEzGnKJJs",
"alg": "ES256",
"use": "sig",
},
"typ": "JWT",
}
expected_payload = {
"session_id": "f1198673-4b90-4618-bde7-10bcc6c8c8a4",
"auth_type": "generatedpassword",
"expiry": [2022, 265, 28366, 802525000],
"uuid": "00000000-0000-0000-0000-000000000018",
"name": "idm_admin",
"displayname": "IDM Administrator",
"spn": "idm_admin@localhost",
"mail_primary": None,
"lim_uidx": False,
"lim_rmax": 128,
"lim_pmax": 256,
"lim_fmax": 32,
}
test_jws = JWS(TEST_TOKEN)
assert test_jws.header.dict() == expected_header
assert test_jws.payload.dict() == expected_payload
def test_tokenstuff() -> None:
"""tests stuff"""
token_store = TokenStore()
token_store[
"idm_admin"
] = "eyJhbGciOiJFUzI1NiIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Im1KQTgtTURfeFRxQXBmSU9nbFptNXJ6RWhoQ3hDdjRxZFNpeGxjV1Q3ZmsiLCJ5IjoiNy0yVkNuY0h3NEF1WVJpYVpYT2FoVXRGMUE2SDd3eUxrUW1FekduS0pKcyIsImFsZyI6IkVTMjU2IiwidXNlIjoic2lnIn0sInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiMTBmZDJjYzMtM2UxZS00MjM1LTk4NjEtNWQyNjQ3NTAyMmVkIiwiYXV0aF90eXBlIjoiZ2VuZXJhdGVkcGFzc3dvcmQiLCJleHBpcnkiOlsyMDIyLDI2NSwzMzkyMywyOTQyNTQwMDBdLCJ1dWlkIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDE4IiwibmFtZSI6ImlkbV9hZG1pbiIsImRpc3BsYXluYW1lIjoiSURNIEFkbWluaXN0cmF0b3IiLCJzcG4iOiJpZG1fYWRtaW5AbG9jYWxob3N0IiwibWFpbF9wcmltYXJ5IjpudWxsLCJsaW1fdWlkeCI6ZmFsc2UsImxpbV9ybWF4IjoxMjgsImxpbV9wbWF4IjoyNTYsImxpbV9mbWF4IjozMn0.rq1y7YNS9iCBWMmAu-FSa4-o4jrSSnMO_18zafgvLRtZFlB7j-Q68CzxceNN9C_1EWnc9uf4fOyeaSNUwGyaIQ"
info = token_store.token_info("idm_admin")
print(f"Parsed token: {info}")
if info is None:
pytest.skip()
print(info.expiry_datetime)
assert (
datetime(
year=2022,
month=9,
day=22,
hour=9,
minute=25,
second=23,
tzinfo=timezone.utc,
)
== info.expiry_datetime
)

View file

@ -0,0 +1,39 @@
""" mocked tests """
# import asyncio
# import aiohttp
# import pytest
# import pook
# from kanidm import KanidmClient
# from kanidm.exceptions import AuthMechUnknown
# this kinda half sorta works but not really - you have to be able to mock a second call and I'm not sure how yet.
# example of how to do the thing https://github.com/h2non/pook/issues/73
# @pytest.mark.mocked
# @pytest.mark.asyncio
# async def test_authenticate_password_raises_authmechunknown() -> None:
# """tests the authenticate() flow"""
# client_config = KanidmClient(uri="https://localhost:8443")
# with pytest.raises(AuthMechUnknown):
# async with aiohttp.ClientSession() as session:
# with pook.post('https://localhost:8443/v1/auth',
# reply=200, response_type='json',response_json={
# "sessionid": "12345",
# "state": {
# "choose" : ["password"],
# "continue" : ["12345"],
# "success" : True,
# }
# },
# response_headers={"x-kanidm-auth-session-id" : "12345"}
# ):
# # async with session.request("GET", "https://localhost:8443") as resp:
# # assert resp.status == 404
# auth_result = await client_config.authenticate_password(username="testing", password="asdfasdfsdf")
# print(f"{auth_result=}")

View file

@ -0,0 +1,50 @@
""" tests the check_vlan function """
from typing import Any
import pytest
from kanidm import KanidmClient
from kanidm.types import KanidmClientConfig, RadiusTokenGroup
from kanidm.radius import check_vlan
@pytest.mark.asyncio
async def test_check_vlan(event_loop: Any) -> None:
"""test 1"""
testconfig = KanidmClientConfig.parse_toml(
"""
uri='https://kanidm.example.com'
radius_groups = [
{ name = "crabz", "vlan" = 1234 },
{ name = "hello world", "vlan" = 12345 },
]
"""
)
print(f"{testconfig=}")
kanidm_client = KanidmClient(
config=testconfig,
)
print(f"{kanidm_client.config=}")
assert (
check_vlan(
acc=12345678,
group=RadiusTokenGroup(spn="crabz@domain.com", uuid="crabz"),
kanidm_client=kanidm_client,
)
== 1234
)
assert (
check_vlan(
acc=12345678,
group=RadiusTokenGroup(spn="foo@bar.com", uuid="lol"),
kanidm_client=kanidm_client,
)
== 12345678
)

View file

@ -10,22 +10,23 @@ from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config from kanidm.utils import load_config
EXAMPLE_CONFIG_FILE="../examples/config" EXAMPLE_CONFIG_FILE = "../../kanidm_rlm_python/examples/config"
def test_load_config_file() -> None: def test_load_config_file() -> None:
""" tests that the file loads """ """tests that the file loads"""
if not Path(EXAMPLE_CONFIG_FILE).expanduser().resolve().exists(): if not Path(EXAMPLE_CONFIG_FILE).expanduser().resolve().exists():
print("Can't find client config file", file=sys.stderr) print("Can't find client config file", file=sys.stderr)
pytest.skip() pytest.skip()
config = load_config(EXAMPLE_CONFIG_FILE) config = load_config(EXAMPLE_CONFIG_FILE)
kanidm_config = KanidmClientConfig.parse_obj(config) kanidm_config = KanidmClientConfig.parse_obj(config)
assert kanidm_config.uri == 'https://idm.example.com/' assert kanidm_config.uri == "https://idm.example.com/"
print(f"{kanidm_config.uri=}") print(f"{kanidm_config.uri=}")
print(kanidm_config) print(kanidm_config)
def test_radius_groups() -> None: def test_radius_groups() -> None:
""" testing loading a config file with radius groups defined """ """testing loading a config file with radius groups defined"""
config_toml = """ config_toml = """
radius_groups = [ radius_groups = [
@ -40,8 +41,9 @@ radius_groups = [
print(group.name) print(group.name)
assert group.name == "hello world" assert group.name == "hello world"
def test_radius_clients() -> None: def test_radius_clients() -> None:
""" testing loading a config file with radius groups defined """ """testing loading a config file with radius groups defined"""
config_toml = """ config_toml = """
radius_clients = [ { name = "hello world", ipaddr = "10.0.0.5", secret = "cr4bj0oz" }, radius_clients = [ { name = "hello world", ipaddr = "10.0.0.5", secret = "cr4bj0oz" },

View file

@ -3,47 +3,27 @@
import json import json
import logging import logging
import os
# from typing import Any
import aiohttp
import pytest import pytest
# from pytest_mock import MockerFixture
# pylint: disable=unused-import # pylint: disable=unused-import
from testutils import client, client_configfile from testutils import client, client_configfile
from kanidm import KanidmClient from kanidm import KanidmClient
# from kanidm.exceptions import AuthCredFailed, AuthInitFailed
# from kanidm.types import AuthBeginResponse
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
RADIUS_TEST_USER = "test"
@pytest.mark.network
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_radius_call(client_configfile: KanidmClient) -> None: async def test_radius_call(client_configfile: KanidmClient) -> None:
"""tests the radius call step""" """tests the radius call step"""
print(f"Doing auth_init for {client_configfile.config.username}") print("Doing auth_init using token")
if "RADIUS_USER" not in os.environ: if client_configfile.config.auth_token is None:
pytest.skip(
"Skipping this test - set RADIUS_USER environment variable to a valid RADIUS user."
)
radius_user = os.environ["RADIUS_USER"]
if client_configfile.config.username is None:
raise ValueError("This path shouldn't be possible in the test!") raise ValueError("This path shouldn't be possible in the test!")
async with aiohttp.ClientSession() as session: result = await client_configfile.get_radius_token(RADIUS_TEST_USER)
client_configfile.session = session
radius_session = await client_configfile.authenticate_password()
result = await client_configfile.get_radius_token(
radius_user, radius_session_id=radius_session.sessionid
)
print(f"{result=}") print(f"{result=}")
print(json.dumps(result.dict(), indent=4, default=str)) print(json.dumps(result.dict(), indent=4, default=str))

View file

@ -11,9 +11,6 @@ from kanidm import KanidmClient
def test_session_header(client: KanidmClient) -> None: def test_session_header(client: KanidmClient) -> None:
"""tests the session_header function""" """tests the session_header function"""
with pytest.raises(ValueError):
client.session_header()
assert client.session_header("testval") == { assert client.session_header("testval") == {
"X-KANIDM-AUTH-SESSION-ID": "testval", "X-KANIDM-AUTH-SESSION-ID": "testval",
} }
@ -23,14 +20,6 @@ def test_session_header(client: KanidmClient) -> None:
async def test_session_creator(client: KanidmClient) -> None: async def test_session_creator(client: KanidmClient) -> None:
"""tests the session_header function""" """tests the session_header function"""
client.session = None
client.config.uri = "🦀" client.config.uri = "🦀"
with pytest.raises(aiohttp.client_exceptions.InvalidURL): with pytest.raises(aiohttp.client_exceptions.InvalidURL):
await client._call(method="GET", path="/") # pylint: disable=protected-access await client._call(method="GET", path="/") # pylint: disable=protected-access
# pytest.raises(ValueError):
# client.session_header()
# assert client.session_header("testval") == {
# "X-KANIDM-AUTH-SESSION-ID": "testval",
# }

View file

@ -18,15 +18,13 @@ async def test_ssl_valid() -> None:
url = "https://badssl.com" url = "https://badssl.com"
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri=url,
uri=url, )
session=session,
)
result = await client.call_get("/") result = await client.call_get("/")
assert result.content assert result.content
print(f"{result.status_code=}") print(f"{result.status_code=}")
@pytest.mark.network @pytest.mark.network
@ -36,14 +34,12 @@ async def test_ssl_self_signed() -> None:
url = "https://self-signed.badssl.com" url = "https://self-signed.badssl.com"
async with aiohttp.ClientSession() as session: print("testing self signed cert with defaults and expecting an error")
print("testing self signed cert with defaults and expecting an error") client = KanidmClient(
client = KanidmClient( uri=url,
uri=url, )
session=session, with pytest.raises(aiohttp.client_exceptions.ClientConnectorCertificateError):
) await client.call_get("/")
with pytest.raises(aiohttp.client_exceptions.ClientConnectorCertificateError):
await client.call_get("/")
@pytest.mark.network @pytest.mark.network
@ -51,14 +47,12 @@ async def test_ssl_self_signed() -> None:
async def test_ssl_self_signed_with_verify() -> None: async def test_ssl_self_signed_with_verify() -> None:
"""tests with a self-signed cert""" """tests with a self-signed cert"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://self-signed.badssl.com",
uri="https://self-signed.badssl.com", verify_certificate=False,
session=session, )
verify_certificate=False, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -66,14 +60,12 @@ async def test_ssl_self_signed_with_verify() -> None:
async def test_ssl_self_signed_no_verify_certificate() -> None: async def test_ssl_self_signed_no_verify_certificate() -> None:
"""tests with a self-signed cert""" """tests with a self-signed cert"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://self-signed.badssl.com",
uri="https://self-signed.badssl.com", verify_certificate=False,
session=session, )
verify_certificate=False, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -81,16 +73,13 @@ async def test_ssl_self_signed_no_verify_certificate() -> None:
async def test_ssl_wrong_hostname_throws_error() -> None: async def test_ssl_wrong_hostname_throws_error() -> None:
"""tests with validate hostnames and wrong hostname in the cert""" """tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session: client = KanidmClient(uri="https://wrong.host.badssl.com/", verify_hostnames=True)
client = KanidmClient( with pytest.raises(
uri="https://wrong.host.badssl.com/", session=session, verify_hostnames=True aiohttp.client_exceptions.ClientConnectorCertificateError,
) match="Cannot connect to host wrong.host.badssl.com:443",
with pytest.raises( ):
aiohttp.client_exceptions.ClientConnectorCertificateError, result = await client.call_get("/")
match="Cannot connect to host wrong.host.badssl.com:443", assert result.content
):
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -98,14 +87,12 @@ async def test_ssl_wrong_hostname_throws_error() -> None:
async def test_ssl_wrong_hostname_dont_verify_hostnames() -> None: async def test_ssl_wrong_hostname_dont_verify_hostnames() -> None:
"""tests with validate hostnames and wrong hostname in the cert""" """tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://wrong.host.badssl.com/",
uri="https://wrong.host.badssl.com/", verify_hostnames=False,
session=session, )
verify_hostnames=False, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -113,15 +100,13 @@ async def test_ssl_wrong_hostname_dont_verify_hostnames() -> None:
async def test_ssl_wrong_hostname_verify_certificate() -> None: async def test_ssl_wrong_hostname_verify_certificate() -> None:
"""tests with validate hostnames and wrong hostname in the cert""" """tests with validate hostnames and wrong hostname in the cert"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://wrong.host.badssl.com/",
uri="https://wrong.host.badssl.com/", verify_hostnames=False,
session=session, verify_certificate=False,
verify_hostnames=False, )
verify_certificate=False, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -129,13 +114,11 @@ async def test_ssl_wrong_hostname_verify_certificate() -> None:
async def test_ssl_revoked() -> None: async def test_ssl_revoked() -> None:
"""tests with a revoked certificate, it'll pass but one day this should be a thing""" """tests with a revoked certificate, it'll pass but one day this should be a thing"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://revoked.badssl.com/",
uri="https://revoked.badssl.com/", )
session=session, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -143,17 +126,15 @@ async def test_ssl_revoked() -> None:
async def test_ssl_expired() -> None: async def test_ssl_expired() -> None:
"""tests with an expired certificate""" """tests with an expired certificate"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://expired.badssl.com/",
uri="https://expired.badssl.com/", )
session=session, with pytest.raises(
) aiohttp.client_exceptions.ClientConnectorCertificateError,
with pytest.raises( match="certificate verify failed: certificate has expired",
aiohttp.client_exceptions.ClientConnectorCertificateError, ):
match="certificate verify failed: certificate has expired", result = await client.call_get("/")
): assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -161,14 +142,12 @@ async def test_ssl_expired() -> None:
async def test_ssl_expired_ignore() -> None: async def test_ssl_expired_ignore() -> None:
"""tests with an expired certificate""" """tests with an expired certificate"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://expired.badssl.com/",
uri="https://expired.badssl.com/", verify_certificate=False,
session=session, )
verify_certificate=False, result = await client.call_get("/")
) assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -176,17 +155,15 @@ async def test_ssl_expired_ignore() -> None:
async def test_ssl_untrusted_root_throws() -> None: async def test_ssl_untrusted_root_throws() -> None:
"""tests with an untrusted root, which should throw an error""" """tests with an untrusted root, which should throw an error"""
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://untrusted-root.badssl.com/",
uri="https://untrusted-root.badssl.com/", )
session=session, with pytest.raises(
) aiohttp.client_exceptions.ClientConnectorCertificateError,
with pytest.raises( match="certificate verify failed: self signed certificate in certificate chain",
aiohttp.client_exceptions.ClientConnectorCertificateError, ):
match="certificate verify failed: self signed certificate in certificate chain", result = await client.call_get("/")
): assert result.content
result = await client.call_get("/")
assert result.content
@pytest.mark.network @pytest.mark.network
@ -199,15 +176,13 @@ async def test_ssl_untrusted_root_configured() -> None:
if not testcert.exists(): if not testcert.exists():
pytest.skip(f"The trusted cert is missing from {testcert}") pytest.skip(f"The trusted cert is missing from {testcert}")
async with aiohttp.ClientSession() as session: client = KanidmClient(
client = KanidmClient( uri="https://untrusted-root.badssl.com/",
uri="https://untrusted-root.badssl.com/", ca_path=testcert.resolve().as_posix(),
session=session, )
ca_path=testcert.resolve().as_posix(), with pytest.raises(
) aiohttp.client_exceptions.ClientConnectorCertificateError,
with pytest.raises( match="certificate verify failed: self signed certificate in certificate chain",
aiohttp.client_exceptions.ClientConnectorCertificateError, ):
match="certificate verify failed: self signed certificate in certificate chain", result = await client.call_get("/")
): assert result.content
result = await client.call_get("/")
assert result.content

View file

@ -57,6 +57,10 @@ def test_kanidmconfig_parse_toml() -> None:
def test_radius_client_bad_hostname() -> None: def test_radius_client_bad_hostname() -> None:
"""tests with a bad hostname""" """tests with a bad hostname"""
with pytest.raises(pydantic.error_wrappers.ValidationError): with pytest.raises(pydantic.error_wrappers.ValidationError):
RadiusClient(name="test", ipaddr="thiscannotpossiblywork.kanidm.example.com",secret="nothing") RadiusClient(
name="test",
ipaddr="thiscannotpossiblywork.kanidm.example.com",
secret="nothing",
)
assert RadiusClient(name="test", ipaddr="kanidm.com",secret="nothing") assert RadiusClient(name="test", ipaddr="kanidm.com", secret="nothing")