From 41ee2dd79eb02c65c20554f327b595ffb4bb936c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 21 Aug 2022 20:21:47 +0000 Subject: [PATCH 01/35] chore: upgrade dependencies --- .pre-commit-config.yaml | 4 +- poetry.lock | 137 ++++++++++++++++++++-------------------- pyproject.toml | 12 ++-- 3 files changed, 78 insertions(+), 75 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f33d006..9b9f01a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,13 +40,13 @@ repos: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.3 hooks: - id: pyupgrade args: ["--py37-plus", "--keep-runtime-typing"] - repo: https://github.com/commitizen-tools/commitizen - rev: v2.28.0 + rev: v2.32.1 hooks: - id: commitizen stages: [commit-msg] diff --git a/poetry.lock b/poetry.lock index 8c519ac9..b1e93f7c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,9 +12,9 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] +test = ["uvloop (>=0.15)", "mock (>=4)", "uvloop (<0.15)", "contextlib2", "trustme", "pytest-mock (>=3.6.1)", "pytest (>=7.0)", "hypothesis (>=4.0)", "coverage[toml] (>=4.5)"] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "packaging"] [[package]] name = "atomicwrites" @@ -26,17 +26,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"] +dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] [[package]] name = "black" @@ -106,7 +106,7 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.1.0" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false @@ -217,15 +217,15 @@ typing-extensions = {version = ">=3.10.0.2", markers = "python_version < \"3.8\" [[package]] name = "filelock" -version = "3.7.1" +version = "3.8.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-timeout (>=1.4.2)", "pytest-cov", "pytest (>=4)", "coverage (>=4)", "covdefaults (>=1.2.0)"] -docs = ["sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4.1)", "furo (>=2021.8.17b43)"] +testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] +docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] [[package]] name = "flake8" @@ -298,8 +298,8 @@ h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" [package.extras] -http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] +http2 = ["h2 (>=3,<5)"] [[package]] name = "httpx" @@ -316,14 +316,14 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] -http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] +http2 = ["h2 (>=3,<5)"] +cli = ["pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)", "click (>=8.0.0,<9.0.0)"] +brotli = ["brotli", "brotlicffi"] [[package]] name = "identify" -version = "2.5.2" +version = "2.5.3" description = "File identification library for Python" category = "dev" optional = false @@ -353,8 +353,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["importlib-resources (>=1.3)", "pytest-mypy", "pytest-black (>=0.3.7)", "flufl.flake8", "pyfakefs", "pep517", "packaging", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "iniconfig" @@ -381,10 +381,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] +pipfile_deprecated_finder = ["requirementslib", "pipreqs"] [[package]] name = "jeepney" @@ -400,7 +400,7 @@ test = ["async-timeout", "trio", "testpath", "pytest-asyncio (>=0.17)", "pytest- [[package]] name = "keyring" -version = "23.7.0" +version = "23.8.2" description = "Store and access your passwords safely." category = "dev" optional = false @@ -413,8 +413,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.3)", "pytest-cov", "flake8 (<5)", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] [[package]] name = "mccabe" @@ -494,8 +494,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +test = ["pytest (>=6)", "pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "appdirs (==1.4.4)"] +docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)"] [[package]] name = "pluggy" @@ -539,7 +539,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" -version = "2.9.0" +version = "2.9.1" description = "Python style guide checker" category = "dev" optional = false @@ -565,8 +565,8 @@ python-versions = ">=3.6.1" typing-extensions = ">=3.7.4.3" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +dotenv = ["python-dotenv (>=0.10.4)"] [[package]] name = "pyflakes" @@ -578,12 +578,15 @@ python-versions = ">=3.6" [[package]] name = "pygments" -version = "2.12.0" +version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -593,7 +596,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -615,7 +618,7 @@ py = ">=1.8.2" tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["xmlschema", "requests", "pygments (>=2.7.2)", "nose", "mock", "hypothesis (>=3.56)", "argcomplete"] [[package]] name = "pytest-asyncio" @@ -674,7 +677,7 @@ six = ">=1.5" [[package]] name = "python-gitlab" -version = "3.6.0" +version = "3.8.1" description = "Interact with GitLab API" category = "dev" optional = false @@ -685,8 +688,8 @@ requests = ">=2.25.0" requests-toolbelt = ">=0.9.1" [package.extras] -autocompletion = ["argcomplete (>=1.10.0,<3)"] yaml = ["PyYaml (>=5.2)"] +autocompletion = ["argcomplete (>=1.10.0,<3)"] [[package]] name = "python-semantic-release" @@ -733,7 +736,7 @@ python-versions = ">=3.6" [[package]] name = "readme-renderer" -version = "35.0" +version = "36.0" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "dev" optional = false @@ -762,8 +765,8 @@ 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)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] [[package]] name = "requests-toolbelt" @@ -792,7 +795,7 @@ idna2008 = ["idna"] [[package]] name = "secretstorage" -version = "3.3.2" +version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" optional = false @@ -870,10 +873,10 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] telegram = ["requests"] +slack = ["slack-sdk"] +notebook = ["ipywidgets (>=6)"] +dev = ["wheel", "twine", "py-make (>=0.1.0)"] [[package]] name = "twine" @@ -957,9 +960,9 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +brotli = ["brotlipy (>=0.6.0)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] [[package]] name = "virtualenv" @@ -976,8 +979,8 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] +testing = ["pytest-timeout (>=1)", "pytest-randomly (>=1)", "pytest-mock (>=2)", "pytest-freezegun (>=0.4.1)", "pytest-env (>=0.6.2)", "pytest (>=4)", "packaging (>=20.0)", "flaky (>=3)", "coverage-enable-subprocess (>=1)", "coverage (>=4)"] +docs = ["towncrier (>=21.3)", "sphinx-rtd-theme (>=0.4.3)", "sphinx-argparse (>=0.2.5)", "sphinx (>=3)", "proselint (>=0.10.2)"] [[package]] name = "webencodings" @@ -996,13 +999,13 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "35b031cd699f9228d50598d2f6e5e3fdd2c8b96e7082c2056675c83e1fa2779d" +content-hash = "d45c803cf0ce552561f7e2b8d01695b3773acf8d3a0713697bc1ca5cbf0f231b" [metadata.files] anyio = [ @@ -1013,8 +1016,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] black = [ {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, @@ -1120,8 +1123,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, @@ -1228,8 +1231,8 @@ faker = [ {file = "Faker-14.1.0.tar.gz", hash = "sha256:0e00bfa1eadf1493f15662edb181222fea4847764cf3f9ff3e66ee0f95c9a644"}, ] filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] flake8 = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, @@ -1260,8 +1263,8 @@ httpx = [ {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] identify = [ - {file = "identify-2.5.2-py2.py3-none-any.whl", hash = "sha256:feaa9db2dc0ce333b453ce171c0cf1247bbfde2c55fc6bb785022d411a1b78b5"}, - {file = "identify-2.5.2.tar.gz", hash = "sha256:a3d4c096b384d50d5e6dc5bc8b9bc44f1f61cefebd750a7b3e9f939b53fb214d"}, + {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, + {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1288,8 +1291,8 @@ jeepney = [ {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, ] keyring = [ - {file = "keyring-23.7.0-py3-none-any.whl", hash = "sha256:e67fc91a7955785fd2efcbccdd72d7dacf136dbc381d27de305b2b660b3de886"}, - {file = "keyring-23.7.0.tar.gz", hash = "sha256:782e1cd1132e91bf459fcd243bcf25b326015c1ac0b198e4408f91fa6791062b"}, + {file = "keyring-23.8.2-py3-none-any.whl", hash = "sha256:10d2a8639663fe2090705a00b8c47c687cacdf97598ea9c11456679fa974473a"}, + {file = "keyring-23.8.2.tar.gz", hash = "sha256:0d9973f8891850f1ade5f26aafd06bb16865fbbae3fc56b0defb6a14a2624003"}, ] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, @@ -1336,8 +1339,8 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ - {file = "pycodestyle-2.9.0-py2.py3-none-any.whl", hash = "sha256:289cdc0969d589d90752582bef6dff57c5fbc6949ee8b013ad6d6449a8ae9437"}, - {file = "pycodestyle-2.9.0.tar.gz", hash = "sha256:beaba44501f89d785be791c9462553f06958a221d166c64e1f107320f839acc2"}, + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, @@ -1385,8 +1388,8 @@ pyflakes = [ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, @@ -1413,8 +1416,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-gitlab = [ - {file = "python-gitlab-3.6.0.tar.gz", hash = "sha256:901c54ff926f10479cb591a34d65f0a3022f2bcc41074f9a192c7fa7e4c57061"}, - {file = "python_gitlab-3.6.0-py3-none-any.whl", hash = "sha256:b855c15e106812a4ada7c4d417def61bc211976bbf2ee1c7e0cd48a83eca4e9f"}, + {file = "python-gitlab-3.8.1.tar.gz", hash = "sha256:e1db25076520e118c7c26becb0d074a7a68127439870d43b8636342206b1e091"}, + {file = "python_gitlab-3.8.1-py3-none-any.whl", hash = "sha256:6d10de90f4fcb95ea92e301528d2442ac5094da9b7137fa1294614369679e791"}, ] python-semantic-release = [ {file = "python-semantic-release-7.31.2.tar.gz", hash = "sha256:4d6a135ebbdfdb78999ccb0e6d58774e5b2145898a0faf74ca38a0919ff07440"}, @@ -1460,8 +1463,8 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] readme-renderer = [ - {file = "readme_renderer-35.0-py3-none-any.whl", hash = "sha256:73b84905d091c31f36e50b4ae05ae2acead661f6a09a9abb4df7d2ddcdb6a698"}, - {file = "readme_renderer-35.0.tar.gz", hash = "sha256:a727999acfc222fc21d82a12ed48c957c4989785e5865807c65a487d21677497"}, + {file = "readme_renderer-36.0-py3-none-any.whl", hash = "sha256:2c37e472ca96755caba6cc58bcbf673a5574bc033385a2ac91d85dfef2799876"}, + {file = "readme_renderer-36.0.tar.gz", hash = "sha256:f71aeef9a588fcbed1f4cc001ba611370e94a0cd27c75b1140537618ec78f0a2"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, @@ -1476,8 +1479,8 @@ rfc3986 = [ {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] secretstorage = [ - {file = "SecretStorage-3.3.2-py3-none-any.whl", hash = "sha256:755dc845b6ad76dcbcbc07ea3da75ae54bb1ea529eb72d15f83d26499a5df319"}, - {file = "SecretStorage-3.3.2.tar.gz", hash = "sha256:0a8eb9645b320881c222e827c26f4cfcf55363e8b374a021981ef886657a912f"}, + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, diff --git a/pyproject.toml b/pyproject.toml index cd2602b0..050b1667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,20 +17,20 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" httpx = "^0.23.0" -pydantic = "^1.9.1" +pydantic = "^1.9.2" [tool.poetry.dev-dependencies] -pytest = "^7.1.2" -flake8 = "^5.0.4" black = "^22.6.0" +Faker = "^14.1.0" +flake8 = "^5.0.4" isort = "^5.10.1" pre-commit = "^2.20.0" +pytest = "^7.1.2" +pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0" pytest-depends = "^1.0.1" -pytest-asyncio = "^0.19.0" -Faker = "^14.1.0" -unasync-cli = "^0.0.9" python-semantic-release = "^7.31.2" +unasync-cli = "^0.0.9" [tool.semantic_release] version_variable = "gotrue/__init__.py:__version__" From 896a40c06f6800b0f0b3eb81a386cf00219f793c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 21 Aug 2022 23:04:33 +0000 Subject: [PATCH 02/35] chore: implement script to sync config of infra --- Makefile | 3 + infra/docker-compose.yml | 10 +-- poetry.lock | 166 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + scripts/gh-download.py | 122 ++++++++++++++++++++++++++++ 5 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 scripts/gh-download.py diff --git a/Makefile b/Makefile index bdec57bc..ab21eb30 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ clean_infra: docker-compose down --remove-orphans &&\ docker system prune -a --volumes -f +sync_infra: + python scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra + run_tests: run_infra sleep tests build_sync: diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index f6d2988f..6f33acd6 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -1,8 +1,7 @@ # docker-compose.yml version: '3' services: - gotrue: - # Signup enabled, autoconfirm off + gotrue: # Signup enabled, autoconfirm off image: supabase/gotrue:latest ports: - '9999:9999' @@ -42,8 +41,7 @@ services: depends_on: - db restart: on-failure - autoconfirm: - # Signup enabled, autoconfirm on + autoconfirm: # Signup enabled, autoconfirm on image: supabase/gotrue:latest ports: - '9998:9998' @@ -72,8 +70,7 @@ services: depends_on: - db restart: on-failure - disabled: - # Signup disabled + disabled: # Signup disabled image: supabase/gotrue:latest ports: - '9997:9997' @@ -112,6 +109,7 @@ services: image: supabase/postgres:14.1.0 ports: - '5432:5432' + command: postgres -c config_file=/etc/postgresql/postgresql.conf volumes: - ./db:/docker-entrypoint-initdb.d/ environment: diff --git a/poetry.lock b/poetry.lock index b1e93f7c..60130cfb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -179,6 +179,20 @@ sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["pytest-cov", "pytest", "PyTest-Cov (<2.6)", "PyTest (<5)", "zipp (<2)", "sphinxcontrib-websupport (<2)", "configparser (<5)", "importlib-resources (<4)", "importlib-metadata (<3)", "sphinx (<2)", "bump2version (<1)", "tox"] + [[package]] name = "distlib" version = "0.3.5" @@ -576,6 +590,23 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "pygithub" +version = "1.55" +description = "Use the full Github API v3" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +deprecated = "*" +pyjwt = ">=2.0" +pynacl = ">=1.4.0" +requests = ">=2.14.0" + +[package.extras] +integrations = ["cryptography"] + [[package]] name = "pygments" version = "2.13.0" @@ -587,6 +618,35 @@ python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyjwt" +version = "2.4.0" +description = "JSON Web Token implementation in Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +docs = ["zope.interface", "sphinx-rtd-theme", "sphinx"] +dev = ["pre-commit", "mypy", "coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)", "cryptography (>=3.3.1)", "zope.interface", "sphinx-rtd-theme", "sphinx"] +crypto = ["cryptography (>=3.3.1)"] + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -736,7 +796,7 @@ python-versions = ">=3.6" [[package]] name = "readme-renderer" -version = "36.0" +version = "37.0" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "dev" optional = false @@ -990,6 +1050,14 @@ category = "dev" optional = false python-versions = "*" +[[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 = "zipp" version = "3.8.1" @@ -1005,7 +1073,7 @@ docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9) [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d45c803cf0ce552561f7e2b8d01695b3773acf8d3a0713697bc1ca5cbf0f231b" +content-hash = "02349689bebe4c64624d8545c13ff47bca48b8c1434109c4dad54214b14056c2" [metadata.files] anyio = [ @@ -1214,6 +1282,10 @@ cryptography = [ {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, ] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] distlib = [ {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, @@ -1387,10 +1459,30 @@ pyflakes = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] +pygithub = [ + {file = "PyGithub-1.55-py3-none-any.whl", hash = "sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b"}, + {file = "PyGithub-1.55.tar.gz", hash = "sha256:1bbfff9372047ff3f21d5cd8e07720f3dbfdaf6462fcaed9d815f528f1ba7283"}, +] pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] +pyjwt = [ + {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, + {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, +] +pynacl = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, @@ -1463,8 +1555,8 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] readme-renderer = [ - {file = "readme_renderer-36.0-py3-none-any.whl", hash = "sha256:2c37e472ca96755caba6cc58bcbf673a5574bc033385a2ac91d85dfef2799876"}, - {file = "readme_renderer-36.0.tar.gz", hash = "sha256:f71aeef9a588fcbed1f4cc001ba611370e94a0cd27c75b1140537618ec78f0a2"}, + {file = "readme_renderer-37.0-py3-none-any.whl", hash = "sha256:9fa416704703e509eeb900696751c908ddeb2011319d93700d8f18baff887a69"}, + {file = "readme_renderer-37.0.tar.gz", hash = "sha256:07b7ea234e03e58f77cc222e206e6abb8f4c0435becce5104794ee591f9301c5"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, @@ -1572,6 +1664,72 @@ webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +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"}, +] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, diff --git a/pyproject.toml b/pyproject.toml index 050b1667..9ca314cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ Faker = "^14.1.0" flake8 = "^5.0.4" isort = "^5.10.1" pre-commit = "^2.20.0" +PyGithub = "^1.55" pytest = "^7.1.2" pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0" diff --git a/scripts/gh-download.py b/scripts/gh-download.py new file mode 100644 index 00000000..c976ecf6 --- /dev/null +++ b/scripts/gh-download.py @@ -0,0 +1,122 @@ +# This code was copied from +# https://gist.github.com/pdashford/2e4bcd4fc2343e2fd03efe4da17f577d +# and modified to work with Python 3, type hints, correct format and +# simplified the code to our needs. + +""" +Downloads folders from github repo +Requires PyGithub +pip install PyGithub +""" + +import base64 +import getopt +import os +import shutil +import sys +from typing import Optional + +from github import Github, GithubException +from github.ContentFile import ContentFile +from github.Repository import Repository + + +def get_sha_for_tag(repository: Repository, tag: str) -> str: + """ + Returns a commit PyGithub object for the specified repository and tag. + """ + branches = repository.get_branches() + matched_branches = [match for match in branches if match.name == tag] + if matched_branches: + return matched_branches[0].commit.sha + + tags = repository.get_tags() + matched_tags = [match for match in tags if match.name == tag] + if not matched_tags: + raise ValueError("No Tag or Branch exists with that name") + return matched_tags[0].commit.sha + + +def download_directory(repository: Repository, sha: str, server_path: str) -> None: + """ + Download all contents at server_path with commit tag sha in + the repository. + """ + if os.path.exists(server_path): + shutil.rmtree(server_path) + + os.makedirs(server_path) + contents = repository.get_dir_contents(server_path, ref=sha) + + for content in contents: + print("Processing %s" % content.path) + if content.type == "dir": + os.makedirs(content.path) + download_directory(repository, sha, content.path) + else: + try: + path = content.path + file_content = repository.get_contents(path, ref=sha) + if not isinstance(file_content, ContentFile): + raise ValueError("Expected ContentFile") + file_out = open(content.path, "w+") + if file_content.content: + file_data = base64.b64decode(file_content.content) + file_out.write(file_data.decode("utf-8")) + file_out.close() + except (GithubException, IOError, ValueError) as exc: + print("Error processing %s: %s", content.path, exc) + + +def usage(): + """ + Prints the usage command lines + """ + print("usage: gh-download --repo=repo --branch=branch --folder=folder") + + +def main(argv): + """ + Main function block + """ + try: + opts, _ = getopt.getopt(argv, "r:b:f:", ["repo=", "branch=", "folder="]) + except getopt.GetoptError as err: + print(str(err)) + usage() + sys.exit(2) + repo: Optional[str] = None + branch: Optional[str] = None + folder: Optional[str] = None + for opt, arg in opts: + if opt in ("-r", "--repo"): + repo = arg + elif opt in ("-b", "--branch"): + branch = arg + elif opt in ("-f", "--folder"): + folder = arg + + if not repo: + print("Repo is required") + usage() + sys.exit(2) + if not branch: + print("Branch is required") + usage() + sys.exit(2) + if not folder: + print("Folder is required") + usage() + sys.exit(2) + + github = Github(None) + repository = github.get_repo(repo) + sha = get_sha_for_tag(repository, branch) + download_directory(repository, sha, folder) + + +if __name__ == "__main__": + """ + Entry point + """ + main(sys.argv[1:]) From 5be3c335822c7963b95dbce98db6ae4d5ae24d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 21 Aug 2022 23:13:48 +0000 Subject: [PATCH 03/35] chore: add sourcery config file --- .sourcery.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .sourcery.yaml diff --git a/.sourcery.yaml b/.sourcery.yaml new file mode 100644 index 00000000..f300aba4 --- /dev/null +++ b/.sourcery.yaml @@ -0,0 +1,2 @@ +refactor: + python_version: '3.7' From 07f897ed3eddd93c9411f8f23f83948ed71bf674 Mon Sep 17 00:00:00 2001 From: "sourcery-ai[bot]" <58596630+sourcery-ai[bot]@users.noreply.github.com> Date: Sun, 21 Aug 2022 19:14:49 -0400 Subject: [PATCH 04/35] 'Refactored by Sourcery' (#149) Co-authored-by: Sourcery AI <> --- scripts/gh-download.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/gh-download.py b/scripts/gh-download.py index c976ecf6..86d282a6 100644 --- a/scripts/gh-download.py +++ b/scripts/gh-download.py @@ -49,7 +49,7 @@ def download_directory(repository: Repository, sha: str, server_path: str) -> No contents = repository.get_dir_contents(server_path, ref=sha) for content in contents: - print("Processing %s" % content.path) + print(f"Processing {content.path}") if content.type == "dir": os.makedirs(content.path) download_directory(repository, sha, content.path) @@ -59,11 +59,10 @@ def download_directory(repository: Repository, sha: str, server_path: str) -> No file_content = repository.get_contents(path, ref=sha) if not isinstance(file_content, ContentFile): raise ValueError("Expected ContentFile") - file_out = open(content.path, "w+") - if file_content.content: - file_data = base64.b64decode(file_content.content) - file_out.write(file_data.decode("utf-8")) - file_out.close() + with open(content.path, "w+") as file_out: + if file_content.content: + file_data = base64.b64decode(file_content.content) + file_out.write(file_data.decode("utf-8")) except (GithubException, IOError, ValueError) as exc: print("Error processing %s: %s", content.path, exc) @@ -82,7 +81,7 @@ def main(argv): try: opts, _ = getopt.getopt(argv, "r:b:f:", ["repo=", "branch=", "folder="]) except getopt.GetoptError as err: - print(str(err)) + print(err) usage() sys.exit(2) repo: Optional[str] = None From 68ed829b33db91a95ef5fef28b344f84f1ecaccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 21 Aug 2022 23:17:47 +0000 Subject: [PATCH 05/35] chore: change made by pyupgrade pre-commit --- scripts/gh-download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gh-download.py b/scripts/gh-download.py index 86d282a6..91b2e567 100644 --- a/scripts/gh-download.py +++ b/scripts/gh-download.py @@ -63,7 +63,7 @@ def download_directory(repository: Repository, sha: str, server_path: str) -> No if file_content.content: file_data = base64.b64decode(file_content.content) file_out.write(file_data.decode("utf-8")) - except (GithubException, IOError, ValueError) as exc: + except (GithubException, OSError, ValueError) as exc: print("Error processing %s: %s", content.path, exc) From 6b15a50466f82b87aae67823e08770c25cb352f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Wed, 31 Aug 2022 08:53:31 +0000 Subject: [PATCH 06/35] chore: update dependencies --- poetry.lock | 127 +++++++++++++++++++++++-------------------------- pyproject.toml | 4 +- 2 files changed, 62 insertions(+), 69 deletions(-) diff --git a/poetry.lock b/poetry.lock index 96137b18..36431ffb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,8 +12,6 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] test = ["uvloop (>=0.15)", "mock (>=4)", "uvloop (<0.15)", "contextlib2", "trustme", "pytest-mock (>=3.6.1)", "pytest (>=7.0)", "hypothesis (>=4.0)", "coverage[toml] (>=4.5)"] doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "packaging"] @@ -35,10 +33,10 @@ optional = false python-versions = ">=3.5" [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"] +tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"] +dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] [[package]] name = "black" @@ -58,10 +56,10 @@ typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implem typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +jupyter = ["tokenize-rt (>=3.2.0)", "ipython (>=7.8.0)"] +d = ["aiohttp (>=3.7.4)"] +colorama = ["colorama (>=0.4.3)"] [[package]] name = "bleach" @@ -76,8 +74,8 @@ six = ">=1.9.0" webencodings = "*" [package.extras] +dev = ["mypy (==0.961)", "black (==22.3.0)", "wheel (==0.37.1)", "twine (==4.0.1)", "tox (==3.25.0)", "Sphinx (==4.3.2)", "pytest (==7.1.2)", "pip-tools (==6.6.2)", "hashin (==0.17.0)", "flake8 (==4.0.1)", "build (==0.8.0)"] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "certifi" @@ -175,11 +173,11 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "deprecated" @@ -197,7 +195,7 @@ dev = ["pytest-cov", "pytest", "PyTest-Cov (<2.6)", "PyTest (<5)", "zipp (<2)", [[package]] name = "distlib" -version = "0.3.5" +version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false @@ -240,8 +238,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] +docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] [[package]] name = "flake8" @@ -332,9 +330,6 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] -http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] http2 = ["h2 (>=3,<5)"] cli = ["pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)", "click (>=8.0.0,<9.0.0)"] @@ -372,8 +367,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +testing = ["importlib-resources (>=1.3)", "pytest-mypy", "pytest-black (>=0.3.7)", "flufl.flake8", "pyfakefs", "pep517", "packaging", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "iniconfig" @@ -400,10 +395,10 @@ 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"] +colors = ["colorama (>=0.4.3,<0.5.0)"] requirements_deprecated_finder = ["pip-api", "pipreqs"] +pipfile_deprecated_finder = ["requirementslib", "pipreqs"] [[package]] name = "jeepney" @@ -414,8 +409,8 @@ optional = false python-versions = ">=3.7" [package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["async-generator", "trio"] +test = ["async-timeout", "trio", "testpath", "pytest-asyncio (>=0.17)", "pytest-trio", "pytest"] [[package]] name = "keyring" @@ -432,8 +427,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.3)", "pytest-cov", "flake8 (<5)", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] [[package]] name = "mccabe" @@ -460,11 +455,11 @@ optional = false python-versions = ">=3.7" [package.extras] -default = ["matplotlib (>=3.3)", "numpy (>=1.19)", "pandas (>=1.1)", "scipy (>=1.5,!=1.6.1)"] +default = ["numpy (>=1.19)", "scipy (>=1.5,!=1.6.1)", "matplotlib (>=3.3)", "pandas (>=1.1)"] developer = ["black (==21.5b1)", "pre-commit (>=2.12)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.1)", "pillow (>=8.2)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx (>=4.0,<5.0)", "sphinx-gallery (>=0.9,<1.0)", "texext (>=0.6.6)"] -extra = ["lxml (>=4.5)", "pydot (>=1.4.1)", "pygraphviz (>=1.7)"] -test = ["codecov (>=2.1)", "pytest (>=6.2)", "pytest-cov (>=2.12)"] +doc = ["sphinx (>=4.0,<5.0)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx-gallery (>=0.9,<1.0)", "numpydoc (>=1.1)", "pillow (>=8.2)", "nb2plots (>=0.6)", "texext (>=0.6.6)"] +extra = ["lxml (>=4.5)", "pygraphviz (>=1.7)", "pydot (>=1.4.1)"] +test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] [[package]] name = "nodeenv" @@ -487,11 +482,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" [[package]] name = "pkginfo" @@ -502,7 +497,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -testing = ["coverage", "nose"] +testing = ["nose", "coverage"] [[package]] name = "platformdirs" @@ -513,8 +508,8 @@ 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)"] +test = ["pytest (>=6)", "pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "appdirs (==1.4.4)"] +docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)"] [[package]] name = "pluggy" @@ -528,8 +523,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "pre-commit" @@ -584,8 +579,8 @@ python-versions = ">=3.7" typing-extensions = ">=4.1.0" [package.extras] -email = ["email-validator (>=1.0.3)"] dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pyflakes" @@ -698,7 +693,7 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["pytest-trio (>=0.7.0)", "mypy (>=0.931)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "coverage (>=6.2)"] [[package]] name = "pytest-cov" @@ -713,7 +708,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytest-depends" @@ -742,7 +737,7 @@ six = ">=1.5" [[package]] name = "python-gitlab" -version = "3.8.1" +version = "3.9.0" description = "Interact with GitLab API" category = "dev" optional = false @@ -753,8 +748,8 @@ requests = ">=2.25.0" requests-toolbelt = ">=0.9.1" [package.extras] -yaml = ["PyYaml (>=5.2)"] autocompletion = ["argcomplete (>=1.10.0,<3)"] +yaml = ["PyYaml (>=5.2)"] [[package]] name = "python-semantic-release" @@ -778,10 +773,10 @@ tomlkit = ">=0.10,<1.0" twine = ">=3,<4" [package.extras] -dev = ["black", "isort", "tox"] -docs = ["Jinja2 (==3.0.3)", "Sphinx (==1.3.6)"] +dev = ["tox", "isort", "black"] +docs = ["Sphinx (==1.3.6)", "Jinja2 (==3.0.3)"] mypy = ["mypy", "types-requests"] -test = ["coverage (>=5,<6)", "mock (==1.3.0)", "pytest (>=5,<6)", "pytest-mock (>=2,<3)", "pytest-xdist (>=1,<2)", "responses (==0.13.3)"] +test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"] [[package]] name = "pywin32-ctypes" @@ -920,7 +915,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.10.2" +version = "0.11.4" description = "Style preserving TOML library" category = "dev" optional = false @@ -983,10 +978,10 @@ python-versions = ">=3.6" click = ">=7.1.1,<9.0.0" [package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +test = ["isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] +all = ["shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] [[package]] name = "typing-extensions" @@ -1018,18 +1013,16 @@ unasync = ">=0.5.0,<0.6.0" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <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)"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] -brotli = ["brotlipy (>=0.6.0)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] [[package]] name = "virtualenv" @@ -1074,13 +1067,13 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "57fbe707ed3e089071998cc52cfdf4d54241e43453b921d5759e96cc74e7c96d" +content-hash = "2cf192262cd47d8f28afc2db5ae68a3eb7f9db4def8c58e90fb3e6c3fb6f7faf" [metadata.files] anyio = [ @@ -1294,8 +1287,8 @@ deprecated = [ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] distlib = [ - {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, - {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] docutils = [ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, @@ -1394,8 +1387,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.10.0-py3-none-any.whl", hash = "sha256:aefa80ac32d5bf1f96139dca67cefb69a431beff4e6bf1168468f37d7ab87015"}, + {file = "pathspec-0.10.0.tar.gz", hash = "sha256:01eecd304ba0e6eeed188ae5fa568e99ef10265af7fd9ab737d6412b4ee0ab85"}, ] pkginfo = [ {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, @@ -1516,8 +1509,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-gitlab = [ - {file = "python-gitlab-3.8.1.tar.gz", hash = "sha256:e1db25076520e118c7c26becb0d074a7a68127439870d43b8636342206b1e091"}, - {file = "python_gitlab-3.8.1-py3-none-any.whl", hash = "sha256:6d10de90f4fcb95ea92e301528d2442ac5094da9b7137fa1294614369679e791"}, + {file = "python-gitlab-3.9.0.tar.gz", hash = "sha256:5fc5e88f81f366e11851cb8b4b9a5b827491ce20ba7585446b74c9b097726ba3"}, + {file = "python_gitlab-3.9.0-py3-none-any.whl", hash = "sha256:ce941f99bf88b6918eea82500ca6206806117f4afe26d4705f4ded2284b35c69"}, ] python-semantic-release = [ {file = "python-semantic-release-7.31.4.tar.gz", hash = "sha256:ee00c396b6d394b87b5ba3cf6be0f759f2f23783c2215e3ac4c22dc4bc676d60"}, @@ -1607,8 +1600,8 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.10.2-py3-none-any.whl", hash = "sha256:905cf92c2111ef80d355708f47ac24ad1b6fc2adc5107455940088c9bbecaedb"}, - {file = "tomlkit-0.10.2.tar.gz", hash = "sha256:30d54c0b914e595f3d10a87888599eab5321a2a69abc773bbefff51599b72db6"}, + {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, + {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, ] tqdm = [ {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, @@ -1661,8 +1654,8 @@ unasync-cli = [ {file = "unasync_cli-0.0.9-py3-none-any.whl", hash = "sha256:f96c42fb2862efa555ce6d6415a5983ceb162aa0e45be701656d20a955c7c540"}, ] urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] virtualenv = [ {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, diff --git a/pyproject.toml b/pyproject.toml index 8c7dd473..72923359 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" httpx = "^0.23.0" -pydantic = "^1.9.2" +pydantic = "^1.10.0" [tool.poetry.dev-dependencies] black = "^22.6.0" @@ -30,7 +30,7 @@ pytest = "^7.1.2" pytest-asyncio = "^0.19.0" pytest-cov = "^3.0.0" pytest-depends = "^1.0.1" -python-semantic-release = "^7.31.2" +python-semantic-release = "^7.31.4" unasync-cli = "^0.0.9" [tool.semantic_release] From 6a200fead8b18cb06fe4fcf93c37b65345b48372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 9 Oct 2022 05:25:06 +0000 Subject: [PATCH 07/35] chore: format docstring to avoid linter warning --- gotrue/_async/api.py | 3 ++- gotrue/_sync/api.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gotrue/_async/api.py b/gotrue/_async/api.py index 11ea229f..9f2098b6 100644 --- a/gotrue/_async/api.py +++ b/gotrue/_async/api.py @@ -276,7 +276,8 @@ async def send_magic_link_email( return check_response(response) async def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: - """Sends a mobile OTP via SMS. Will register the account if it doesn't already exist + """Sends a mobile OTP via SMS. + Will register the account if it doesn't already exist Parameters ---------- diff --git a/gotrue/_sync/api.py b/gotrue/_sync/api.py index 6ab024f9..dfb6c76f 100644 --- a/gotrue/_sync/api.py +++ b/gotrue/_sync/api.py @@ -276,7 +276,8 @@ def send_magic_link_email( return check_response(response) def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: - """Sends a mobile OTP via SMS. Will register the account if it doesn't already exist + """Sends a mobile OTP via SMS. + Will register the account if it doesn't already exist Parameters ---------- From 93aabbe1f5f130677b9a3765c998936940b75dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 9 Oct 2022 05:28:30 +0000 Subject: [PATCH 08/35] chore(deps): bump dev dependencies --- poetry.lock | 653 +++++++++++++++++++++++++++---------------------- pyproject.toml | 8 +- 2 files changed, 363 insertions(+), 298 deletions(-) diff --git a/poetry.lock b/poetry.lock index 36431ffb..8a7adaf7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,17 +12,9 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] -test = ["uvloop (>=0.15)", "mock (>=4)", "uvloop (<0.15)", "contextlib2", "trustme", "pytest-mock (>=3.6.1)", "pytest (>=7.0)", "hypothesis (>=4.0)", "coverage[toml] (>=4.5)"] -doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "packaging"] - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" @@ -33,18 +25,18 @@ optional = false python-versions = ">=3.5" [package.extras] -tests_no_zope = ["cloudpickle", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] -tests = ["cloudpickle", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] -docs = ["sphinx-notfound-page", "zope.interface", "sphinx", "furo"] -dev = ["cloudpickle", "pre-commit", "sphinx-notfound-page", "sphinx", "furo", "zope.interface", "pytest-mypy-plugins", "mypy (>=0.900,!=0.940)", "pytest (>=4.3.0)", "pympler", "hypothesis", "coverage[toml] (>=5.0.2)"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "black" -version = "22.6.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -56,10 +48,10 @@ typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implem typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] -uvloop = ["uvloop (>=0.15.2)"] -jupyter = ["tokenize-rt (>=3.2.0)", "ipython (>=7.8.0)"] -d = ["aiohttp (>=3.7.4)"] colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" @@ -74,12 +66,12 @@ six = ">=1.9.0" webencodings = "*" [package.extras] -dev = ["mypy (==0.961)", "black (==22.3.0)", "wheel (==0.37.1)", "twine (==4.0.1)", "tox (==3.25.0)", "Sphinx (==4.3.2)", "pytest (==7.1.2)", "pip-tools (==6.6.2)", "hashin (==0.17.0)", "flake8 (==4.0.1)", "build (==0.8.0)"] css = ["tinycss2 (>=1.1.0,<1.2)"] +dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "certifi" -version = "2022.6.15" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -148,7 +140,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.4" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -162,7 +154,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.4" +version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false @@ -173,14 +165,14 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] -name = "deprecated" +name = "Deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "dev" @@ -191,7 +183,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["pytest-cov", "pytest", "PyTest-Cov (<2.6)", "PyTest (<5)", "zipp (<2)", "sphinxcontrib-websupport (<2)", "configparser (<5)", "importlib-resources (<4)", "importlib-metadata (<3)", "sphinx (<2)", "bump2version (<1)", "tox"] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] name = "distlib" @@ -218,12 +210,12 @@ optional = false python-versions = ">=3.5,<4.0" [[package]] -name = "faker" -version = "14.1.1" +name = "Faker" +version = "15.0.0" description = "Faker is a Python package that generates fake data for you." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] python-dateutil = ">=2.4" @@ -238,8 +230,8 @@ optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-timeout (>=2.1)", "pytest-cov (>=3)", "pytest (>=7.1.2)", "coverage (>=6.4.2)", "covdefaults (>=2.2)"] -docs = ["sphinx-autodoc-typehints (>=1.19.1)", "sphinx (>=5.1.1)", "furo (>=2022.6.21)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -278,8 +270,8 @@ python-versions = ">=3.6" smmap = ">=3.0.1,<6" [[package]] -name = "gitpython" -version = "3.1.27" +name = "GitPython" +version = "3.1.28" description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false @@ -312,8 +304,8 @@ h11 = ">=0.11,<0.13" sniffio = ">=1.0.0,<2.0.0" [package.extras] -socks = ["socksio (>=1.0.0,<2.0.0)"] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" @@ -330,14 +322,14 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -socks = ["socksio (>=1.0.0,<2.0.0)"] -http2 = ["h2 (>=3,<5)"] -cli = ["pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)", "click (>=8.0.0,<9.0.0)"] brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.5.3" +version = "2.5.6" description = "File identification library for Python" category = "dev" optional = false @@ -348,7 +340,7 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -367,8 +359,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -testing = ["importlib-resources (>=1.3)", "pytest-mypy", "pytest-black (>=0.3.7)", "flufl.flake8", "pyfakefs", "pep517", "packaging", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] -docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] [[package]] name = "iniconfig" @@ -380,7 +372,7 @@ python-versions = "*" [[package]] name = "invoke" -version = "1.7.1" +version = "1.7.3" description = "Pythonic task execution" category = "dev" optional = false @@ -395,10 +387,25 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -plugins = ["setuptools"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +plugins = ["setuptools"] requirements_deprecated_finder = ["pip-api", "pipreqs"] -pipfile_deprecated_finder = ["requirementslib", "pipreqs"] + +[[package]] +name = "jaraco.classes" +version = "3.2.3" +description = "Utility functions for Python class constructs" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "jeepney" @@ -409,12 +416,12 @@ optional = false python-versions = ">=3.7" [package.extras] -trio = ["async-generator", "trio"] -test = ["async-timeout", "trio", "testpath", "pytest-asyncio (>=0.17)", "pytest-trio", "pytest"] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] [[package]] name = "keyring" -version = "23.8.2" +version = "23.9.3" description = "Store and access your passwords safely." category = "dev" optional = false @@ -422,13 +429,14 @@ python-versions = ">=3.7" [package.dependencies] importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +"jaraco.classes" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.3)", "pytest-cov", "flake8 (<5)", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "mccabe" @@ -438,6 +446,14 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "more-itertools" +version = "8.14.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -455,11 +471,11 @@ optional = false python-versions = ">=3.7" [package.extras] -default = ["numpy (>=1.19)", "scipy (>=1.5,!=1.6.1)", "matplotlib (>=3.3)", "pandas (>=1.1)"] +default = ["matplotlib (>=3.3)", "numpy (>=1.19)", "pandas (>=1.1)", "scipy (>=1.5,!=1.6.1)"] developer = ["black (==21.5b1)", "pre-commit (>=2.12)"] -doc = ["sphinx (>=4.0,<5.0)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx-gallery (>=0.9,<1.0)", "numpydoc (>=1.1)", "pillow (>=8.2)", "nb2plots (>=0.6)", "texext (>=0.6.6)"] -extra = ["lxml (>=4.5)", "pygraphviz (>=1.7)", "pydot (>=1.4.1)"] -test = ["pytest (>=6.2)", "pytest-cov (>=2.12)", "codecov (>=2.1)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.1)", "pillow (>=8.2)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx (>=4.0,<5.0)", "sphinx-gallery (>=0.9,<1.0)", "texext (>=0.6.6)"] +extra = ["lxml (>=4.5)", "pydot (>=1.4.1)", "pygraphviz (>=1.7)"] +test = ["codecov (>=2.1)", "pytest (>=6.2)", "pytest-cov (>=2.12)"] [[package]] name = "nodeenv" @@ -469,6 +485,9 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "21.3" @@ -482,7 +501,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.10.0" +version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -497,7 +516,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -testing = ["nose", "coverage"] +testing = ["coverage", "nose"] [[package]] name = "platformdirs" @@ -508,8 +527,8 @@ optional = false python-versions = ">=3.7" [package.extras] -test = ["pytest (>=6)", "pytest-mock (>=3.6)", "pytest-cov (>=2.7)", "appdirs (==1.4.4)"] -docs = ["sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=2021.7.5b38)"] +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" @@ -523,8 +542,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -569,7 +588,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.10.0" +version = "1.10.2" description = "Data validation and settings management using python type hints" category = "main" optional = false @@ -591,7 +610,7 @@ optional = false python-versions = ">=3.6" [[package]] -name = "pygithub" +name = "PyGithub" version = "1.55" description = "Use the full Github API v3" category = "dev" @@ -608,7 +627,7 @@ requests = ">=2.14.0" integrations = ["cryptography"] [[package]] -name = "pygments" +name = "Pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" @@ -619,21 +638,21 @@ python-versions = ">=3.6" plugins = ["importlib-metadata"] [[package]] -name = "pyjwt" -version = "2.4.0" +name = "PyJWT" +version = "2.5.0" description = "JSON Web Token implementation in Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] +crypto = ["cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "types-cryptography (>=3.3.21)", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] -docs = ["zope.interface", "sphinx-rtd-theme", "sphinx"] -dev = ["pre-commit", "mypy", "coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)", "cryptography (>=3.3.1)", "zope.interface", "sphinx-rtd-theme", "sphinx"] -crypto = ["cryptography (>=3.3.1)"] [[package]] -name = "pynacl" +name = "PyNaCl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "dev" @@ -644,8 +663,8 @@ python-versions = ">=3.6" cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] +docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pyparsing" @@ -660,14 +679,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.2" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -678,7 +696,7 @@ py = ">=1.8.2" tomli = ">=1.0.0" [package.extras] -testing = ["xmlschema", "requests", "pygments (>=2.7.2)", "nose", "mock", "hypothesis (>=3.56)", "argcomplete"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" @@ -693,11 +711,11 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-trio (>=0.7.0)", "mypy (>=0.931)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "coverage (>=6.2)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" -version = "3.0.0" +version = "4.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -708,7 +726,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-depends" @@ -737,7 +755,7 @@ six = ">=1.5" [[package]] name = "python-gitlab" -version = "3.9.0" +version = "3.10.0" description = "Interact with GitLab API" category = "dev" optional = false @@ -753,7 +771,7 @@ yaml = ["PyYaml (>=5.2)"] [[package]] name = "python-semantic-release" -version = "7.31.4" +version = "7.32.1" description = "Automatic Semantic Versioning for Python projects" category = "dev" optional = false @@ -771,12 +789,13 @@ requests = ">=2.25,<3" semver = ">=2.10,<3" tomlkit = ">=0.10,<1.0" twine = ">=3,<4" +wheel = "*" [package.extras] -dev = ["tox", "isort", "black"] -docs = ["Sphinx (==1.3.6)", "Jinja2 (==3.0.3)"] +dev = ["black", "isort", "tox"] +docs = ["Jinja2 (==3.0.3)", "Sphinx (==1.3.6)"] mypy = ["mypy", "types-requests"] -test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"] +test = ["coverage (>=5,<6)", "mock (==1.3.0)", "pytest (>=5,<6)", "pytest-mock (>=2,<3)", "pytest-xdist (>=1,<2)", "responses (==0.13.3)"] [[package]] name = "pywin32-ctypes" @@ -787,7 +806,7 @@ optional = false python-versions = "*" [[package]] -name = "pyyaml" +name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" @@ -796,7 +815,7 @@ python-versions = ">=3.6" [[package]] name = "readme-renderer" -version = "37.0" +version = "37.2" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "dev" optional = false @@ -825,16 +844,16 @@ idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.9.1" +version = "0.10.0" description = "A utility belt for advanced users of python-requests" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] requests = ">=2.0.1,<3.0.0" @@ -854,7 +873,7 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} idna2008 = ["idna"] [[package]] -name = "secretstorage" +name = "SecretStorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" @@ -873,6 +892,18 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "setuptools" +version = "58.5.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-inline-tabs", "sphinxcontrib-towncrier"] +testing = ["flake8-2020", "jaraco.envs", "jaraco.path (>=3.2.0)", "mock", "paver", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-virtualenv (>=1.2.7)", "pytest-xdist", "sphinx", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -891,11 +922,11 @@ python-versions = ">=3.6" [[package]] name = "sniffio" -version = "1.2.0" +version = "1.3.0" description = "Sniff out which async library your code is running under" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "toml" @@ -915,7 +946,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.4" +version = "0.11.5" description = "Style preserving TOML library" category = "dev" optional = false @@ -923,7 +954,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "tqdm" -version = "4.64.0" +version = "4.64.1" description = "Fast, Extensible Progress Meter" category = "dev" optional = false @@ -933,10 +964,10 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -telegram = ["requests"] -slack = ["slack-sdk"] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] notebook = ["ipywidgets (>=6)"] -dev = ["wheel", "twine", "py-make (>=0.1.0)"] +slack = ["slack-sdk"] +telegram = ["requests"] [[package]] name = "twine" @@ -978,14 +1009,14 @@ python-versions = ">=3.6" click = ">=7.1.1,<9.0.0" [package.extras] -test = ["isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] -all = ["shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "shellingham (>=1.3.0,<2.0.0)"] [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -1008,6 +1039,7 @@ optional = false python-versions = ">=3.6.14,<4.0.0" [package.dependencies] +setuptools = ">=58.2.0,<59.0.0" typer = ">=0.4.0,<0.5.0" unasync = ">=0.5.0,<0.6.0" @@ -1020,8 +1052,8 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +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)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1039,8 +1071,8 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" [package.extras] -testing = ["pytest-timeout (>=1)", "pytest-randomly (>=1)", "pytest-mock (>=2)", "pytest-freezegun (>=0.4.1)", "pytest-env (>=0.6.2)", "pytest (>=4)", "packaging (>=20.0)", "flaky (>=3)", "coverage-enable-subprocess (>=1)", "coverage (>=4)"] -docs = ["towncrier (>=21.3)", "sphinx-rtd-theme (>=0.4.3)", "sphinx-argparse (>=0.2.5)", "sphinx (>=3)", "proselint (>=0.10.2)"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "webencodings" @@ -1050,6 +1082,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "wheel" +version = "0.37.1" +description = "A built-package format for Python" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.extras] +test = ["pytest (>=3.0.0)", "pytest-cov"] + [[package]] name = "wrapt" version = "1.14.1" @@ -1060,65 +1103,60 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.8.1" +version = "3.9.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -testing = ["pytest-mypy (>=0.9.1)", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.3)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] -docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=9)", "sphinx"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2cf192262cd47d8f28afc2db5ae68a3eb7f9db4def8c58e90fb3e6c3fb6f7faf" +content-hash = "33e991b9b52bb9e4cba8659760e2cd833f1a1b112d0367bda5b12e3f46d9e8b1" [metadata.files] anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] black = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] bleach = [ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, ] certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -1207,82 +1245,86 @@ colorama = [ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, - {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, - {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, - {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, -] -deprecated = [ + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, + {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, + {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, + {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, +] +Deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] @@ -1298,9 +1340,9 @@ dotty-dict = [ {file = "dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f"}, {file = "dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15"}, ] -faker = [ - {file = "Faker-14.1.1-py3-none-any.whl", hash = "sha256:15bc9d01b3caf427a459d191d972bbca868f2200fbfc157da6ed09c2a58fbb87"}, - {file = "Faker-14.1.1.tar.gz", hash = "sha256:2d506c09aeafa87296f834fd15c6efa87182320f8bf4f709bd3d882397c18076"}, +Faker = [ + {file = "Faker-15.0.0-py3-none-any.whl", hash = "sha256:84c83f0ac1a2c8ecabd784c501aa0ef1d082d4aee52c3d797d586081c166434c"}, + {file = "Faker-15.0.0.tar.gz", hash = "sha256:245fc7d23470dc57164bd9a59b7b1126e16289ffcf813d88a6c8e9b8a37ea3fb"}, ] filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, @@ -1318,9 +1360,9 @@ gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, ] -gitpython = [ - {file = "GitPython-3.1.27-py3-none-any.whl", hash = "sha256:5b68b000463593e05ff2b261acff0ff0972df8ab1b70d3cdbd41b546c8b8fc3d"}, - {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, +GitPython = [ + {file = "GitPython-3.1.28-py3-none-any.whl", hash = "sha256:77bfbd299d8709f6af7e0c70840ef26e7aff7cf0c1ed53b42dd7fc3a310fcb02"}, + {file = "GitPython-3.1.28.tar.gz", hash = "sha256:6bd3451b8271132f099ceeaf581392eaf6c274af74bb06144307870479d0697c"}, ] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, @@ -1335,12 +1377,12 @@ httpx = [ {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] identify = [ - {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, - {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, + {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, + {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, @@ -1351,25 +1393,33 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] invoke = [ - {file = "invoke-1.7.1-py3-none-any.whl", hash = "sha256:2dc975b4f92be0c0a174ad2d063010c8a1fdb5e9389d69871001118b4fcac4fb"}, - {file = "invoke-1.7.1.tar.gz", hash = "sha256:7b6deaf585eee0a848205d0b8c0014b9bf6f287a8eb798818a642dff1df14b19"}, + {file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"}, + {file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"}, ] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +"jaraco.classes" = [ + {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, + {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, +] jeepney = [ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, ] keyring = [ - {file = "keyring-23.8.2-py3-none-any.whl", hash = "sha256:10d2a8639663fe2090705a00b8c47c687cacdf97598ea9c11456679fa974473a"}, - {file = "keyring-23.8.2.tar.gz", hash = "sha256:0d9973f8891850f1ade5f26aafd06bb16865fbbae3fc56b0defb6a14a2624003"}, + {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, + {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, ] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +more-itertools = [ + {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, + {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, +] 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"}, @@ -1387,8 +1437,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.10.0-py3-none-any.whl", hash = "sha256:aefa80ac32d5bf1f96139dca67cefb69a431beff4e6bf1168468f37d7ab87015"}, - {file = "pathspec-0.10.0.tar.gz", hash = "sha256:01eecd304ba0e6eeed188ae5fa568e99ef10265af7fd9ab737d6412b4ee0ab85"}, + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] pkginfo = [ {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, @@ -1419,60 +1469,60 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydantic = [ - {file = "pydantic-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e34e46dd08dafd4c75b8378efe3eae7d8e5212950fcd894d86c1df2dcfb80fe"}, - {file = "pydantic-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4af55f33ae5be6cccecd4fa462630daffef1f161f60c3f194b24eca705d50748"}, - {file = "pydantic-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1856bc6640aced42886f7ee48f5ed1fa5adf35e34064b5f9532b52d5a3b8a0d3"}, - {file = "pydantic-1.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d73ae7e210929a1b7d288034835dd787e5b0597192d58ab7342bacbeec0f33df"}, - {file = "pydantic-1.10.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1192c17667d21652ab93b5eecd1a776cd0a4e384ea8c331bb830c9d130293af"}, - {file = "pydantic-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:026427be4e251f876e7519a63af37ae5ebb8b593ca8b02180bdc6becd1ea4ef4"}, - {file = "pydantic-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:d1dffae1f219d06a997ec78d1d2daafdbfecf243ad8eb36bfbcbc73e30e17385"}, - {file = "pydantic-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b549eebe8de4e50fc3b4f8c1f9cc2f731d91787fc3f7d031561668377b8679bc"}, - {file = "pydantic-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a0ba8710bfdaddb7424c05ad2dc1da04796003751eac6ad30c218ac1d68a174e"}, - {file = "pydantic-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0985ba95af937389c9ce8d747138417303569cb736bd12469646ef53cd66e1c"}, - {file = "pydantic-1.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d484fbbe6267b6c936a6d005d5170ab553f3f4367348c7e88d3e17f0a7179981"}, - {file = "pydantic-1.10.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9500586151cd56a20bacb8f1082df1b4489000120d1c7ddc44c8b20870e8adbd"}, - {file = "pydantic-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1b5212604aaf5954e9a7cea8f0c60d6dbef996aa7b41edefd329e6b5011ce8cf"}, - {file = "pydantic-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:39212b3853eea165a3cda11075d5b7d09d4291fcbc3c0ecefd23797ee21b29e9"}, - {file = "pydantic-1.10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b3e3aed33fbd9518cf508d5415a58af683743d53dc5e58953973d73605774f34"}, - {file = "pydantic-1.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed4e5c18cac70fadd4cf339f444c4f1795f0876dfd5b70cf0a841890b52f0001"}, - {file = "pydantic-1.10.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45a6d0a9fdaad2a27ea69aec4659705ed8f60a5664e892c73e2b977d8f5166cc"}, - {file = "pydantic-1.10.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:158f1479367da20914961b5406ac3b29dfe1d858ae2af96c444f73543defcf0c"}, - {file = "pydantic-1.10.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:172aaeeaff8fc3ac326fb8a2934a063ca0938586c5fe8848285052de83a240f7"}, - {file = "pydantic-1.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:231b19c010288bfbfdcd3f79df38b5ff893c6547cd8c7d006203435790b22815"}, - {file = "pydantic-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22206c152f9b86c0ee169928f9c24e1c0c566edb2462600b298ccb04860961aa"}, - {file = "pydantic-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ef840ef803ef17a7bd52480eb85faca0eed728d70233fd560f7d1066330247"}, - {file = "pydantic-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f99b4de6936a0f9fe255d1c7fdc447700ddd027c9ad38a612d453ed5fc7d6d0"}, - {file = "pydantic-1.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:134b4fd805737496ce4efd24ce2f8da0e08c66dcfc054fee1a19673eec780f2c"}, - {file = "pydantic-1.10.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c4c76af6ad47bc46cf16bd0e4a5e536a7a2bec0dec14ea08b712daa6645bf293"}, - {file = "pydantic-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e03402b0a6b23a2d0b9ee31e45d80612c95562b5af8b5c900171b9d9015ddc5f"}, - {file = "pydantic-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a3a60fcb5ce08cab593b7978d02db67b8d153e9d582adab7c0b69d7200d78be"}, - {file = "pydantic-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8e5c5a50821c55b76dcf422610225cb7e44685cdd81832d0d504fa8c9343f35"}, - {file = "pydantic-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:645b83297a9428a675c98c1f69a7237a381900e34f23245c0ea73d74e454bf68"}, - {file = "pydantic-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ab3f31f35dc4f8fc85b04d13569e5fdc9de2d3050ae64c1fdc3430dfe7d92d"}, - {file = "pydantic-1.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e290915a0ed53d3c59d6071fc7d2c843ed04c33affcd752dd1f3daa859b44a76"}, - {file = "pydantic-1.10.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:af669da39ede365069dbc5de56564b011e3353f801acdbdd7145002a78abc3d9"}, - {file = "pydantic-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e796f915762dec4678fafc89b1f0441ab9209517a8a682ddb3f988f7ffe0827"}, - {file = "pydantic-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:652727f9e1d3ae30bd8a4dfbebcafd50df45277b97f3deabbbfedcf731f94aa5"}, - {file = "pydantic-1.10.0-py3-none-any.whl", hash = "sha256:4d2b9258f5bd2d129bd4cf2d31f9d40094b9ed6ef64896e2f7a70729b2d599ea"}, - {file = "pydantic-1.10.0.tar.gz", hash = "sha256:e13788fcad1baf5eb3236856b2a9a74f7dac6b3ea7ca1f60a4ad8bad4239cf4c"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, + {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, + {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, + {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, + {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, + {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, + {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, + {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, + {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, + {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, + {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, + {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, + {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, + {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, + {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, + {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, + {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, + {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, + {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, + {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, + {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, + {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, + {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pyflakes = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] -pygithub = [ +PyGithub = [ {file = "PyGithub-1.55-py3-none-any.whl", hash = "sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b"}, {file = "PyGithub-1.55.tar.gz", hash = "sha256:1bbfff9372047ff3f21d5cd8e07720f3dbfdaf6462fcaed9d815f528f1ba7283"}, ] -pygments = [ +Pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] -pyjwt = [ - {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, - {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, +PyJWT = [ + {file = "PyJWT-2.5.0-py3-none-any.whl", hash = "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80"}, + {file = "PyJWT-2.5.0.tar.gz", hash = "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b"}, ] -pynacl = [ +PyNaCl = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, @@ -1489,16 +1539,16 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, + {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"}, ] pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] pytest-depends = [ {file = "pytest-depends-1.0.1.tar.gz", hash = "sha256:90a28e2b87b75b18abd128c94015248544acac20e4392e9921e5a86f93319dfe"}, @@ -1509,18 +1559,18 @@ python-dateutil = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-gitlab = [ - {file = "python-gitlab-3.9.0.tar.gz", hash = "sha256:5fc5e88f81f366e11851cb8b4b9a5b827491ce20ba7585446b74c9b097726ba3"}, - {file = "python_gitlab-3.9.0-py3-none-any.whl", hash = "sha256:ce941f99bf88b6918eea82500ca6206806117f4afe26d4705f4ded2284b35c69"}, + {file = "python-gitlab-3.10.0.tar.gz", hash = "sha256:14930a16fdd7f36f67b9373e7d4d4720e8e374800028380289db3306e9f74614"}, + {file = "python_gitlab-3.10.0-py3-none-any.whl", hash = "sha256:6b5a24d0f479c43c1759cb174cf42116ed27cced31284bedc82d584d860fa238"}, ] python-semantic-release = [ - {file = "python-semantic-release-7.31.4.tar.gz", hash = "sha256:ee00c396b6d394b87b5ba3cf6be0f759f2f23783c2215e3ac4c22dc4bc676d60"}, - {file = "python_semantic_release-7.31.4-py3-none-any.whl", hash = "sha256:05d37cb3e3b6e84770128debede8421b24fceaecfbaa9eb62ffcfc1ea2c857f2"}, + {file = "python-semantic-release-7.32.1.tar.gz", hash = "sha256:ba47100e4ffa74c006529d6a8c22ca98f85ff4c145e8584eb2b17e6830a5116e"}, + {file = "python_semantic_release-7.32.1-py3-none-any.whl", hash = "sha256:9a29a02b3fb33fc318b07bf77c001c62aed9060d148eca25af7f4aa3afb49f4e"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] -pyyaml = [ +PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1528,6 +1578,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1556,22 +1613,22 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] readme-renderer = [ - {file = "readme_renderer-37.0-py3-none-any.whl", hash = "sha256:9fa416704703e509eeb900696751c908ddeb2011319d93700d8f18baff887a69"}, - {file = "readme_renderer-37.0.tar.gz", hash = "sha256:07b7ea234e03e58f77cc222e206e6abb8f4c0435becce5104794ee591f9301c5"}, + {file = "readme_renderer-37.2-py3-none-any.whl", hash = "sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106"}, + {file = "readme_renderer-37.2.tar.gz", hash = "sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] requests-toolbelt = [ - {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, - {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, + {file = "requests-toolbelt-0.10.0.tar.gz", hash = "sha256:f695d6207931200b46c8ef6addbc8a921fb5d77cc4cd209c2e7d39293fcd2b30"}, + {file = "requests_toolbelt-0.10.0-py2.py3-none-any.whl", hash = "sha256:64c6b8c51b515d123f9f708a29743f44eb70c4479440641ed2df8c4dea56d985"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] -secretstorage = [ +SecretStorage = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] @@ -1579,6 +1636,10 @@ semver = [ {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, ] +setuptools = [ + {file = "setuptools-58.5.3-py3-none-any.whl", hash = "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf"}, + {file = "setuptools-58.5.3.tar.gz", hash = "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1588,8 +1649,8 @@ smmap = [ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1600,12 +1661,12 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, - {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, + {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, + {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, ] tqdm = [ - {file = "tqdm-4.64.0-py2.py3-none-any.whl", hash = "sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"}, - {file = "tqdm-4.64.0.tar.gz", hash = "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d"}, + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, ] twine = [ {file = "twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"}, @@ -1642,8 +1703,8 @@ typer = [ {file = "typer-0.4.2.tar.gz", hash = "sha256:b8261c6c0152dd73478b5ba96ba677e5d6948c715c310f7c91079f311f62ec03"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] unasync = [ {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, @@ -1665,6 +1726,10 @@ webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +wheel = [ + {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, + {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, +] 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"}, @@ -1732,6 +1797,6 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, + {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, ] diff --git a/pyproject.toml b/pyproject.toml index 72923359..04c11c1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,15 +20,15 @@ httpx = "^0.23.0" pydantic = "^1.10.0" [tool.poetry.dev-dependencies] -black = "^22.6.0" -Faker = "^14.1.1" +black = "^22.10.0" +Faker = "^15.0.0" flake8 = "^5.0.4" isort = "^5.10.1" pre-commit = "^2.20.0" PyGithub = "^1.55" -pytest = "^7.1.2" +pytest = "^7.1.3" pytest-asyncio = "^0.19.0" -pytest-cov = "^3.0.0" +pytest-cov = "^4.0.0" pytest-depends = "^1.0.1" python-semantic-release = "^7.31.4" unasync-cli = "^0.0.9" From 514827788828799817ea47f3f89f442c163ba5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 9 Oct 2022 05:31:18 +0000 Subject: [PATCH 09/35] chore(deps): fix poetry lock file --- poetry.lock | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 023879c4..8a7adaf7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -164,7 +164,7 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] @@ -1117,7 +1117,6 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" lock-version = "1.1" python-versions = "^3.7" content-hash = "33e991b9b52bb9e4cba8659760e2cd833f1a1b112d0367bda5b12e3f46d9e8b1" -content-hash = "fb0a657db775ef10368ae91af2577d2d68b6b163ec1b0a85bec56e8985f2b0a9" [metadata.files] anyio = [ @@ -1138,7 +1137,6 @@ black = [ {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, @@ -1147,7 +1145,6 @@ black = [ {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, From 20b882cc2501ac39dce5b6fe622d0fd27d946eea Mon Sep 17 00:00:00 2001 From: "sourcery-ai[bot]" <58596630+sourcery-ai[bot]@users.noreply.github.com> Date: Sun, 9 Oct 2022 01:32:21 -0400 Subject: [PATCH 10/35] 'Refactored by Sourcery' (#172) Co-authored-by: Sourcery AI <> --- gotrue/_async/api.py | 3 +-- gotrue/_sync/api.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gotrue/_async/api.py b/gotrue/_async/api.py index 9f2098b6..e0b1a7b9 100644 --- a/gotrue/_async/api.py +++ b/gotrue/_async/api.py @@ -423,8 +423,7 @@ def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: The headers required for a successful request statement with the supabase backend. """ - headers = {**self.headers} - headers["Authorization"] = f"Bearer {jwt}" + headers = {**self.headers, "Authorization": f"Bearer {jwt}"} return headers async def sign_out(self, *, jwt: str) -> None: diff --git a/gotrue/_sync/api.py b/gotrue/_sync/api.py index dfb6c76f..bec3f70b 100644 --- a/gotrue/_sync/api.py +++ b/gotrue/_sync/api.py @@ -423,8 +423,7 @@ def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: The headers required for a successful request statement with the supabase backend. """ - headers = {**self.headers} - headers["Authorization"] = f"Bearer {jwt}" + headers = {**self.headers, "Authorization": f"Bearer {jwt}"} return headers def sign_out(self, *, jwt: str) -> None: From 4cb771352e7f4065b2aac5de1c2b1260a0771268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Tue, 18 Oct 2022 05:55:56 +0000 Subject: [PATCH 11/35] refactor: migrate to implementation as similar as possible to the implementation of gotrue-js The tests still need to be implemented. --- gotrue/__init__.py | 19 +- gotrue/_async/api.py | 642 ---------------- gotrue/_async/client.py | 648 ----------------- gotrue/_async/gotrue_admin_api.py | 144 ++++ gotrue/_async/gotrue_base_api.py | 98 +++ gotrue/_async/gotrue_client.py | 683 ++++++++++++++++++ gotrue/_sync/api.py | 642 ---------------- gotrue/_sync/client.py | 640 ---------------- gotrue/_sync/gotrue_admin_api.py | 144 ++++ gotrue/_sync/gotrue_base_api.py | 98 +++ gotrue/_sync/gotrue_client.py | 683 ++++++++++++++++++ gotrue/constants.py | 14 +- gotrue/errors.py | 115 +++ gotrue/exceptions.py | 51 -- gotrue/helpers.py | 76 +- gotrue/timer.py | 44 ++ gotrue/types.py | 474 ++++++++---- .../test_api_with_auto_confirm_disabled.py | 14 +- .../test_api_with_auto_confirm_enabled.py | 6 +- .../test_client_with_auto_confirm_disabled.py | 6 +- .../test_client_with_auto_confirm_enabled.py | 6 +- .../test_client_with_sign_ups_disabled.py | 12 +- tests/_async/test_provider.py | 4 +- tests/_async/test_subscriptions.py | 4 +- .../test_api_with_auto_confirm_disabled.py | 14 +- .../test_api_with_auto_confirm_enabled.py | 6 +- .../test_client_with_auto_confirm_disabled.py | 6 +- .../test_client_with_auto_confirm_enabled.py | 6 +- .../test_client_with_sign_ups_disabled.py | 12 +- tests/_sync/test_provider.py | 4 +- tests/_sync/test_subscriptions.py | 4 +- 31 files changed, 2480 insertions(+), 2839 deletions(-) delete mode 100644 gotrue/_async/api.py delete mode 100644 gotrue/_async/client.py create mode 100644 gotrue/_async/gotrue_admin_api.py create mode 100644 gotrue/_async/gotrue_base_api.py create mode 100644 gotrue/_async/gotrue_client.py delete mode 100644 gotrue/_sync/api.py delete mode 100644 gotrue/_sync/client.py create mode 100644 gotrue/_sync/gotrue_admin_api.py create mode 100644 gotrue/_sync/gotrue_base_api.py create mode 100644 gotrue/_sync/gotrue_client.py create mode 100644 gotrue/errors.py delete mode 100644 gotrue/exceptions.py create mode 100644 gotrue/timer.py diff --git a/gotrue/__init__.py b/gotrue/__init__.py index 6db087d8..f478859f 100644 --- a/gotrue/__init__.py +++ b/gotrue/__init__.py @@ -2,13 +2,12 @@ __version__ = "0.5.4" -from ._async.api import AsyncGoTrueAPI -from ._async.client import AsyncGoTrueClient -from ._async.storage import AsyncMemoryStorage, AsyncSupportedStorage -from ._sync.api import SyncGoTrueAPI -from ._sync.client import SyncGoTrueClient -from ._sync.storage import SyncMemoryStorage, SyncSupportedStorage -from .types import * - -Client = SyncGoTrueClient -GoTrueAPI = SyncGoTrueAPI +from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI # type: ignore # noqa: F401 +from ._async.gotrue_client import AsyncGoTrueClient # type: ignore # noqa: F401 +from ._async.storage import AsyncMemoryStorage # type: ignore # noqa: F401 +from ._async.storage import AsyncSupportedStorage # type: ignore # noqa: F401 +from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI # type: ignore # noqa: F401 +from ._sync.gotrue_client import SyncGoTrueClient # type: ignore # noqa: F401 +from ._sync.storage import SyncMemoryStorage # type: ignore # noqa: F401 +from ._sync.storage import SyncSupportedStorage # type: ignore # noqa: F401 +from .types import * # type: ignore # noqa: F401, F403 diff --git a/gotrue/_async/api.py b/gotrue/_async/api.py deleted file mode 100644 index e0b1a7b9..00000000 --- a/gotrue/_async/api.py +++ /dev/null @@ -1,642 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict, List, Optional, Union - -from pydantic import parse_obj_as - -from ..exceptions import APIError -from ..helpers import check_response, encode_uri_component -from ..http_clients import AsyncClient -from ..types import ( - CookieOptions, - LinkType, - Provider, - Session, - User, - UserAttributes, - determine_session_or_user_model_from_response, -) - - -class AsyncGoTrueAPI: - def __init__( - self, - *, - url: str, - headers: Dict[str, str], - cookie_options: CookieOptions, - http_client: Optional[AsyncClient] = None, - ) -> None: - """Initialise API class.""" - self.url = url - self.headers = headers - self.cookie_options = cookie_options - self.http_client = http_client or AsyncClient() - - async def __aenter__(self) -> AsyncGoTrueAPI: - return self - - async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: - await self.close() - - async def close(self) -> None: - await self.http_client.aclose() - - async def create_user(self, *, attributes: UserAttributes) -> User: - """Creates a new user. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Parameters - ---------- - attributes: UserAttributes - The data you want to create the user with. - - Returns - ------- - response : User - The created user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = attributes.dict() - url = f"{self.url}/admin/users" - response = await self.http_client.post(url, json=data, headers=headers) - return User.parse_response(response) - - async def list_users(self) -> List[User]: - """Get a list of users. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Returns - ------- - response : List[User] - A list of users - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - url = f"{self.url}/admin/users" - response = await self.http_client.get(url, headers=headers) - check_response(response) - users = response.json().get("users") - if users is None: - raise APIError("No users found in response", 400) - if not isinstance(users, list): - raise APIError("Expected a list of users", 400) - return parse_obj_as(List[User], users) - - async def sign_up_with_email( - self, - *, - email: str, - password: str, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Creates a new user using their email address. - - Parameters - ---------- - email : str - The email address of the user. - password : str - The password of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "password": password, "data": data} - url = f"{self.url}/signup{query_string}" - response = await self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - async def sign_in_with_email( - self, - *, - email: str, - password: str, - redirect_to: Optional[str] = None, - ) -> Session: - """Logs in an existing user using their email address. - - Parameters - ---------- - email : str - The email address of the user. - password : str - The password of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "?grant_type=password" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string += f"&redirect_to={redirect_to_encoded}" - data = {"email": email, "password": password} - url = f"{self.url}/token{query_string}" - response = await self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - async def sign_up_with_phone( - self, - *, - phone: str, - password: str, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Signs up a new user using their phone number and a password. - - Parameters - ---------- - phone : str - The phone number of the user. - password : str - The password of the user. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = {"phone": phone, "password": password, "data": data} - url = f"{self.url}/signup" - response = await self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - async def sign_in_with_phone( - self, - *, - phone: str, - password: str, - ) -> Session: - """Logs in an existing user using their phone number and password. - - Parameters - ---------- - phone : str - The phone number of the user. - password : str - The password of the user. - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - data = {"phone": phone, "password": password} - url = f"{self.url}/token?grant_type=password" - headers = self.headers - response = await self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - async def send_magic_link_email( - self, - *, - email: str, - create_user: bool, - redirect_to: Optional[str] = None, - ) -> None: - """Sends a magic login link to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "create_user": create_user} - url = f"{self.url}/magiclink{query_string}" - response = await self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - async def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: - """Sends a mobile OTP via SMS. - Will register the account if it doesn't already exist - - Parameters - ---------- - phone : str - The user's phone number WITH international prefix - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = {"phone": phone, "create_user": create_user} - url = f"{self.url}/otp" - response = await self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - async def verify_mobile_otp( - self, - *, - phone: str, - token: str, - redirect_to: Optional[str] = None, - ) -> Union[Session, User]: - """Send User supplied Mobile OTP to be verified - - Parameters - ---------- - phone : str - The user's phone number WITH international prefix - token : str - Token that user was sent to their mobile phone - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = { - "phone": phone, - "token": token, - "type": "sms", - } - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - data["redirect_to"] = redirect_to_encoded - url = f"{self.url}/verify" - response = await self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - async def invite_user_by_email( - self, - *, - email: str, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> User: - """Sends an invite link to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "data": data} - url = f"{self.url}/invite{query_string}" - response = await self.http_client.post(url, json=data, headers=headers) - return User.parse_response(response) - - async def reset_password_for_email( - self, - *, - email: str, - redirect_to: Optional[str] = None, - ) -> None: - """Sends a reset request to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email} - url = f"{self.url}/recover{query_string}" - response = await self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: - """Create temporary object. - - Create a temporary object with all configured headers and adds the - Authorization token to be used on request methods. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - - Returns - ------- - headers : dict of str - The headers required for a successful request statement with the - supabase backend. - """ - headers = {**self.headers, "Authorization": f"Bearer {jwt}"} - return headers - - async def sign_out(self, *, jwt: str) -> None: - """Removes a logged-in session. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/logout" - await self.http_client.post(url, headers=headers) - - async def get_url_for_provider( - self, - *, - provider: Provider, - redirect_to: Optional[str] = None, - scopes: Optional[str] = None, - ) -> str: - """Generates the relevant login URL for a third-party provider. - - Parameters - ---------- - provider : Provider - One of the providers supported by GoTrue. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - scopes : Optional[str] - A space-separated list of scopes granted to the OAuth application. - - Returns - ------- - url : str - The URL to redirect the user to. - - Raises - ------ - error : APIError - If an error occurs - """ - url_params = [f"provider={encode_uri_component(provider)}"] - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - url_params.append(f"redirect_to={redirect_to_encoded}") - if scopes: - url_params.append(f"scopes={encode_uri_component(scopes)}") - return f"{self.url}/authorize?{'&'.join(url_params)}" - - async def get_user(self, *, jwt: str) -> User: - """Gets the user details. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/user" - response = await self.http_client.get(url, headers=headers) - return User.parse_response(response) - - async def update_user( - self, - *, - jwt: str, - attributes: UserAttributes, - ) -> User: - """ - Updates the user data. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - attributes : UserAttributes - The data you want to update. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - data = attributes.dict() - url = f"{self.url}/user" - response = await self.http_client.put(url, json=data, headers=headers) - return User.parse_response(response) - - async def delete_user(self, *, uid: str, jwt: str) -> None: - """Delete a user. Requires a `service_role` key. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Parameters - ---------- - uid : str - The user uid you want to remove. - jwt : str - A valid, logged-in JWT. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/admin/users/{uid}" - response = await self.http_client.delete(url, headers=headers) - return check_response(response) - - async def refresh_access_token(self, *, refresh_token: str) -> Session: - """Generates a new JWT. - - Parameters - ---------- - refresh_token : str - A valid refresh token that was returned on login. - - Returns - ------- - response : Session - A session - - Raises - ------ - error : APIError - If an error occurs - """ - data = {"refresh_token": refresh_token} - url = f"{self.url}/token?grant_type=refresh_token" - headers = self.headers - response = await self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - async def generate_link( - self, - *, - type: LinkType, - email: str, - password: Optional[str] = None, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """ - Generates links to be sent via email or other. - - Parameters - ---------- - type : LinkType - The link type ("signup" or "magiclink" or "recovery" or "invite"). - email : str - The user's email. - password : Optional[str] - User password. For signup only. - redirect_to : Optional[str] - The link type ("signup" or "magiclink" or "recovery" or "invite"). - data : Optional[Dict[str, Any]] - Optional user metadata. For signup only. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = { - "type": type, - "email": email, - "data": data, - } - if password: - data["password"] = password - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - data["redirect_to"] = redirect_to_encoded - url = f"{self.url}/admin/generate_link" - response = await self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - async def set_auth_cookie(self, *, req, res): - """Stub for parity with JS api.""" - raise NotImplementedError("set_auth_cookie not implemented.") - - async def get_user_by_cookie(self, *, req): - """Stub for parity with JS api.""" - raise NotImplementedError("get_user_by_cookie not implemented.") diff --git a/gotrue/_async/client.py b/gotrue/_async/client.py deleted file mode 100644 index 6073cc71..00000000 --- a/gotrue/_async/client.py +++ /dev/null @@ -1,648 +0,0 @@ -from __future__ import annotations - -from functools import partial -from json import dumps, loads -from threading import Timer -from time import time -from typing import Any, Callable, Dict, Optional, Tuple, Union, cast -from urllib.parse import parse_qs, urlparse -from uuid import uuid4 - -from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY -from ..exceptions import APIError -from ..types import ( - AuthChangeEvent, - CookieOptions, - Provider, - Session, - Subscription, - User, - UserAttributes, - UserAttributesDict, -) -from .api import AsyncGoTrueAPI -from .storage import AsyncMemoryStorage, AsyncSupportedStorage - - -class AsyncGoTrueClient: - def __init__( - self, - *, - url: str = GOTRUE_URL, - headers: Dict[str, str] = {}, - auto_refresh_token: bool = True, - persist_session: bool = True, - local_storage: AsyncSupportedStorage = AsyncMemoryStorage(), - cookie_options: CookieOptions = CookieOptions.parse_obj(COOKIE_OPTIONS), - api: Optional[AsyncGoTrueAPI] = None, - replace_default_headers: bool = False, - ) -> None: - """Create a new client - - url : str - The URL of the GoTrue server. - headers : Dict[str, str] - Any additional headers to send to the GoTrue server. - auto_refresh_token : bool - Set to "true" if you want to automatically refresh the token before - expiring. - persist_session : bool - Set to "true" if you want to automatically save the user session - into local storage. - local_storage : SupportedStorage - The storage engine to use for persisting the session. - cookie_options : CookieOptions - The options for the cookie. - """ - if url.startswith("http://"): - print( - "Warning:\n\nDO NOT USE HTTP IN PRODUCTION FOR GOTRUE EVER!\n" - "GoTrue REQUIRES HTTPS to work securely." - ) - self.state_change_emitters: Dict[str, Subscription] = {} - self.refresh_token_timer: Optional[Timer] = None - self.current_user: Optional[User] = None - self.current_session: Optional[Session] = None - self.auto_refresh_token = auto_refresh_token - self.persist_session = persist_session - self.local_storage = local_storage - empty_or_default_headers = {} if replace_default_headers else DEFAULT_HEADERS - args = { - "url": url, - "headers": {**empty_or_default_headers, **headers}, - "cookie_options": cookie_options, - } - self.api = api or AsyncGoTrueAPI(**args) - - async def __aenter__(self) -> AsyncGoTrueClient: - return self - - async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: - await self.close() - - async def close(self) -> None: - await self.api.close() - - async def init_recover(self) -> None: - """Recover the current session from local storage.""" - await self._recover_session() - await self._recover_and_refresh() - - async def sign_up( - self, - *, - email: Optional[str] = None, - phone: Optional[str] = None, - password: Optional[str] = None, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Creates a new user. If email and phone are provided, email will be - used and phone will be ignored. - - Parameters - --------- - email : Optional[str] - The user's email address. - phone : Optional[str] - The user's phone number. - password : Optional[str] - The user's password. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - await self._remove_session() - - if email and password: - response = await self.api.sign_up_with_email( - email=email, - password=password, - redirect_to=redirect_to, - data=data, - ) - elif phone and password: - response = await self.api.sign_up_with_phone( - phone=phone, password=password, data=data - ) - elif not password: - raise ValueError("Password must be defined, can't be None.") - else: - raise ValueError("Email or phone must be defined, both can't be None.") - - if isinstance(response, Session): - # The user has confirmed their email or the underlying DB doesn't - # require email confirmation. - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - async def sign_in( - self, - *, - email: Optional[str] = None, - phone: Optional[str] = None, - password: Optional[str] = None, - refresh_token: Optional[str] = None, - provider: Optional[Provider] = None, - redirect_to: Optional[str] = None, - scopes: Optional[str] = None, - create_user: bool = False, - ) -> Optional[Union[Session, str]]: - """Log in an existing user, or login via a third-party provider. - If email and phone are provided, email will be used and phone will be ignored. - - Parameters - --------- - email : Optional[str] - The user's email address. - phone : Optional[str] - The user's phone number. - password : Optional[str] - The user's password. - refresh_token : Optional[str] - A valid refresh token that was returned on login. - provider : Optional[Provider] - One of the providers supported by GoTrue. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - scopes : Optional[str] - A space-separated list of scopes granted to the OAuth application. - - Returns - ------- - response : Optional[Union[Session, str]] - If only email are provided between the email and password, - None is returned and send magic link to email - - If email and password are provided, a logged-in session is returned. - - If only phone are provided between the phone and password, - None is returned and send message to phone - - If phone and password are provided, a logged-in session is returned. - - If refresh_token is provided, a logged-in session is returned. - - If provider is provided, an redirect URL is returned. - - Otherwise, error is raised. - - Raises - ------ - error : APIError - If an error occurs - """ - await self._remove_session() - if email: - if password: - response = await self._handle_email_sign_in( - email=email, - password=password, - redirect_to=redirect_to, - ) - else: - response = await self.api.send_magic_link_email( - email=email, create_user=create_user - ) - elif phone: - if password: - response = await self._handle_phone_sign_in( - phone=phone, password=password - ) - else: - response = await self.api.send_mobile_otp( - phone=phone, create_user=create_user - ) - elif refresh_token: - # current_session and current_user will be updated to latest - # on _call_refresh_token using the passed refresh_token - await self._call_refresh_token(refresh_token=refresh_token) - response = self.current_session - elif provider: - response = await self._handle_provider_sign_in( - provider=provider, - redirect_to=redirect_to, - scopes=scopes, - ) - else: - raise ValueError( - "Email, phone, refresh_token, or provider must be defined, " - "all can't be None." - ) - return response - - async def verify_otp( - self, - *, - phone: str, - token: str, - redirect_to: Optional[str] = None, - ) -> Union[Session, User]: - """Log in a user given a User supplied OTP received via mobile. - - Parameters - ---------- - phone : str - The user's phone number. - token : str - The user's OTP. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - await self._remove_session() - response = await self.api.verify_mobile_otp( - phone=phone, - token=token, - redirect_to=redirect_to, - ) - if isinstance(response, Session): - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def user(self) -> Optional[User]: - """Returns the user data, if there is a logged in user.""" - return self.current_user - - def session(self) -> Optional[Session]: - """Returns the session data, if there is an active session.""" - return self.current_session - - async def refresh_session(self) -> Session: - """Force refreshes the session. - - Force refreshes the session including the user data incase it was - updated in a different session. - """ - if not self.current_session: - raise ValueError("Not logged in.") - return await self._call_refresh_token() - - async def update( - self, *, attributes: Union[UserAttributesDict, UserAttributes] - ) -> User: - """Updates user data, if there is a logged in user. - - Parameters - ---------- - attributes : UserAttributesDict | UserAttributes - Attributes to update, could be: email, password, email_change_token, data - - Returns - ------- - response : User - The updated user data. - - Raises - ------ - error : APIError - If an error occurs - """ - if not self.current_session: - raise ValueError("Not logged in.") - - if isinstance(attributes, dict): - attributes_to_update = UserAttributes(**attributes) - else: - attributes_to_update = attributes - - response = await self.api.update_user( - jwt=self.current_session.access_token, - attributes=attributes_to_update, - ) - self.current_session.user = response - await self._save_session(session=self.current_session) - self._notify_all_subscribers(event=AuthChangeEvent.USER_UPDATED) - return response - - async def set_session(self, *, refresh_token: str) -> Session: - """Sets the session data from refresh_token and returns current Session - - Parameters - ---------- - refresh_token : str - A JWT token - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - response = await self.api.refresh_access_token(refresh_token=refresh_token) - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - async def set_auth(self, *, access_token: str) -> Session: - """Overrides the JWT on the current client. The JWT will then be sent in - all subsequent network requests. - - Parameters - ---------- - access_token : str - A JWT token - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - session = Session( - access_token=access_token, - token_type="bearer", - user=None, - expires_in=None, - expires_at=None, - refresh_token=None, - provider_token=None, - ) - if self.current_session: - session.expires_in = self.current_session.expires_in - session.expires_at = self.current_session.expires_at - session.refresh_token = self.current_session.refresh_token - session.provider_token = self.current_session.provider_token - await self._save_session(session=session) - return session - - async def get_session_from_url( - self, - *, - url: str, - store_session: bool = False, - ) -> Session: - """Gets the session data from a URL string. - - Parameters - ---------- - url : str - The URL string. - store_session : bool - Optionally store the session in the browser - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - data = urlparse(url) - query = parse_qs(data.query) - error_description = query.get("error_description") - access_token = query.get("access_token") - expires_in = query.get("expires_in") - refresh_token = query.get("refresh_token") - token_type = query.get("token_type") - if error_description: - raise APIError(error_description[0], 400) - if not access_token or not access_token[0]: - raise APIError("No access_token detected.", 400) - if not refresh_token or not refresh_token[0]: - raise APIError("No refresh_token detected.", 400) - if not token_type or not token_type[0]: - raise APIError("No token_type detected.", 400) - if not expires_in or not expires_in[0]: - raise APIError("No expires_in detected.", 400) - try: - expires_at = round(time()) + int(expires_in[0]) - except ValueError: - raise APIError("Invalid expires_in.", 400) - response = await self.api.get_user(jwt=access_token[0]) - provider_token = query.get("provider_token") - session = Session( - access_token=access_token[0], - token_type=token_type[0], - user=response, - expires_in=int(expires_in[0]), - expires_at=expires_at, - refresh_token=refresh_token[0], - provider_token=provider_token[0] if provider_token else None, - ) - if store_session: - await self._save_session(session=session) - recovery_mode = query.get("type") - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - if recovery_mode and recovery_mode[0] == "recovery": - self._notify_all_subscribers(event=AuthChangeEvent.PASSWORD_RECOVERY) - return session - - async def sign_out(self) -> None: - """Log the user out.""" - access_token: Optional[str] = None - if self.current_session: - access_token = self.current_session.access_token - await self._remove_session() - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_OUT) - if access_token: - await self.api.sign_out(jwt=access_token) - - def _unsubscribe(self, *, id: str) -> None: - """Unsubscribe from a subscription.""" - self.state_change_emitters.pop(id) - - def on_auth_state_change( - self, - *, - callback: Callable[[AuthChangeEvent, Optional[Session]], None], - ) -> Subscription: - """Receive a notification every time an auth event happens. - - Parameters - ---------- - callback : Callable[[AuthChangeEvent, Optional[Session]], None] - The callback to call when an auth event happens. - - Returns - ------- - subscription : Subscription - A subscription object which can be used to unsubscribe itself. - - Raises - ------ - error : APIError - If an error occurs - """ - unique_id = uuid4() - subscription = Subscription( - id=unique_id, - callback=callback, - unsubscribe=partial(self._unsubscribe, id=unique_id.hex), - ) - self.state_change_emitters[unique_id.hex] = subscription - return subscription - - async def _handle_email_sign_in( - self, - *, - email: str, - password: str, - redirect_to: Optional[str], - ) -> Session: - """Sign in with email and password.""" - response = await self.api.sign_in_with_email( - email=email, - password=password, - redirect_to=redirect_to, - ) - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - async def _handle_phone_sign_in(self, *, phone: str, password: str) -> Session: - """Sign in with phone and password.""" - response = await self.api.sign_in_with_phone(phone=phone, password=password) - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - async def _handle_provider_sign_in( - self, - *, - provider: Provider, - redirect_to: Optional[str], - scopes: Optional[str], - ) -> str: - """Sign in with provider.""" - return await self.api.get_url_for_provider( - provider=provider, - redirect_to=redirect_to, - scopes=scopes, - ) - - async def _recover_common(self) -> Optional[Tuple[Session, int, int]]: - """Recover common logic""" - json = await self.local_storage.get_item(STORAGE_KEY) - if not json: - return - data = loads(json) - session_raw = data.get("session") - expires_at_raw = data.get("expires_at") - if ( - expires_at_raw - and isinstance(expires_at_raw, int) - and session_raw - and isinstance(session_raw, dict) - ): - session = Session.parse_obj(session_raw) - expires_at = int(expires_at_raw) - time_now = round(time()) - return session, expires_at, time_now - - async def _recover_session(self) -> None: - """Attempts to get the session from LocalStorage""" - result = await self._recover_common() - if not result: - return - session, expires_at, time_now = result - if expires_at >= time_now: - await self._save_session(session=session) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - - async def _recover_and_refresh(self) -> None: - """Recovers the session from LocalStorage and refreshes""" - result = await self._recover_common() - if not result: - return - session, expires_at, time_now = result - if expires_at < time_now and self.auto_refresh_token and session.refresh_token: - try: - await self._call_refresh_token(refresh_token=session.refresh_token) - except APIError: - await self._remove_session() - elif expires_at < time_now or not session or not session.user: - await self._remove_session() - else: - await self._save_session(session=session) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - - async def _call_refresh_token( - self, *, refresh_token: Optional[str] = None - ) -> Session: - if refresh_token is None: - if self.current_session: - refresh_token = self.current_session.refresh_token - else: - raise ValueError("No current session and refresh_token not supplied.") - response = await self.api.refresh_access_token( - refresh_token=cast(str, refresh_token) - ) - await self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.TOKEN_REFRESHED) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def _notify_all_subscribers(self, *, event: AuthChangeEvent) -> None: - """Notify all subscribers that auth event happened.""" - for value in self.state_change_emitters.values(): - value.callback(event, self.current_session) - - async def _save_session(self, *, session: Session) -> None: - """Save session to client.""" - self.current_session = session - self.current_user = session.user - if session.expires_at: - time_now = round(time()) - expire_in = session.expires_at - time_now - refresh_duration_before_expires = 60 if expire_in > 60 else 0.5 - self._start_auto_refresh_token( - value=(expire_in - refresh_duration_before_expires) * 1000 - ) - if self.persist_session and session.expires_at: - await self._persist_session(session=session) - - async def _persist_session(self, *, session: Session) -> None: - data = {"session": session.dict(), "expires_at": session.expires_at} - await self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str)) - - async def _remove_session(self) -> None: - """Remove the session.""" - self.current_session = None - self.current_user = None - if self.refresh_token_timer: - self.refresh_token_timer.cancel() - await self.local_storage.remove_item(STORAGE_KEY) - - def _start_auto_refresh_token(self, *, value: float) -> None: - if self.refresh_token_timer: - self.refresh_token_timer.cancel() - if value <= 0 or not self.auto_refresh_token: - return - self.refresh_token_timer = Timer(value, self._call_refresh_token) - self.refresh_token_timer.start() diff --git a/gotrue/_async/gotrue_admin_api.py b/gotrue/_async/gotrue_admin_api.py new file mode 100644 index 00000000..7a568cf1 --- /dev/null +++ b/gotrue/_async/gotrue_admin_api.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from typing import Dict, List, Union + +from ..helpers import parse_link_response, parse_user_response +from ..http_clients import AsyncClient +from ..types import ( + AdminUserAttributes, + GenerateLinkParams, + GenerateLinkResponse, + Options, + User, + UserResponse, +) +from .gotrue_base_api import AsyncGoTrueBaseAPI + + +class AsyncGoTrueAdminAPI(AsyncGoTrueBaseAPI): + def __init__( + self, + *, + url: str = "", + headers: Dict[str, str] = {}, + http_client: Union[AsyncClient, None] = None, + ) -> None: + AsyncGoTrueBaseAPI.__init__( + self, + url=url, + headers=headers, + http_client=http_client, + ) + + async def sign_out(self, jwt: str) -> None: + """ + Removes a logged-in session. + """ + return await self._request("POST", "logout", jwt=jwt) + + async def invite_user_by_email( + self, + email: str, + options: Options = {}, + ) -> UserResponse: + """ + Sends an invite link to an email address. + """ + return await self._request( + "POST", + "invite", + body={"email": email, "data": options.get("data")}, + redirect_to=options.get("redirect_to"), + xform=parse_user_response, + ) + + async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkResponse: + """ + Generates email links and OTPs to be sent via a custom email provider. + """ + return await self._request( + "POST", + "admin/generate_link", + body={ + "type": params.get("type"), + "email": params.get("email"), + "password": params.get("password"), + "new_email": params.get("new_email"), + "data": params.get("options", {}).get("data"), + }, + redirect_to=params.get("options", {}).get("redirect_to"), + xform=parse_link_response, + ) + + # User Admin API + + async def create_user(self, attributes: AdminUserAttributes) -> UserResponse: + """ + Creates a new user. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return await self._request( + "POST", + "admin/users", + body=attributes, + xform=parse_user_response, + ) + + async def list_users(self) -> List[User]: + """ + Get a list of users. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return await self._request( + "GET", + "admin/users", + xform=lambda data: [User.parse_obj(user) for user in data], + ) + + async def get_user_by_id(self, uid: str) -> UserResponse: + """ + Get user by id. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return await self._request( + "GET", + f"admin/users/{uid}", + xform=parse_user_response, + ) + + async def update_user_by_id( + self, + uid: str, + attributes: AdminUserAttributes, + ) -> UserResponse: + """ + Updates the user data. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return await self._request( + "PUT", + f"admin/users/{uid}", + body=attributes, + xform=parse_user_response, + ) + + async def delete_user(self, id: str) -> UserResponse: + """ + Delete a user. Requires a `service_role` key. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return await self._request( + "DELETE", + f"admin/users/{id}", + xform=parse_user_response, + ) diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py new file mode 100644 index 00000000..89ed39d7 --- /dev/null +++ b/gotrue/_async/gotrue_base_api.py @@ -0,0 +1,98 @@ +from __future__ import annotations +from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload + +from pydantic import BaseModel +from typing_extensions import Self + +from ..helpers import handle_exception +from ..http_clients import AsyncClient + +T = TypeVar("T") + + +class AsyncGoTrueBaseAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + http_client: Union[AsyncClient, None], + ): + self._url = url + self._headers = headers + self._http_client = http_client or AsyncClient() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + await self.close() + + async def close(self) -> None: + await self._http_client.aclose() + + @overload + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + xform: Callable[[Any], T], + ) -> T: + ... + + @overload + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + ) -> None: + ... + + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + xform: Union[Callable[[Any], T], None] = None, + ) -> Union[T, None]: + url = f"{self._url}/{path}" + headers = {**self._headers, **(headers or {})} + if jwt: + headers["Authorization"] = f"Bearer {jwt}" + query = query or {} + if redirect_to: + query["redirect_to"] = redirect_to + try: + response = await self._http_client.request( + method, + url, + headers=headers, + params=query, + json=body.dict() if isinstance(body, BaseModel) else body, + ) + response.raise_for_status() + result = response if no_resolve_json else response.json() + if xform: + return xform(result) + except Exception as e: + raise handle_exception(e) diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py new file mode 100644 index 00000000..a19772ef --- /dev/null +++ b/gotrue/_async/gotrue_client.py @@ -0,0 +1,683 @@ +from __future__ import annotations + +from base64 import b64decode +from json import loads +from time import time +from typing import Callable, Dict, List, Tuple, Union +from urllib.parse import parse_qs, quote, urlencode, urlparse +from uuid import uuid4 + +from ..constants import ( + DEFAULT_HEADERS, + EXPIRY_MARGIN, + GOTRUE_URL, + MAX_RETRIES, + RETRY_INTERVAL, + STORAGE_KEY, +) +from ..errors import ( + AuthImplicitGrantRedirectError, + AuthInvalidCredentialsError, + AuthRetryableError, + AuthSessionMissingError, +) +from ..helpers import parse_auth_response, parse_user_response +from ..http_clients import AsyncClient +from ..timer import Timer +from ..types import ( + AuthChangeEvent, + AuthResponse, + OAuthResponse, + Options, + Provider, + Session, + SignInWithOAuthCredentials, + SignInWithPasswordCredentials, + SignInWithPasswordlessCredentials, + SignUpWithPasswordCredentials, + Subscription, + UserAttributes, + UserResponse, + VerifyOtpParams, +) +from .gotrue_admin_api import AsyncGoTrueAdminAPI +from .gotrue_base_api import AsyncGoTrueBaseAPI +from .storage import AsyncMemoryStorage, AsyncSupportedStorage + + +class AsyncGoTrueClient(AsyncGoTrueBaseAPI): + def __init__( + self, + *, + url: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + storage_key: Union[str, None] = None, + auto_refresh_token: bool = True, + persist_session: bool = True, + storage: Union[AsyncSupportedStorage, None] = None, + http_client: Union[AsyncClient, None] = None, + ) -> None: + AsyncGoTrueBaseAPI.__init__( + self, + url=url or GOTRUE_URL, + headers=headers or DEFAULT_HEADERS, + http_client=http_client, + ) + self._storage_key = storage_key or STORAGE_KEY + self._auto_refresh_token = auto_refresh_token + self._persist_session = persist_session + self._storage = storage or AsyncMemoryStorage() + self._in_memory_session: Union[Session, None] = None + self._refresh_token_timer: Union[Timer, None] = None + self._network_retries = 0 + self._state_change_emitters: Dict[str, Subscription] = {} + + self.admin = AsyncGoTrueAdminAPI( + url=self._url, + headers=self._headers, + http_client=self._http_client, + ) + + # Initializations + + async def initialize(self, *, url: Union[str, None] = None) -> None: + if url and self._is_implicit_grant_flow(url): + await self.initialize_from_url(url) + else: + await self.initialize_from_storage() + + async def initialize_from_storage(self) -> None: + return await self._recover_and_refresh() + + async def initialize_from_url(self, url: str) -> None: + try: + if self._is_implicit_grant_flow(url): + session, redirect_type = await self._get_session_from_url(url) + await self._save_session(session) + self._notify_all_subscribers("SIGNED_IN", session) + if redirect_type == "recovery": + self._notify_all_subscribers("PASSWORD_RECOVERY", session) + except Exception as e: + await self._remove_session() + raise e + + # Public methods + + async def sign_up( + self, + credentials: SignUpWithPasswordCredentials, + ) -> AuthResponse: + """ + Creates a new user. + """ + await self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + password = credentials.get("password") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + data = options.get("data") + captcha_token = options.get("captcha_token") + if email: + response = await self._request( + "POST", + "signup", + body={ + "email": email, + "password": password, + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + redirect_to=redirect_to, + xform=parse_auth_response, + ) + elif phone: + response = await self._request( + "POST", + "signup", + body={ + "phone": phone, + "password": password, + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_response, + ) + else: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number and a password" + ) + if response.session: + await self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + async def sign_in_with_password( + self, + credentials: SignInWithPasswordCredentials, + ) -> AuthResponse: + """ + Log in an existing user with an email or phone and password. + """ + await self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + password = credentials.get("password") + options = credentials.get("options", {}) + captcha_token = options.get("captcha_token") + if email: + response = await self._request( + "POST", + "token", + body={ + "email": email, + "password": password, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "password", + }, + xform=parse_auth_response, + ) + elif phone: + response = await self._request( + "POST", + "token", + body={ + "phone": phone, + "password": password, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "password", + }, + xform=parse_auth_response, + ) + else: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number and a password" + ) + if response.session: + await self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + async def sign_in_with_oauth( + self, + credentials: SignInWithOAuthCredentials, + ) -> OAuthResponse: + """ + Log in an existing user via a third-party provider. + """ + await self._remove_session() + provider = credentials.get("provider") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + scopes = options.get("scopes") + params = options.get("query_params", {}) + if redirect_to: + params["redirect_to"] = redirect_to + if scopes: + params["scopes"] = scopes + url = self._get_url_for_provider(provider, params) + return OAuthResponse(provider=provider, url=url) + + async def sign_in_with_otp( + self, + credentials: SignInWithPasswordlessCredentials, + ) -> AuthResponse: + """ + Log in a user using magiclink or a one-time password (OTP). + + If the `{{ .ConfirmationURL }}` variable is specified in + the email template, a magiclink will be sent. + + If the `{{ .Token }}` variable is specified in the email + template, an OTP will be sent. + + If you're using phone sign-ins, only an OTP will be sent. + You won't be able to send a magiclink for phone sign-ins. + """ + await self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + options = credentials.get("options", {}) + email_redirect_to = options.get("email_redirect_to") + should_create_user = options.get("create_user", True) + data = options.get("data") + captcha_token = options.get("captcha_token") + if email: + return await self._request( + "POST", + "otp", + body={ + "email": email, + "data": data, + "create_user": should_create_user, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + redirect_to=email_redirect_to, + xform=parse_auth_response, + ) + if phone: + return await self._request( + "POST", + "otp", + body={ + "phone": phone, + "data": data, + "create_user": should_create_user, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + async def verify_otp(self, params: VerifyOtpParams) -> AuthResponse: + """ + Log in a user given a User supplied OTP received via mobile. + """ + await self._remove_session() + response = await self._request( + "POST", + "verify", + body={ + "gotrue_meta_security": { + "captcha_token": params.get("options", {}).get("captcha_token"), + }, + **params, + }, + redirect_to=params.get("options", {}).get("redirect_to"), + xform=parse_auth_response, + ) + if response.session: + await self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + async def get_session(self) -> Union[Session, None]: + """ + Returns the session, refreshing it if necessary. + + The session returned can be null if the session is not detected which + can happen in the event a user is not signed-in or has logged out. + """ + current_session: Union[Session, None] = None + if self._persist_session: + maybe_session = await self._storage.get_item(self._storage_key) + current_session = self._get_valid_session(maybe_session) + if not current_session: + await self._remove_session() + else: + current_session = self._in_memory_session + if not current_session: + return None + has_expired = ( + current_session.expires_at <= time() + if current_session.expires_at + else False + ) + if not has_expired: + return current_session + return await self._call_refresh_token(current_session.refresh_token) + + async def get_user(self, jwt: Union[str, None] = None) -> UserResponse: + """ + Gets the current user details if there is an existing session. + + Takes in an optional access token `jwt`. If no `jwt` is provided, + `get_user()` will attempt to get the `jwt` from the current session. + """ + if not jwt: + session = await self.get_session() + if session: + jwt = session.access_token + return await self._request("GET", "user", jwt=jwt, xform=parse_user_response) + + async def update_user(self, attributes: UserAttributes) -> UserResponse: + """ + Updates user data, if there is a logged in user. + """ + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + response = await self._request( + "PUT", + "user", + body=attributes, + jwt=session.access_token, + xform=parse_user_response, + ) + session.user = response.user + await self._save_session(session) + self._notify_all_subscribers("USER_UPDATED", session) + return response + + async def set_session(self, access_token: str, refresh_token: str) -> AuthResponse: + """ + Sets the session data from the current session. If the current session + is expired, `set_session` will take care of refreshing it to obtain a + new session. + + If the refresh token in the current session is invalid and the current + session has expired, an error will be thrown. + + If the current session does not contain at `expires_at` field, + `set_session` will use the exp claim defined in the access token. + + The current session that minimally contains an access token, + refresh token and a user. + """ + time_now = round(time()) + expires_at = time_now + has_expired = True + session: Union[Session, None] = None + if access_token and access_token.split(".")[1]: + json_raw = b64decode(access_token.split(".")[1] + "===").decode("utf-8") + payload = loads(json_raw) + if payload.get("exp"): + expires_at = int(payload.get("exp")) + has_expired = expires_at <= time_now + if has_expired: + if not refresh_token: + raise AuthSessionMissingError() + response = await self._refresh_access_token(refresh_token) + if not response.session: + return AuthResponse() + session = response.session + else: + response = await self.get_user(access_token) + session = Session( + access_token=access_token, + refresh_token=refresh_token, + user=response.user, + token_type="bearer", + expires_in=expires_at - time_now, + expires_at=expires_at, + ) + await self._save_session(session) + self._notify_all_subscribers("TOKEN_REFRESHED", session) + return AuthResponse(session=session, user=response.user) + + async def sign_out(self) -> None: + """ + Inside a browser context, `sign_out` will remove the logged in user from the + browser session and log them out - removing all items from localstorage and + then trigger a `"SIGNED_OUT"` event. + + For server-side management, you can revoke all refresh tokens for a user by + passing a user's JWT through to `api.sign_out`. + + There is no way to revoke a user's access token jwt until it expires. + It is recommended to set a shorter expiry on the jwt for this reason. + """ + session = await self.get_session() + access_token = session.access_token if session else None + if access_token: + await self.admin.sign_out(access_token) + await self._remove_session() + self._notify_all_subscribers("SIGNED_OUT", None) + + async def on_auth_state_change( + self, + callback: Callable[[AuthChangeEvent, Union[Session, None]], None], + ) -> Subscription: + """ + Receive a notification every time an auth event happens. + """ + unique_id = str(uuid4()) + + def _unsubscribe() -> None: + self._state_change_emitters.pop(unique_id) + + subscription = Subscription( + id=unique_id, + callback=callback, + unsubscribe=_unsubscribe, + ) + self._state_change_emitters[unique_id] = subscription + return subscription + + async def reset_password_email( + self, + email: str, + options: Options = {}, + ) -> None: + """ + Sends a password reset request to an email address. + """ + raise NotImplementedError + + # Private methods + + async def _remove_session(self) -> None: + if self._persist_session: + await self._storage.remove_item(self._storage_key) + else: + self._in_memory_session = None + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = None + + async def _get_session_from_url( + self, + url: str, + ) -> Tuple[Session, Union[str, None]]: + if not self._is_implicit_grant_flow(url): + raise AuthImplicitGrantRedirectError("Not a valid implicit grant flow url.") + result = urlparse(url) + params = parse_qs(result.query) + error_description = self._get_param(params, "error_description") + if error_description: + error_code = self._get_param(params, "error_code") + error = self._get_param(params, "error") + if not error_code: + raise AuthImplicitGrantRedirectError("No error_code detected.") + if not error: + raise AuthImplicitGrantRedirectError("No error detected.") + raise AuthImplicitGrantRedirectError( + error_description, + {"code": error_code, "error": error}, + ) + provider_token = self._get_param(params, "provider_token") + provider_refresh_token = self._get_param(params, "provider_refresh_token") + access_token = self._get_param(params, "access_token") + if not access_token: + raise AuthImplicitGrantRedirectError("No access_token detected.") + expires_in = self._get_param(params, "expires_in") + if not expires_in: + raise AuthImplicitGrantRedirectError("No expires_in detected.") + refresh_token = self._get_param(params, "refresh_token") + if not refresh_token: + raise AuthImplicitGrantRedirectError("No refresh_token detected.") + token_type = self._get_param(params, "token_type") + if not token_type: + raise AuthImplicitGrantRedirectError("No token_type detected.") + time_now = round(time()) + expires_at = time_now + int(expires_in) + user = await self.get_user(access_token) + session = Session( + provider_token=provider_token, + provider_refresh_token=provider_refresh_token, + access_token=access_token, + expires_in=int(expires_in), + expires_at=expires_at, + refresh_token=refresh_token, + token_type=token_type, + user=user.user, + ) + redirect_type = self._get_param(params, "type") + return session, redirect_type + + async def _recover_and_refresh(self) -> None: + raw_session = await self._storage.get_item(self._storage_key) + current_session = self._get_valid_session(raw_session) + if not current_session: + if raw_session: + await self._remove_session() + return + time_now = round(time()) + expires_at = current_session.expires_at + if expires_at and expires_at < time_now + EXPIRY_MARGIN: + refresh_token = current_session.refresh_token + if self._auto_refresh_token and refresh_token: + self._network_retries += 1 + try: + await self._call_refresh_token(refresh_token) + self._network_retries = 0 + except Exception as e: + if ( + isinstance(e, AuthRetryableError) + and self._network_retries < MAX_RETRIES + ): + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = Timer( + (RETRY_INTERVAL ** (self._network_retries * 100)), + self._recover_and_refresh, + ) + self._refresh_token_timer.start() + return + await self._remove_session() + return + if self._persist_session: + await self._save_session(current_session) + self._notify_all_subscribers("SIGNED_IN", current_session) + + async def _call_refresh_token(self, refresh_token: str) -> Session: + if not refresh_token: + raise AuthSessionMissingError() + response = await self._refresh_access_token(refresh_token) + if not response.session: + raise AuthSessionMissingError() + await self._save_session(response.session) + self._notify_all_subscribers("TOKEN_REFRESHED", response.session) + return response.session + + async def _refresh_access_token(self, refresh_token: str) -> AuthResponse: + return await self._request( + "POST", + "token", + query={"grant_type": "refresh_token"}, + body={"refresh_token": refresh_token}, + xform=parse_auth_response, + ) + + async def _save_session(self, session: Session) -> None: + if not self._persist_session: + self._in_memory_session = session + expire_at = session.expires_at + if expire_at: + pass + time_now = round(time()) + expire_in = expire_at - time_now + refresh_duration_before_expires = ( + EXPIRY_MARGIN if expire_in > EXPIRY_MARGIN else 0.5 + ) + value = (expire_in - refresh_duration_before_expires) * 1000 + await self._start_auto_refresh_token(value) + if self._persist_session and session.expires_at: + await self._storage.set_item(self._storage_key, session.json()) + + async def _start_auto_refresh_token(self, value: float) -> None: + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = None + if value <= 0 or not self._auto_refresh_token: + return + + async def refresh_token_function(): + self._network_retries += 1 + try: + session = await self.get_session() + if session: + await self._call_refresh_token(session.refresh_token) + self._network_retries = 0 + except Exception as e: + if ( + isinstance(e, AuthRetryableError) + and self._network_retries < MAX_RETRIES + ): + await self._start_auto_refresh_token( + RETRY_INTERVAL ** (self._network_retries * 100) + ) + + self._refresh_token_timer = Timer(value, refresh_token_function) + self._refresh_token_timer.start() + + def _notify_all_subscribers( + self, + event: AuthChangeEvent, + session: Union[Session, None], + ) -> None: + for subscription in self._state_change_emitters.values(): + subscription.callback(event, session) + + def _get_valid_session( + self, + raw_session: Union[str, None], + ) -> Union[Session, None]: + if not raw_session: + return None + data = loads(raw_session) + if not data: + return None + if not data.get("access_token"): + return None + if not data.get("refresh_token"): + return None + if not data.get("expires_at"): + return None + try: + expires_at = int(data["expires_at"]) + data["expires_at"] = expires_at + except ValueError: + return None + try: + session = Session.parse_obj(data) + return session + except Exception: + return None + + def _get_param( + self, + query_params: Dict[str, List[str]], + name: str, + ) -> Union[str, None]: + if name in query_params: + return query_params[name][0] + return None + + def _is_implicit_grant_flow(self, url: str) -> bool: + result = urlparse(url) + params = parse_qs(result.query) + return "access_token" in params or "error_description" in params + + def _get_url_for_provider( + self, + provider: Provider, + params: Dict[str, str], + ) -> str: + params = {k: quote(v) for k, v in params.items()} + params["provider"] = quote(provider) + query = urlencode(params) + return f"{self._url}/authorize?{query}" + + +async def test(): + client = AsyncGoTrueClient() + await client.initialize() diff --git a/gotrue/_sync/api.py b/gotrue/_sync/api.py deleted file mode 100644 index bec3f70b..00000000 --- a/gotrue/_sync/api.py +++ /dev/null @@ -1,642 +0,0 @@ -from __future__ import annotations - -from typing import Any, Dict, List, Optional, Union - -from pydantic import parse_obj_as - -from ..exceptions import APIError -from ..helpers import check_response, encode_uri_component -from ..http_clients import SyncClient -from ..types import ( - CookieOptions, - LinkType, - Provider, - Session, - User, - UserAttributes, - determine_session_or_user_model_from_response, -) - - -class SyncGoTrueAPI: - def __init__( - self, - *, - url: str, - headers: Dict[str, str], - cookie_options: CookieOptions, - http_client: Optional[SyncClient] = None, - ) -> None: - """Initialise API class.""" - self.url = url - self.headers = headers - self.cookie_options = cookie_options - self.http_client = http_client or SyncClient() - - def __enter__(self) -> SyncGoTrueAPI: - return self - - def __exit__(self, exc_t, exc_v, exc_tb) -> None: - self.close() - - def close(self) -> None: - self.http_client.aclose() - - def create_user(self, *, attributes: UserAttributes) -> User: - """Creates a new user. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Parameters - ---------- - attributes: UserAttributes - The data you want to create the user with. - - Returns - ------- - response : User - The created user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = attributes.dict() - url = f"{self.url}/admin/users" - response = self.http_client.post(url, json=data, headers=headers) - return User.parse_response(response) - - def list_users(self) -> List[User]: - """Get a list of users. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Returns - ------- - response : List[User] - A list of users - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - url = f"{self.url}/admin/users" - response = self.http_client.get(url, headers=headers) - check_response(response) - users = response.json().get("users") - if users is None: - raise APIError("No users found in response", 400) - if not isinstance(users, list): - raise APIError("Expected a list of users", 400) - return parse_obj_as(List[User], users) - - def sign_up_with_email( - self, - *, - email: str, - password: str, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Creates a new user using their email address. - - Parameters - ---------- - email : str - The email address of the user. - password : str - The password of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "password": password, "data": data} - url = f"{self.url}/signup{query_string}" - response = self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - def sign_in_with_email( - self, - *, - email: str, - password: str, - redirect_to: Optional[str] = None, - ) -> Session: - """Logs in an existing user using their email address. - - Parameters - ---------- - email : str - The email address of the user. - password : str - The password of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "?grant_type=password" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string += f"&redirect_to={redirect_to_encoded}" - data = {"email": email, "password": password} - url = f"{self.url}/token{query_string}" - response = self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - def sign_up_with_phone( - self, - *, - phone: str, - password: str, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Signs up a new user using their phone number and a password. - - Parameters - ---------- - phone : str - The phone number of the user. - password : str - The password of the user. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = {"phone": phone, "password": password, "data": data} - url = f"{self.url}/signup" - response = self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - def sign_in_with_phone( - self, - *, - phone: str, - password: str, - ) -> Session: - """Logs in an existing user using their phone number and password. - - Parameters - ---------- - phone : str - The phone number of the user. - password : str - The password of the user. - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - data = {"phone": phone, "password": password} - url = f"{self.url}/token?grant_type=password" - headers = self.headers - response = self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - def send_magic_link_email( - self, - *, - email: str, - create_user: bool, - redirect_to: Optional[str] = None, - ) -> None: - """Sends a magic login link to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "create_user": create_user} - url = f"{self.url}/magiclink{query_string}" - response = self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: - """Sends a mobile OTP via SMS. - Will register the account if it doesn't already exist - - Parameters - ---------- - phone : str - The user's phone number WITH international prefix - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = {"phone": phone, "create_user": create_user} - url = f"{self.url}/otp" - response = self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - def verify_mobile_otp( - self, - *, - phone: str, - token: str, - redirect_to: Optional[str] = None, - ) -> Union[Session, User]: - """Send User supplied Mobile OTP to be verified - - Parameters - ---------- - phone : str - The user's phone number WITH international prefix - token : str - Token that user was sent to their mobile phone - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = { - "phone": phone, - "token": token, - "type": "sms", - } - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - data["redirect_to"] = redirect_to_encoded - url = f"{self.url}/verify" - response = self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - def invite_user_by_email( - self, - *, - email: str, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> User: - """Sends an invite link to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email, "data": data} - url = f"{self.url}/invite{query_string}" - response = self.http_client.post(url, json=data, headers=headers) - return User.parse_response(response) - - def reset_password_for_email( - self, - *, - email: str, - redirect_to: Optional[str] = None, - ) -> None: - """Sends a reset request to an email address. - - Parameters - ---------- - email : str - The email address of the user. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - query_string = "" - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - query_string = f"?redirect_to={redirect_to_encoded}" - data = {"email": email} - url = f"{self.url}/recover{query_string}" - response = self.http_client.post(url, json=data, headers=headers) - return check_response(response) - - def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: - """Create temporary object. - - Create a temporary object with all configured headers and adds the - Authorization token to be used on request methods. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - - Returns - ------- - headers : dict of str - The headers required for a successful request statement with the - supabase backend. - """ - headers = {**self.headers, "Authorization": f"Bearer {jwt}"} - return headers - - def sign_out(self, *, jwt: str) -> None: - """Removes a logged-in session. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/logout" - self.http_client.post(url, headers=headers) - - def get_url_for_provider( - self, - *, - provider: Provider, - redirect_to: Optional[str] = None, - scopes: Optional[str] = None, - ) -> str: - """Generates the relevant login URL for a third-party provider. - - Parameters - ---------- - provider : Provider - One of the providers supported by GoTrue. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - scopes : Optional[str] - A space-separated list of scopes granted to the OAuth application. - - Returns - ------- - url : str - The URL to redirect the user to. - - Raises - ------ - error : APIError - If an error occurs - """ - url_params = [f"provider={encode_uri_component(provider)}"] - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - url_params.append(f"redirect_to={redirect_to_encoded}") - if scopes: - url_params.append(f"scopes={encode_uri_component(scopes)}") - return f"{self.url}/authorize?{'&'.join(url_params)}" - - def get_user(self, *, jwt: str) -> User: - """Gets the user details. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/user" - response = self.http_client.get(url, headers=headers) - return User.parse_response(response) - - def update_user( - self, - *, - jwt: str, - attributes: UserAttributes, - ) -> User: - """ - Updates the user data. - - Parameters - ---------- - jwt : str - A valid, logged-in JWT. - attributes : UserAttributes - The data you want to update. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - data = attributes.dict() - url = f"{self.url}/user" - response = self.http_client.put(url, json=data, headers=headers) - return User.parse_response(response) - - def delete_user(self, *, uid: str, jwt: str) -> None: - """Delete a user. Requires a `service_role` key. - - This function should only be called on a server. - Never expose your `service_role` key in the browser. - - Parameters - ---------- - uid : str - The user uid you want to remove. - jwt : str - A valid, logged-in JWT. - - Returns - ------- - response : User - A user - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self._create_request_headers(jwt=jwt) - url = f"{self.url}/admin/users/{uid}" - response = self.http_client.delete(url, headers=headers) - return check_response(response) - - def refresh_access_token(self, *, refresh_token: str) -> Session: - """Generates a new JWT. - - Parameters - ---------- - refresh_token : str - A valid refresh token that was returned on login. - - Returns - ------- - response : Session - A session - - Raises - ------ - error : APIError - If an error occurs - """ - data = {"refresh_token": refresh_token} - url = f"{self.url}/token?grant_type=refresh_token" - headers = self.headers - response = self.http_client.post(url, json=data, headers=headers) - return Session.parse_response(response) - - def generate_link( - self, - *, - type: LinkType, - email: str, - password: Optional[str] = None, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """ - Generates links to be sent via email or other. - - Parameters - ---------- - type : LinkType - The link type ("signup" or "magiclink" or "recovery" or "invite"). - email : str - The user's email. - password : Optional[str] - User password. For signup only. - redirect_to : Optional[str] - The link type ("signup" or "magiclink" or "recovery" or "invite"). - data : Optional[Dict[str, Any]] - Optional user metadata. For signup only. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - headers = self.headers - data = { - "type": type, - "email": email, - "data": data, - } - if password: - data["password"] = password - if redirect_to: - redirect_to_encoded = encode_uri_component(redirect_to) - data["redirect_to"] = redirect_to_encoded - url = f"{self.url}/admin/generate_link" - response = self.http_client.post(url, json=data, headers=headers) - SessionOrUserModel = determine_session_or_user_model_from_response(response) - return SessionOrUserModel.parse_response(response) - - def set_auth_cookie(self, *, req, res): - """Stub for parity with JS api.""" - raise NotImplementedError("set_auth_cookie not implemented.") - - def get_user_by_cookie(self, *, req): - """Stub for parity with JS api.""" - raise NotImplementedError("get_user_by_cookie not implemented.") diff --git a/gotrue/_sync/client.py b/gotrue/_sync/client.py deleted file mode 100644 index f7f20455..00000000 --- a/gotrue/_sync/client.py +++ /dev/null @@ -1,640 +0,0 @@ -from __future__ import annotations - -from functools import partial -from json import dumps, loads -from threading import Timer -from time import time -from typing import Any, Callable, Dict, Optional, Tuple, Union, cast -from urllib.parse import parse_qs, urlparse -from uuid import uuid4 - -from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY -from ..exceptions import APIError -from ..types import ( - AuthChangeEvent, - CookieOptions, - Provider, - Session, - Subscription, - User, - UserAttributes, - UserAttributesDict, -) -from .api import SyncGoTrueAPI -from .storage import SyncMemoryStorage, SyncSupportedStorage - - -class SyncGoTrueClient: - def __init__( - self, - *, - url: str = GOTRUE_URL, - headers: Dict[str, str] = {}, - auto_refresh_token: bool = True, - persist_session: bool = True, - local_storage: SyncSupportedStorage = SyncMemoryStorage(), - cookie_options: CookieOptions = CookieOptions.parse_obj(COOKIE_OPTIONS), - api: Optional[SyncGoTrueAPI] = None, - replace_default_headers: bool = False, - ) -> None: - """Create a new client - - url : str - The URL of the GoTrue server. - headers : Dict[str, str] - Any additional headers to send to the GoTrue server. - auto_refresh_token : bool - Set to "true" if you want to automatically refresh the token before - expiring. - persist_session : bool - Set to "true" if you want to automatically save the user session - into local storage. - local_storage : SupportedStorage - The storage engine to use for persisting the session. - cookie_options : CookieOptions - The options for the cookie. - """ - if url.startswith("http://"): - print( - "Warning:\n\nDO NOT USE HTTP IN PRODUCTION FOR GOTRUE EVER!\n" - "GoTrue REQUIRES HTTPS to work securely." - ) - self.state_change_emitters: Dict[str, Subscription] = {} - self.refresh_token_timer: Optional[Timer] = None - self.current_user: Optional[User] = None - self.current_session: Optional[Session] = None - self.auto_refresh_token = auto_refresh_token - self.persist_session = persist_session - self.local_storage = local_storage - empty_or_default_headers = {} if replace_default_headers else DEFAULT_HEADERS - args = { - "url": url, - "headers": {**empty_or_default_headers, **headers}, - "cookie_options": cookie_options, - } - self.api = api or SyncGoTrueAPI(**args) - - def __enter__(self) -> SyncGoTrueClient: - return self - - def __exit__(self, exc_t, exc_v, exc_tb) -> None: - self.close() - - def close(self) -> None: - self.api.close() - - def init_recover(self) -> None: - """Recover the current session from local storage.""" - self._recover_session() - self._recover_and_refresh() - - def sign_up( - self, - *, - email: Optional[str] = None, - phone: Optional[str] = None, - password: Optional[str] = None, - redirect_to: Optional[str] = None, - data: Optional[Dict[str, Any]] = None, - ) -> Union[Session, User]: - """Creates a new user. If email and phone are provided, email will be - used and phone will be ignored. - - Parameters - --------- - email : Optional[str] - The user's email address. - phone : Optional[str] - The user's phone number. - password : Optional[str] - The user's password. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - data : Optional[Dict[str, Any]] - Optional user metadata. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - self._remove_session() - - if email and password: - response = self.api.sign_up_with_email( - email=email, - password=password, - redirect_to=redirect_to, - data=data, - ) - elif phone and password: - response = self.api.sign_up_with_phone( - phone=phone, password=password, data=data - ) - elif not password: - raise ValueError("Password must be defined, can't be None.") - else: - raise ValueError("Email or phone must be defined, both can't be None.") - - if isinstance(response, Session): - # The user has confirmed their email or the underlying DB doesn't - # require email confirmation. - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def sign_in( - self, - *, - email: Optional[str] = None, - phone: Optional[str] = None, - password: Optional[str] = None, - refresh_token: Optional[str] = None, - provider: Optional[Provider] = None, - redirect_to: Optional[str] = None, - scopes: Optional[str] = None, - create_user: bool = False, - ) -> Optional[Union[Session, str]]: - """Log in an existing user, or login via a third-party provider. - If email and phone are provided, email will be used and phone will be ignored. - - Parameters - --------- - email : Optional[str] - The user's email address. - phone : Optional[str] - The user's phone number. - password : Optional[str] - The user's password. - refresh_token : Optional[str] - A valid refresh token that was returned on login. - provider : Optional[Provider] - One of the providers supported by GoTrue. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - scopes : Optional[str] - A space-separated list of scopes granted to the OAuth application. - - Returns - ------- - response : Optional[Union[Session, str]] - If only email are provided between the email and password, - None is returned and send magic link to email - - If email and password are provided, a logged-in session is returned. - - If only phone are provided between the phone and password, - None is returned and send message to phone - - If phone and password are provided, a logged-in session is returned. - - If refresh_token is provided, a logged-in session is returned. - - If provider is provided, an redirect URL is returned. - - Otherwise, error is raised. - - Raises - ------ - error : APIError - If an error occurs - """ - self._remove_session() - if email: - if password: - response = self._handle_email_sign_in( - email=email, - password=password, - redirect_to=redirect_to, - ) - else: - response = self.api.send_magic_link_email( - email=email, create_user=create_user - ) - elif phone: - if password: - response = self._handle_phone_sign_in(phone=phone, password=password) - else: - response = self.api.send_mobile_otp( - phone=phone, create_user=create_user - ) - elif refresh_token: - # current_session and current_user will be updated to latest - # on _call_refresh_token using the passed refresh_token - self._call_refresh_token(refresh_token=refresh_token) - response = self.current_session - elif provider: - response = self._handle_provider_sign_in( - provider=provider, - redirect_to=redirect_to, - scopes=scopes, - ) - else: - raise ValueError( - "Email, phone, refresh_token, or provider must be defined, " - "all can't be None." - ) - return response - - def verify_otp( - self, - *, - phone: str, - token: str, - redirect_to: Optional[str] = None, - ) -> Union[Session, User]: - """Log in a user given a User supplied OTP received via mobile. - - Parameters - ---------- - phone : str - The user's phone number. - token : str - The user's OTP. - redirect_to : Optional[str] - A URL or mobile address to send the user to after they are confirmed. - - Returns - ------- - response : Union[Session, User] - A logged-in session if the server has "autoconfirm" ON - A user if the server has "autoconfirm" OFF - - Raises - ------ - error : APIError - If an error occurs - """ - self._remove_session() - response = self.api.verify_mobile_otp( - phone=phone, - token=token, - redirect_to=redirect_to, - ) - if isinstance(response, Session): - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def user(self) -> Optional[User]: - """Returns the user data, if there is a logged in user.""" - return self.current_user - - def session(self) -> Optional[Session]: - """Returns the session data, if there is an active session.""" - return self.current_session - - def refresh_session(self) -> Session: - """Force refreshes the session. - - Force refreshes the session including the user data incase it was - updated in a different session. - """ - if not self.current_session: - raise ValueError("Not logged in.") - return self._call_refresh_token() - - def update(self, *, attributes: Union[UserAttributesDict, UserAttributes]) -> User: - """Updates user data, if there is a logged in user. - - Parameters - ---------- - attributes : UserAttributesDict | UserAttributes - Attributes to update, could be: email, password, email_change_token, data - - Returns - ------- - response : User - The updated user data. - - Raises - ------ - error : APIError - If an error occurs - """ - if not self.current_session: - raise ValueError("Not logged in.") - - if isinstance(attributes, dict): - attributes_to_update = UserAttributes(**attributes) - else: - attributes_to_update = attributes - - response = self.api.update_user( - jwt=self.current_session.access_token, - attributes=attributes_to_update, - ) - self.current_session.user = response - self._save_session(session=self.current_session) - self._notify_all_subscribers(event=AuthChangeEvent.USER_UPDATED) - return response - - def set_session(self, *, refresh_token: str) -> Session: - """Sets the session data from refresh_token and returns current Session - - Parameters - ---------- - refresh_token : str - A JWT token - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - response = self.api.refresh_access_token(refresh_token=refresh_token) - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def set_auth(self, *, access_token: str) -> Session: - """Overrides the JWT on the current client. The JWT will then be sent in - all subsequent network requests. - - Parameters - ---------- - access_token : str - A JWT token - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - session = Session( - access_token=access_token, - token_type="bearer", - user=None, - expires_in=None, - expires_at=None, - refresh_token=None, - provider_token=None, - ) - if self.current_session: - session.expires_in = self.current_session.expires_in - session.expires_at = self.current_session.expires_at - session.refresh_token = self.current_session.refresh_token - session.provider_token = self.current_session.provider_token - self._save_session(session=session) - return session - - def get_session_from_url( - self, - *, - url: str, - store_session: bool = False, - ) -> Session: - """Gets the session data from a URL string. - - Parameters - ---------- - url : str - The URL string. - store_session : bool - Optionally store the session in the browser - - Returns - ------- - response : Session - A logged-in session - - Raises - ------ - error : APIError - If an error occurs - """ - data = urlparse(url) - query = parse_qs(data.query) - error_description = query.get("error_description") - access_token = query.get("access_token") - expires_in = query.get("expires_in") - refresh_token = query.get("refresh_token") - token_type = query.get("token_type") - if error_description: - raise APIError(error_description[0], 400) - if not access_token or not access_token[0]: - raise APIError("No access_token detected.", 400) - if not refresh_token or not refresh_token[0]: - raise APIError("No refresh_token detected.", 400) - if not token_type or not token_type[0]: - raise APIError("No token_type detected.", 400) - if not expires_in or not expires_in[0]: - raise APIError("No expires_in detected.", 400) - try: - expires_at = round(time()) + int(expires_in[0]) - except ValueError: - raise APIError("Invalid expires_in.", 400) - response = self.api.get_user(jwt=access_token[0]) - provider_token = query.get("provider_token") - session = Session( - access_token=access_token[0], - token_type=token_type[0], - user=response, - expires_in=int(expires_in[0]), - expires_at=expires_at, - refresh_token=refresh_token[0], - provider_token=provider_token[0] if provider_token else None, - ) - if store_session: - self._save_session(session=session) - recovery_mode = query.get("type") - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - if recovery_mode and recovery_mode[0] == "recovery": - self._notify_all_subscribers(event=AuthChangeEvent.PASSWORD_RECOVERY) - return session - - def sign_out(self) -> None: - """Log the user out.""" - access_token: Optional[str] = None - if self.current_session: - access_token = self.current_session.access_token - self._remove_session() - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_OUT) - if access_token: - self.api.sign_out(jwt=access_token) - - def _unsubscribe(self, *, id: str) -> None: - """Unsubscribe from a subscription.""" - self.state_change_emitters.pop(id) - - def on_auth_state_change( - self, - *, - callback: Callable[[AuthChangeEvent, Optional[Session]], None], - ) -> Subscription: - """Receive a notification every time an auth event happens. - - Parameters - ---------- - callback : Callable[[AuthChangeEvent, Optional[Session]], None] - The callback to call when an auth event happens. - - Returns - ------- - subscription : Subscription - A subscription object which can be used to unsubscribe itself. - - Raises - ------ - error : APIError - If an error occurs - """ - unique_id = uuid4() - subscription = Subscription( - id=unique_id, - callback=callback, - unsubscribe=partial(self._unsubscribe, id=unique_id.hex), - ) - self.state_change_emitters[unique_id.hex] = subscription - return subscription - - def _handle_email_sign_in( - self, - *, - email: str, - password: str, - redirect_to: Optional[str], - ) -> Session: - """Sign in with email and password.""" - response = self.api.sign_in_with_email( - email=email, - password=password, - redirect_to=redirect_to, - ) - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def _handle_phone_sign_in(self, *, phone: str, password: str) -> Session: - """Sign in with phone and password.""" - response = self.api.sign_in_with_phone(phone=phone, password=password) - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def _handle_provider_sign_in( - self, - *, - provider: Provider, - redirect_to: Optional[str], - scopes: Optional[str], - ) -> str: - """Sign in with provider.""" - return self.api.get_url_for_provider( - provider=provider, - redirect_to=redirect_to, - scopes=scopes, - ) - - def _recover_common(self) -> Optional[Tuple[Session, int, int]]: - """Recover common logic""" - json = self.local_storage.get_item(STORAGE_KEY) - if not json: - return - data = loads(json) - session_raw = data.get("session") - expires_at_raw = data.get("expires_at") - if ( - expires_at_raw - and isinstance(expires_at_raw, int) - and session_raw - and isinstance(session_raw, dict) - ): - session = Session.parse_obj(session_raw) - expires_at = int(expires_at_raw) - time_now = round(time()) - return session, expires_at, time_now - - def _recover_session(self) -> None: - """Attempts to get the session from LocalStorage""" - result = self._recover_common() - if not result: - return - session, expires_at, time_now = result - if expires_at >= time_now: - self._save_session(session=session) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - - def _recover_and_refresh(self) -> None: - """Recovers the session from LocalStorage and refreshes""" - result = self._recover_common() - if not result: - return - session, expires_at, time_now = result - if expires_at < time_now and self.auto_refresh_token and session.refresh_token: - try: - self._call_refresh_token(refresh_token=session.refresh_token) - except APIError: - self._remove_session() - elif expires_at < time_now or not session or not session.user: - self._remove_session() - else: - self._save_session(session=session) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - - def _call_refresh_token(self, *, refresh_token: Optional[str] = None) -> Session: - if refresh_token is None: - if self.current_session: - refresh_token = self.current_session.refresh_token - else: - raise ValueError("No current session and refresh_token not supplied.") - response = self.api.refresh_access_token(refresh_token=cast(str, refresh_token)) - self._save_session(session=response) - self._notify_all_subscribers(event=AuthChangeEvent.TOKEN_REFRESHED) - self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) - return response - - def _notify_all_subscribers(self, *, event: AuthChangeEvent) -> None: - """Notify all subscribers that auth event happened.""" - for value in self.state_change_emitters.values(): - value.callback(event, self.current_session) - - def _save_session(self, *, session: Session) -> None: - """Save session to client.""" - self.current_session = session - self.current_user = session.user - if session.expires_at: - time_now = round(time()) - expire_in = session.expires_at - time_now - refresh_duration_before_expires = 60 if expire_in > 60 else 0.5 - self._start_auto_refresh_token( - value=(expire_in - refresh_duration_before_expires) * 1000 - ) - if self.persist_session and session.expires_at: - self._persist_session(session=session) - - def _persist_session(self, *, session: Session) -> None: - data = {"session": session.dict(), "expires_at": session.expires_at} - self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str)) - - def _remove_session(self) -> None: - """Remove the session.""" - self.current_session = None - self.current_user = None - if self.refresh_token_timer: - self.refresh_token_timer.cancel() - self.local_storage.remove_item(STORAGE_KEY) - - def _start_auto_refresh_token(self, *, value: float) -> None: - if self.refresh_token_timer: - self.refresh_token_timer.cancel() - if value <= 0 or not self.auto_refresh_token: - return - self.refresh_token_timer = Timer(value, self._call_refresh_token) - self.refresh_token_timer.start() diff --git a/gotrue/_sync/gotrue_admin_api.py b/gotrue/_sync/gotrue_admin_api.py new file mode 100644 index 00000000..00576d58 --- /dev/null +++ b/gotrue/_sync/gotrue_admin_api.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from typing import Dict, List, Union + +from ..helpers import parse_link_response, parse_user_response +from ..http_clients import SyncClient +from ..types import ( + AdminUserAttributes, + GenerateLinkParams, + GenerateLinkResponse, + Options, + User, + UserResponse, +) +from .gotrue_base_api import SyncGoTrueBaseAPI + + +class SyncGoTrueAdminAPI(SyncGoTrueBaseAPI): + def __init__( + self, + *, + url: str = "", + headers: Dict[str, str] = {}, + http_client: Union[SyncClient, None] = None, + ) -> None: + SyncGoTrueBaseAPI.__init__( + self, + url=url, + headers=headers, + http_client=http_client, + ) + + def sign_out(self, jwt: str) -> None: + """ + Removes a logged-in session. + """ + return self._request("POST", "logout", jwt=jwt) + + def invite_user_by_email( + self, + email: str, + options: Options = {}, + ) -> UserResponse: + """ + Sends an invite link to an email address. + """ + return self._request( + "POST", + "invite", + body={"email": email, "data": options.get("data")}, + redirect_to=options.get("redirect_to"), + xform=parse_user_response, + ) + + def generate_link(self, params: GenerateLinkParams) -> GenerateLinkResponse: + """ + Generates email links and OTPs to be sent via a custom email provider. + """ + return self._request( + "POST", + "admin/generate_link", + body={ + "type": params.get("type"), + "email": params.get("email"), + "password": params.get("password"), + "new_email": params.get("new_email"), + "data": params.get("options", {}).get("data"), + }, + redirect_to=params.get("options", {}).get("redirect_to"), + xform=parse_link_response, + ) + + # User Admin API + + def create_user(self, attributes: AdminUserAttributes) -> UserResponse: + """ + Creates a new user. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return self._request( + "POST", + "admin/users", + body=attributes, + xform=parse_user_response, + ) + + def list_users(self) -> List[User]: + """ + Get a list of users. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return self._request( + "GET", + "admin/users", + xform=lambda data: [User.parse_obj(user) for user in data], + ) + + def get_user_by_id(self, uid: str) -> UserResponse: + """ + Get user by id. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return self._request( + "GET", + f"admin/users/{uid}", + xform=parse_user_response, + ) + + def update_user_by_id( + self, + uid: str, + attributes: AdminUserAttributes, + ) -> UserResponse: + """ + Updates the user data. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return self._request( + "PUT", + f"admin/users/{uid}", + body=attributes, + xform=parse_user_response, + ) + + def delete_user(self, id: str) -> UserResponse: + """ + Delete a user. Requires a `service_role` key. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + """ + return self._request( + "DELETE", + f"admin/users/{id}", + xform=parse_user_response, + ) diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py new file mode 100644 index 00000000..a1868fce --- /dev/null +++ b/gotrue/_sync/gotrue_base_api.py @@ -0,0 +1,98 @@ +from __future__ import annotations +from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload + +from pydantic import BaseModel +from typing_extensions import Self + +from ..helpers import handle_exception +from ..http_clients import SyncClient + +T = TypeVar("T") + + +class SyncGoTrueBaseAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + http_client: Union[SyncClient, None], + ): + self._url = url + self._headers = headers + self._http_client = http_client or SyncClient() + + def __enter__(self) -> Self: + return self + + def __exit__(self, exc_t, exc_v, exc_tb) -> None: + self.close() + + def close(self) -> None: + self._http_client.aclose() + + @overload + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + xform: Callable[[Any], T], + ) -> T: + ... + + @overload + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + ) -> None: + ... + + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: Union[bool, None] = None, + xform: Union[Callable[[Any], T], None] = None, + ) -> Union[T, None]: + url = f"{self._url}/{path}" + headers = {**self._headers, **(headers or {})} + if jwt: + headers["Authorization"] = f"Bearer {jwt}" + query = query or {} + if redirect_to: + query["redirect_to"] = redirect_to + try: + response = self._http_client.request( + method, + url, + headers=headers, + params=query, + json=body.dict() if isinstance(body, BaseModel) else body, + ) + response.raise_for_status() + result = response if no_resolve_json else response.json() + if xform: + return xform(result) + except Exception as e: + raise handle_exception(e) diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py new file mode 100644 index 00000000..15571e19 --- /dev/null +++ b/gotrue/_sync/gotrue_client.py @@ -0,0 +1,683 @@ +from __future__ import annotations + +from base64 import b64decode +from json import loads +from time import time +from typing import Callable, Dict, List, Tuple, Union +from urllib.parse import parse_qs, quote, urlencode, urlparse +from uuid import uuid4 + +from ..constants import ( + DEFAULT_HEADERS, + EXPIRY_MARGIN, + GOTRUE_URL, + MAX_RETRIES, + RETRY_INTERVAL, + STORAGE_KEY, +) +from ..errors import ( + AuthImplicitGrantRedirectError, + AuthInvalidCredentialsError, + AuthRetryableError, + AuthSessionMissingError, +) +from ..helpers import parse_auth_response, parse_user_response +from ..http_clients import SyncClient +from ..timer import Timer +from ..types import ( + AuthChangeEvent, + AuthResponse, + OAuthResponse, + Options, + Provider, + Session, + SignInWithOAuthCredentials, + SignInWithPasswordCredentials, + SignInWithPasswordlessCredentials, + SignUpWithPasswordCredentials, + Subscription, + UserAttributes, + UserResponse, + VerifyOtpParams, +) +from .gotrue_admin_api import SyncGoTrueAdminAPI +from .gotrue_base_api import SyncGoTrueBaseAPI +from .storage import SyncMemoryStorage, SyncSupportedStorage + + +class SyncGoTrueClient(SyncGoTrueBaseAPI): + def __init__( + self, + *, + url: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + storage_key: Union[str, None] = None, + auto_refresh_token: bool = True, + persist_session: bool = True, + storage: Union[SyncSupportedStorage, None] = None, + http_client: Union[SyncClient, None] = None, + ) -> None: + SyncGoTrueBaseAPI.__init__( + self, + url=url or GOTRUE_URL, + headers=headers or DEFAULT_HEADERS, + http_client=http_client, + ) + self._storage_key = storage_key or STORAGE_KEY + self._auto_refresh_token = auto_refresh_token + self._persist_session = persist_session + self._storage = storage or SyncMemoryStorage() + self._in_memory_session: Union[Session, None] = None + self._refresh_token_timer: Union[Timer, None] = None + self._network_retries = 0 + self._state_change_emitters: Dict[str, Subscription] = {} + + self.admin = SyncGoTrueAdminAPI( + url=self._url, + headers=self._headers, + http_client=self._http_client, + ) + + # Initializations + + def initialize(self, *, url: Union[str, None] = None) -> None: + if url and self._is_implicit_grant_flow(url): + self.initialize_from_url(url) + else: + self.initialize_from_storage() + + def initialize_from_storage(self) -> None: + return self._recover_and_refresh() + + def initialize_from_url(self, url: str) -> None: + try: + if self._is_implicit_grant_flow(url): + session, redirect_type = self._get_session_from_url(url) + self._save_session(session) + self._notify_all_subscribers("SIGNED_IN", session) + if redirect_type == "recovery": + self._notify_all_subscribers("PASSWORD_RECOVERY", session) + except Exception as e: + self._remove_session() + raise e + + # Public methods + + def sign_up( + self, + credentials: SignUpWithPasswordCredentials, + ) -> AuthResponse: + """ + Creates a new user. + """ + self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + password = credentials.get("password") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + data = options.get("data") + captcha_token = options.get("captcha_token") + if email: + response = self._request( + "POST", + "signup", + body={ + "email": email, + "password": password, + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + redirect_to=redirect_to, + xform=parse_auth_response, + ) + elif phone: + response = self._request( + "POST", + "signup", + body={ + "phone": phone, + "password": password, + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_response, + ) + else: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number and a password" + ) + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + def sign_in_with_password( + self, + credentials: SignInWithPasswordCredentials, + ) -> AuthResponse: + """ + Log in an existing user with an email or phone and password. + """ + self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + password = credentials.get("password") + options = credentials.get("options", {}) + captcha_token = options.get("captcha_token") + if email: + response = self._request( + "POST", + "token", + body={ + "email": email, + "password": password, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "password", + }, + xform=parse_auth_response, + ) + elif phone: + response = self._request( + "POST", + "token", + body={ + "phone": phone, + "password": password, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "password", + }, + xform=parse_auth_response, + ) + else: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number and a password" + ) + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + def sign_in_with_oauth( + self, + credentials: SignInWithOAuthCredentials, + ) -> OAuthResponse: + """ + Log in an existing user via a third-party provider. + """ + self._remove_session() + provider = credentials.get("provider") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + scopes = options.get("scopes") + params = options.get("query_params", {}) + if redirect_to: + params["redirect_to"] = redirect_to + if scopes: + params["scopes"] = scopes + url = self._get_url_for_provider(provider, params) + return OAuthResponse(provider=provider, url=url) + + def sign_in_with_otp( + self, + credentials: SignInWithPasswordlessCredentials, + ) -> AuthResponse: + """ + Log in a user using magiclink or a one-time password (OTP). + + If the `{{ .ConfirmationURL }}` variable is specified in + the email template, a magiclink will be sent. + + If the `{{ .Token }}` variable is specified in the email + template, an OTP will be sent. + + If you're using phone sign-ins, only an OTP will be sent. + You won't be able to send a magiclink for phone sign-ins. + """ + self._remove_session() + email = credentials.get("email") + phone = credentials.get("phone") + options = credentials.get("options", {}) + email_redirect_to = options.get("email_redirect_to") + should_create_user = options.get("create_user", True) + data = options.get("data") + captcha_token = options.get("captcha_token") + if email: + return self._request( + "POST", + "otp", + body={ + "email": email, + "data": data, + "create_user": should_create_user, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + redirect_to=email_redirect_to, + xform=parse_auth_response, + ) + if phone: + return self._request( + "POST", + "otp", + body={ + "phone": phone, + "data": data, + "create_user": should_create_user, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + def verify_otp(self, params: VerifyOtpParams) -> AuthResponse: + """ + Log in a user given a User supplied OTP received via mobile. + """ + self._remove_session() + response = self._request( + "POST", + "verify", + body={ + "gotrue_meta_security": { + "captcha_token": params.get("options", {}).get("captcha_token"), + }, + **params, + }, + redirect_to=params.get("options", {}).get("redirect_to"), + xform=parse_auth_response, + ) + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + def get_session(self) -> Union[Session, None]: + """ + Returns the session, refreshing it if necessary. + + The session returned can be null if the session is not detected which + can happen in the event a user is not signed-in or has logged out. + """ + current_session: Union[Session, None] = None + if self._persist_session: + maybe_session = self._storage.get_item(self._storage_key) + current_session = self._get_valid_session(maybe_session) + if not current_session: + self._remove_session() + else: + current_session = self._in_memory_session + if not current_session: + return None + has_expired = ( + current_session.expires_at <= time() + if current_session.expires_at + else False + ) + if not has_expired: + return current_session + return self._call_refresh_token(current_session.refresh_token) + + def get_user(self, jwt: Union[str, None] = None) -> UserResponse: + """ + Gets the current user details if there is an existing session. + + Takes in an optional access token `jwt`. If no `jwt` is provided, + `get_user()` will attempt to get the `jwt` from the current session. + """ + if not jwt: + session = self.get_session() + if session: + jwt = session.access_token + return self._request("GET", "user", jwt=jwt, xform=parse_user_response) + + def update_user(self, attributes: UserAttributes) -> UserResponse: + """ + Updates user data, if there is a logged in user. + """ + session = self.get_session() + if not session: + raise AuthSessionMissingError() + response = self._request( + "PUT", + "user", + body=attributes, + jwt=session.access_token, + xform=parse_user_response, + ) + session.user = response.user + self._save_session(session) + self._notify_all_subscribers("USER_UPDATED", session) + return response + + def set_session(self, access_token: str, refresh_token: str) -> AuthResponse: + """ + Sets the session data from the current session. If the current session + is expired, `set_session` will take care of refreshing it to obtain a + new session. + + If the refresh token in the current session is invalid and the current + session has expired, an error will be thrown. + + If the current session does not contain at `expires_at` field, + `set_session` will use the exp claim defined in the access token. + + The current session that minimally contains an access token, + refresh token and a user. + """ + time_now = round(time()) + expires_at = time_now + has_expired = True + session: Union[Session, None] = None + if access_token and access_token.split(".")[1]: + json_raw = b64decode(access_token.split(".")[1] + "===").decode("utf-8") + payload = loads(json_raw) + if payload.get("exp"): + expires_at = int(payload.get("exp")) + has_expired = expires_at <= time_now + if has_expired: + if not refresh_token: + raise AuthSessionMissingError() + response = self._refresh_access_token(refresh_token) + if not response.session: + return AuthResponse() + session = response.session + else: + response = self.get_user(access_token) + session = Session( + access_token=access_token, + refresh_token=refresh_token, + user=response.user, + token_type="bearer", + expires_in=expires_at - time_now, + expires_at=expires_at, + ) + self._save_session(session) + self._notify_all_subscribers("TOKEN_REFRESHED", session) + return AuthResponse(session=session, user=response.user) + + def sign_out(self) -> None: + """ + Inside a browser context, `sign_out` will remove the logged in user from the + browser session and log them out - removing all items from localstorage and + then trigger a `"SIGNED_OUT"` event. + + For server-side management, you can revoke all refresh tokens for a user by + passing a user's JWT through to `api.sign_out`. + + There is no way to revoke a user's access token jwt until it expires. + It is recommended to set a shorter expiry on the jwt for this reason. + """ + session = self.get_session() + access_token = session.access_token if session else None + if access_token: + self.admin.sign_out(access_token) + self._remove_session() + self._notify_all_subscribers("SIGNED_OUT", None) + + def on_auth_state_change( + self, + callback: Callable[[AuthChangeEvent, Union[Session, None]], None], + ) -> Subscription: + """ + Receive a notification every time an auth event happens. + """ + unique_id = str(uuid4()) + + def _unsubscribe() -> None: + self._state_change_emitters.pop(unique_id) + + subscription = Subscription( + id=unique_id, + callback=callback, + unsubscribe=_unsubscribe, + ) + self._state_change_emitters[unique_id] = subscription + return subscription + + def reset_password_email( + self, + email: str, + options: Options = {}, + ) -> None: + """ + Sends a password reset request to an email address. + """ + raise NotImplementedError + + # Private methods + + def _remove_session(self) -> None: + if self._persist_session: + self._storage.remove_item(self._storage_key) + else: + self._in_memory_session = None + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = None + + def _get_session_from_url( + self, + url: str, + ) -> Tuple[Session, Union[str, None]]: + if not self._is_implicit_grant_flow(url): + raise AuthImplicitGrantRedirectError("Not a valid implicit grant flow url.") + result = urlparse(url) + params = parse_qs(result.query) + error_description = self._get_param(params, "error_description") + if error_description: + error_code = self._get_param(params, "error_code") + error = self._get_param(params, "error") + if not error_code: + raise AuthImplicitGrantRedirectError("No error_code detected.") + if not error: + raise AuthImplicitGrantRedirectError("No error detected.") + raise AuthImplicitGrantRedirectError( + error_description, + {"code": error_code, "error": error}, + ) + provider_token = self._get_param(params, "provider_token") + provider_refresh_token = self._get_param(params, "provider_refresh_token") + access_token = self._get_param(params, "access_token") + if not access_token: + raise AuthImplicitGrantRedirectError("No access_token detected.") + expires_in = self._get_param(params, "expires_in") + if not expires_in: + raise AuthImplicitGrantRedirectError("No expires_in detected.") + refresh_token = self._get_param(params, "refresh_token") + if not refresh_token: + raise AuthImplicitGrantRedirectError("No refresh_token detected.") + token_type = self._get_param(params, "token_type") + if not token_type: + raise AuthImplicitGrantRedirectError("No token_type detected.") + time_now = round(time()) + expires_at = time_now + int(expires_in) + user = self.get_user(access_token) + session = Session( + provider_token=provider_token, + provider_refresh_token=provider_refresh_token, + access_token=access_token, + expires_in=int(expires_in), + expires_at=expires_at, + refresh_token=refresh_token, + token_type=token_type, + user=user.user, + ) + redirect_type = self._get_param(params, "type") + return session, redirect_type + + def _recover_and_refresh(self) -> None: + raw_session = self._storage.get_item(self._storage_key) + current_session = self._get_valid_session(raw_session) + if not current_session: + if raw_session: + self._remove_session() + return + time_now = round(time()) + expires_at = current_session.expires_at + if expires_at and expires_at < time_now + EXPIRY_MARGIN: + refresh_token = current_session.refresh_token + if self._auto_refresh_token and refresh_token: + self._network_retries += 1 + try: + self._call_refresh_token(refresh_token) + self._network_retries = 0 + except Exception as e: + if ( + isinstance(e, AuthRetryableError) + and self._network_retries < MAX_RETRIES + ): + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = Timer( + (RETRY_INTERVAL ** (self._network_retries * 100)), + self._recover_and_refresh, + ) + self._refresh_token_timer.start() + return + self._remove_session() + return + if self._persist_session: + self._save_session(current_session) + self._notify_all_subscribers("SIGNED_IN", current_session) + + def _call_refresh_token(self, refresh_token: str) -> Session: + if not refresh_token: + raise AuthSessionMissingError() + response = self._refresh_access_token(refresh_token) + if not response.session: + raise AuthSessionMissingError() + self._save_session(response.session) + self._notify_all_subscribers("TOKEN_REFRESHED", response.session) + return response.session + + def _refresh_access_token(self, refresh_token: str) -> AuthResponse: + return self._request( + "POST", + "token", + query={"grant_type": "refresh_token"}, + body={"refresh_token": refresh_token}, + xform=parse_auth_response, + ) + + def _save_session(self, session: Session) -> None: + if not self._persist_session: + self._in_memory_session = session + expire_at = session.expires_at + if expire_at: + pass + time_now = round(time()) + expire_in = expire_at - time_now + refresh_duration_before_expires = ( + EXPIRY_MARGIN if expire_in > EXPIRY_MARGIN else 0.5 + ) + value = (expire_in - refresh_duration_before_expires) * 1000 + self._start_auto_refresh_token(value) + if self._persist_session and session.expires_at: + self._storage.set_item(self._storage_key, session.json()) + + def _start_auto_refresh_token(self, value: float) -> None: + if self._refresh_token_timer: + self._refresh_token_timer.cancel() + self._refresh_token_timer = None + if value <= 0 or not self._auto_refresh_token: + return + + def refresh_token_function(): + self._network_retries += 1 + try: + session = self.get_session() + if session: + self._call_refresh_token(session.refresh_token) + self._network_retries = 0 + except Exception as e: + if ( + isinstance(e, AuthRetryableError) + and self._network_retries < MAX_RETRIES + ): + self._start_auto_refresh_token( + RETRY_INTERVAL ** (self._network_retries * 100) + ) + + self._refresh_token_timer = Timer(value, refresh_token_function) + self._refresh_token_timer.start() + + def _notify_all_subscribers( + self, + event: AuthChangeEvent, + session: Union[Session, None], + ) -> None: + for subscription in self._state_change_emitters.values(): + subscription.callback(event, session) + + def _get_valid_session( + self, + raw_session: Union[str, None], + ) -> Union[Session, None]: + if not raw_session: + return None + data = loads(raw_session) + if not data: + return None + if not data.get("access_token"): + return None + if not data.get("refresh_token"): + return None + if not data.get("expires_at"): + return None + try: + expires_at = int(data["expires_at"]) + data["expires_at"] = expires_at + except ValueError: + return None + try: + session = Session.parse_obj(data) + return session + except Exception: + return None + + def _get_param( + self, + query_params: Dict[str, List[str]], + name: str, + ) -> Union[str, None]: + if name in query_params: + return query_params[name][0] + return None + + def _is_implicit_grant_flow(self, url: str) -> bool: + result = urlparse(url) + params = parse_qs(result.query) + return "access_token" in params or "error_description" in params + + def _get_url_for_provider( + self, + provider: Provider, + params: Dict[str, str], + ) -> str: + params = {k: quote(v) for k, v in params.items()} + params["provider"] = quote(provider) + query = urlencode(params) + return f"{self._url}/authorize?{query}" + + +def test(): + client = SyncGoTrueClient() + client.initialize() diff --git a/gotrue/constants.py b/gotrue/constants.py index e87dd060..ad001f27 100644 --- a/gotrue/constants.py +++ b/gotrue/constants.py @@ -1,18 +1,12 @@ from __future__ import annotations -from gotrue import __version__ +from . import __version__ GOTRUE_URL = "http://localhost:9999" -AUDIENCE = "" DEFAULT_HEADERS = { "X-Client-Info": f"gotrue-py/{__version__}", } -EXPIRY_MARGIN = 60 * 1000 +EXPIRY_MARGIN = 10 # seconds +MAX_RETRIES = 10 +RETRY_INTERVAL = 2 # deciseconds STORAGE_KEY = "supabase.auth.token" -COOKIE_OPTIONS = { - "name": "sb:token", - "lifetime": 60 * 60 * 8, - "domain": "", - "path": "/", - "same_site": "lax", -} diff --git a/gotrue/errors.py b/gotrue/errors.py new file mode 100644 index 00000000..742d5d44 --- /dev/null +++ b/gotrue/errors.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import Union + +from typing_extensions import TypedDict + + +class AuthError(Exception): + def __init__(self, message: str) -> None: + Exception.__init__(self, message) + self.message = message + self.name = "AuthError" + + +class AuthApiErrorDict(TypedDict): + name: str + message: str + status: int + + +class AuthApiError(AuthError): + def __init__(self, message: str, status: int) -> None: + AuthError.__init__(self, message) + self.name = "AuthApiError" + self.status = status + + def to_dict(self) -> AuthApiErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + } + + +class AuthUnknownError(AuthError): + def __init__(self, message: str, original_error: Exception) -> None: + AuthError.__init__(self, message) + self.name = "AuthUnknownError" + self.original_error = original_error + + +class CustomAuthError(AuthError): + def __init__(self, message: str, name: str, status: int) -> None: + AuthError.__init__(self, message) + self.name = name + self.status = status + + def to_dict(self) -> AuthApiErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + } + + +class AuthSessionMissingError(CustomAuthError): + def __init__(self) -> None: + CustomAuthError.__init__( + self, + "Auth session missing!", + "AuthSessionMissingError", + 400, + ) + + +class AuthInvalidCredentialsError(CustomAuthError): + def __init__(self, message: str) -> None: + CustomAuthError.__init__( + self, + message, + "AuthInvalidCredentialsError", + 400, + ) + + +class AuthImplicitGrantRedirectErrorDetails(TypedDict): + error: str + code: str + + +class AuthImplicitGrantRedirectErrorDict(AuthApiErrorDict): + details: Union[AuthImplicitGrantRedirectErrorDetails, None] + + +class AuthImplicitGrantRedirectError(CustomAuthError): + def __init__( + self, + message: str, + details: Union[AuthImplicitGrantRedirectErrorDetails, None] = None, + ) -> None: + CustomAuthError.__init__( + self, + message, + "AuthImplicitGrantRedirectError", + 500, + ) + self.details = details + + def to_dict(self) -> AuthImplicitGrantRedirectErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + "details": self.details, + } + + +class AuthRetryableError(CustomAuthError): + def __init__(self, message: str, status: int) -> None: + CustomAuthError.__init__( + self, + message, + "AuthRetryableError", + status, + ) diff --git a/gotrue/exceptions.py b/gotrue/exceptions.py deleted file mode 100644 index 9979410f..00000000 --- a/gotrue/exceptions.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from dataclasses import asdict, dataclass -from typing import Any, Dict - - -@dataclass -class APIError(Exception): - msg: str - code: int - - def __post_init__(self) -> None: - self.msg = str(self.msg) - self.code = int(str(self.code)) - - @classmethod - def parse_dict(cls, **json: dict) -> APIError: - ret = cls(msg="Unknown error", code=-1) - for new_name, new_val in json.items(): - setattr(ret, new_name, new_val) - return ret - - @classmethod - def from_dict(cls, data: dict) -> APIError: - if "msg" in data and "code" in data: - return APIError( - msg=data["msg"], - code=data["code"], - ) - if "error" in data and "error_description" in data: - try: - code = int(data["error"]) - except ValueError: - code = -1 - return APIError( - msg=data["error_description"], - code=code, - ) - if "message" in data: - try: - code = int(data.get("code", -1)) - except ValueError: - code = -1 - return APIError( - msg=data["message"], - code=code, - ) - return cls.parse_dict(**data) - - def to_dict(self) -> Dict[str, Any]: - return asdict(self) diff --git a/gotrue/helpers.py b/gotrue/helpers.py index 069b79b7..53a5cf7b 100644 --- a/gotrue/helpers.py +++ b/gotrue/helpers.py @@ -1,18 +1,76 @@ from __future__ import annotations -from urllib.parse import quote +from typing import Any, Union, cast -from httpx import HTTPError, Response +from httpx import HTTPStatusError -from .exceptions import APIError +from .errors import AuthApiError, AuthError, AuthRetryableError, AuthUnknownError +from .types import ( + AuthResponse, + GenerateLinkProperties, + GenerateLinkResponse, + Session, + User, + UserResponse, +) -def encode_uri_component(uri: str) -> str: - return quote(uri.encode("utf-8")) +def parse_auth_response(data: Any) -> AuthResponse: + session: Union[Session, None] = None + if ( + "access_token" in data + and "refresh_token" in data + and "expires_in" in data + and data["access_token"] + and data["refresh_token"] + and data["expires_in"] + ): + session = Session.parse_obj(data) + user = User.parse_obj(data["user"]) if "user" in data else User.parse_obj(data) + return AuthResponse(session=session, user=user) -def check_response(response: Response) -> None: +def parse_link_response(data: Any) -> GenerateLinkResponse: + properties = GenerateLinkProperties( + action_link=data.get("action_link"), + email_otp=data.get("email_otp"), + hashed_token=data.get("hashed_token"), + redirect_to=data.get("redirect_to"), + verification_type=data.get("verification_type"), + ) + user = User.parse_obj({k: v for k, v in data.items() if k not in properties.dict()}) + return GenerateLinkResponse(properties=properties, user=user) + + +def parse_user_response(data: Any) -> UserResponse: + if "user" not in data: + data = {"user": data} + return UserResponse.parse_obj(data) + + +def get_error_message(error: Any) -> str: + props = ["msg", "message", "error_description", "error"] + filter = ( + lambda prop: prop in error if isinstance(error, dict) else hasattr(error, prop) + ) + return next((error[prop] for prop in props if filter(prop)), str(error)) + + +def looks_like_http_status_error(exception: Exception) -> bool: + return isinstance(exception, HTTPStatusError) + + +def handle_exception(exception: Exception) -> AuthError: + if not looks_like_http_status_error(exception): + return AuthRetryableError(get_error_message(exception), 0) + error = cast(HTTPStatusError, exception) try: - response.raise_for_status() - except HTTPError: - raise APIError.from_dict(response.json()) + network_error_codes = [502, 503, 504] + if error.response.status_code in network_error_codes: + return AuthRetryableError( + get_error_message(error), error.response.status_code + ) + json = error.response.json() + return AuthApiError(get_error_message(json), error.response.status_code or 500) + except Exception as e: + return AuthUnknownError(get_error_message(error), e) diff --git a/gotrue/timer.py b/gotrue/timer.py new file mode 100644 index 00000000..1b6bf9b8 --- /dev/null +++ b/gotrue/timer.py @@ -0,0 +1,44 @@ +import asyncio +from threading import Timer as _Timer +from typing import Any, Callable, Coroutine, Union, cast + + +class Timer: + def __init__( + self, + seconds: float, + function: Callable[[], Union[Coroutine[Any, Any, None], None]], + ) -> None: + self._milliseconds = seconds + self._function = function + self._task: Union[asyncio.Task, None] = None + self._timer: Union[_Timer, None] = None + + def start(self) -> None: + if asyncio.iscoroutinefunction(self._function): + + async def schedule(): + await asyncio.sleep(self._milliseconds / 1000) + await cast(Coroutine[Any, Any, None], self._function()) + + def cleanup(_): + self._task = None + + self._task = asyncio.create_task(schedule()) + self._task.add_done_callback(cleanup) + else: + self._timer = _Timer(self._milliseconds / 1000, self._function) + self._timer.start() + + def cancel(self) -> None: + if self._task is not None: + self._task.cancel() + self._task = None + if self._timer is not None: + self._timer.cancel() + self._timer = None + + def is_alive(self) -> bool: + return self._task is not None or ( + self._timer is not None and self._timer.is_alive() + ) diff --git a/gotrue/types.py b/gotrue/types.py index fdfd294d..59d785bd 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -1,167 +1,371 @@ from __future__ import annotations -import sys from datetime import datetime -from enum import Enum from time import time -from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union -from uuid import UUID +from typing import Any, Callable, Dict, List, Literal, Union -if sys.version_info >= (3, 8): - from typing import TypedDict -else: - from typing_extensions import TypedDict - -from httpx import Response from pydantic import BaseModel, root_validator +from typing_extensions import NotRequired, TypedDict + +Provider = Literal[ + "apple", + "azure", + "bitbucket", + "discord", + "facebook", + "github", + "gitlab", + "google", + "keycloak", + "linkedin", + "notion", + "slack", + "spotify", + "twitch", + "twitter", + "workos", +] + +AuthChangeEvent = Literal[ + "PASSWORD_RECOVERY", + "SIGNED_IN", + "SIGNED_OUT", + "TOKEN_REFRESHED", + "USER_UPDATED", + "USER_DELETED", +] + + +class Options(TypedDict, total=False): + redirect_to: str + data: Any + + +class AuthResponse(BaseModel): + user: Union[User, None] = None + session: Union[Session, None] = None + + +class OAuthResponse(BaseModel): + provider: Provider + url: str + + +class UserResponse(BaseModel): + user: User + + +class Session(BaseModel): + provider_token: Union[str, None] = None + """ + The oauth provider token. If present, this can be used to make external API + requests to the oauth provider used. + """ + provider_refresh_token: Union[str, None] = None + """ + The oauth provider refresh token. If present, this can be used to refresh + the provider_token via the oauth provider's API. + + Not all oauth providers return a provider refresh token. If the + provider_refresh_token is missing, please refer to the oauth provider's + documentation for information on how to obtain the provider refresh token. + """ + access_token: str + refresh_token: str + expires_in: int + """ + The number of seconds until the token expires (since it was issued). + Returned when a login is confirmed. + """ + expires_at: Union[int, None] = None + """ + A timestamp of when the token will expire. Returned when a login is confirmed. + """ + token_type: str + user: User -from gotrue.helpers import check_response + @root_validator + def validator(cls, values: dict) -> dict: + expires_in = values.get("expires_in") + if expires_in and not values.get("expires_at"): + values["expires_at"] = round(time()) + expires_in + return values -T = TypeVar("T", bound=BaseModel) +class UserIdentity(BaseModel): + id: str + user_id: str + identity_data: Dict[str, Any] + provider: str + created_at: datetime + last_sign_in_at: datetime + updated_at: Union[datetime, None] = None -def determine_session_or_user_model_from_response( - response: Response, -) -> Union[Type[Session], Type[User]]: - return Session if "access_token" in response.json() else User +class User(BaseModel): + id: str + app_metadata: Dict[str, Any] + user_metadata: Dict[str, Any] + aud: str + confirmation_sent_at: Union[datetime, None] = None + recovery_sent_at: Union[datetime, None] = None + email_change_sent_at: Union[datetime, None] = None + new_email: Union[str, None] = None + invited_at: Union[datetime, None] = None + action_link: Union[str, None] = None + email: Union[str, None] = None + phone: Union[str, None] = None + created_at: datetime + confirmed_at: Union[datetime, None] = None + email_confirmed_at: Union[datetime, None] = None + phone_confirmed_at: Union[datetime, None] = None + last_sign_in_at: Union[datetime, None] = None + role: Union[str, None] = None + updated_at: Union[datetime, None] = None + identities: Union[List[UserIdentity], None] = None -class BaseModelFromResponse(BaseModel): - @classmethod - def parse_response(cls: Type[T], response: Response) -> T: - check_response(response) - return cls.parse_obj(response.json()) +class UserAttributes(TypedDict, total=False): + email: str + phone: str + password: str + data: Any -class CookieOptions(BaseModelFromResponse): - name: str - """The name of the cookie. Defaults to `sb:token`.""" - lifetime: int - """The cookie lifetime (expiration) in seconds. Set to 8 hours by default.""" - domain: str - """The cookie domain this should run on. - Leave it blank to restrict it to your domain.""" - path: str - same_site: str - """SameSite configuration for the session cookie. - Defaults to 'lax', but can be changed to 'strict' or 'none'. - Set it to false if you want to disable the SameSite setting.""" +class AdminUserAttributes(UserAttributes, TypedDict, total=False): + user_metadata: Any + app_metadata: Any + email_confirm: bool + phone_confirm: bool + ban_duration: Union[str, Literal["none"]] -class Identity(BaseModelFromResponse): + +class Subscription(BaseModel): id: str - user_id: UUID - provider: str - created_at: datetime - updated_at: datetime - identity_data: Optional[Dict[str, Any]] = None - last_sign_in_at: Optional[datetime] = None + """ + The subscriber UUID. This will be set by the client. + """ + callback: Callable[[AuthChangeEvent, Union[Session, None]], None] + """ + The function to call every time there is an event. + """ + unsubscribe: Callable[[], None] + """ + Call this to remove the listener. + """ -class User(BaseModelFromResponse): - app_metadata: Dict[str, Any] - aud: str - """The user's audience. Use audiences to group users.""" - created_at: datetime - id: UUID - user_metadata: Dict[str, Any] - identities: Optional[List[Identity]] = None - confirmation_sent_at: Optional[datetime] = None - action_link: Optional[str] = None - last_sign_in_at: Optional[datetime] = None - phone: Optional[str] = None - phone_confirmed_at: Optional[datetime] = None - recovery_sent_at: Optional[datetime] = None - role: Optional[str] = None - updated_at: Optional[datetime] = None - email_confirmed_at: Optional[datetime] = None - confirmed_at: Optional[datetime] = None - invited_at: Optional[datetime] = None - email: Optional[str] = None - new_email: Optional[str] = None - email_change_sent_at: Optional[datetime] = None - new_phone: Optional[str] = None - phone_change_sent_at: Optional[datetime] = None - - -class UserAttributes(BaseModelFromResponse): - email: Optional[str] = None - """The user's email.""" - password: Optional[str] = None - """The user's password.""" - email_change_token: Optional[str] = None - """An email change token.""" - data: Optional[Any] = None - """A custom data object. Can be any JSON.""" - - -class Session(BaseModelFromResponse): - access_token: str - token_type: str - expires_at: Optional[int] = None - """A timestamp of when the token will expire. Returned when a login is confirmed.""" - expires_in: Optional[int] = None - """The number of seconds until the token expires (since it was issued). - Returned when a login is confirmed.""" - provider_token: Optional[str] = None - refresh_token: Optional[str] = None - user: Optional[User] = None +class SignUpWithEmailAndPasswordCredentialsOptions(TypedDict, total=False): + email_redirect_to: str + data: Any + captcha_token: str - @root_validator - def validator(cls, values: dict) -> dict: - expires_in = values.get("expires_in") - if expires_in and not values.get("expires_at"): - values["expires_at"] = round(time()) + expires_in - return values +class SignUpWithEmailAndPasswordCredentials(TypedDict): + email: str + password: str + options: NotRequired[SignUpWithEmailAndPasswordCredentialsOptions] -class AuthChangeEvent(str, Enum): - PASSWORD_RECOVERY = "PASSWORD_RECOVERY" - SIGNED_IN = "SIGNED_IN" - SIGNED_OUT = "SIGNED_OUT" - TOKEN_REFRESHED = "TOKEN_REFRESHED" - USER_UPDATED = "USER_UPDATED" - USER_DELETED = "USER_DELETED" +class SignUpWithPhoneAndPasswordCredentialsOptions(TypedDict, total=False): + data: Any + captcha_token: str -class Subscription(BaseModelFromResponse): - id: UUID - """The subscriber UUID. This will be set by the client.""" - callback: Callable[[AuthChangeEvent, Optional[Session]], None] - """The function to call every time there is an event.""" - unsubscribe: Callable[[], None] - """Call this to remove the listener.""" + +class SignUpWithPhoneAndPasswordCredentials(TypedDict): + phone: str + password: str + options: NotRequired[SignUpWithPhoneAndPasswordCredentialsOptions] + + +SignUpWithPasswordCredentials = Union[ + SignUpWithEmailAndPasswordCredentials, + SignUpWithPhoneAndPasswordCredentials, +] + + +class SignInWithPasswordCredentialsOptions(TypedDict, total=False): + captcha_token: str + + +class SignInWithEmailAndPasswordCredentials(TypedDict): + email: str + password: str + options: NotRequired[SignInWithPasswordCredentialsOptions] + + +class SignInWithPhoneAndPasswordCredentials(TypedDict): + phone: str + password: str + options: NotRequired[SignInWithPasswordCredentialsOptions] + + +SignInWithPasswordCredentials = Union[ + SignInWithEmailAndPasswordCredentials, + SignInWithPhoneAndPasswordCredentials, +] + + +class SignInWithEmailAndPasswordlessCredentialsOptions(TypedDict, total=False): + email_redirect_to: str + should_create_user: bool + data: Any + captcha_token: str + + +class SignInWithEmailAndPasswordlessCredentials(TypedDict): + email: str + options: NotRequired[SignInWithEmailAndPasswordlessCredentialsOptions] + + +class SignInWithPhoneAndPasswordlessCredentialsOptions(TypedDict, total=False): + should_create_user: bool + data: Any + captcha_token: str + + +class SignInWithPhoneAndPasswordlessCredentials(TypedDict): + phone: str + options: NotRequired[SignInWithPhoneAndPasswordlessCredentialsOptions] + + +SignInWithPasswordlessCredentials = Union[ + SignInWithEmailAndPasswordlessCredentials, + SignInWithPhoneAndPasswordlessCredentials, +] + + +class SignInWithOAuthCredentialsOptions(TypedDict, total=False): + redirect_to: str + scopes: str + query_params: Dict[str, str] + + +class SignInWithOAuthCredentials(TypedDict): + provider: Provider + options: NotRequired[SignInWithOAuthCredentialsOptions] + + +class VerifyOtpParamsOptions(TypedDict, total=False): + redirect_to: str + captcha_token: str + + +class VerifyEmailOtpParams(TypedDict): + email: str + token: str + type: Literal[ + "signup", + "invite", + "magiclink", + "recovery", + "email_change", + ] + options: NotRequired[VerifyOtpParamsOptions] + + +class VerifyMobileOtpParams(TypedDict): + phone: str + token: str + type: Literal[ + "sms", + "phone_change", + ] + options: NotRequired[VerifyOtpParamsOptions] + + +VerifyOtpParams = Union[ + VerifyEmailOtpParams, + VerifyMobileOtpParams, +] + + +class GenerateLinkParamsOptions(TypedDict, total=False): + redirect_to: str + + +class GenerateLinkParamsWithDataOptions( + GenerateLinkParamsOptions, + TypedDict, + total=False, +): + data: Any + + +class GenerateSignupLinkParams(TypedDict): + type: Literal["signup"] + email: str + password: str + options: NotRequired[GenerateLinkParamsWithDataOptions] -class Provider(str, Enum): - apple = "apple" - azure = "azure" - bitbucket = "bitbucket" - discord = "discord" - facebook = "facebook" - github = "github" - gitlab = "gitlab" - google = "google" - notion = "notion" - slack = "slack" - spotify = "spotify" - twitter = "twitter" - twitch = "twitch" +class GenerateInviteOrMagiclinkParams(TypedDict): + type: Literal["invite", "magiclink"] + email: str + options: NotRequired[GenerateLinkParamsWithDataOptions] -class LinkType(str, Enum): - """The type of link.""" +class GenerateRecoveryLinkParams(TypedDict): + type: Literal["recovery"] + email: str + options: NotRequired[GenerateLinkParamsOptions] - signup = "signup" - magiclink = "magiclink" - recovery = "recovery" - invite = "invite" +class GenerateEmailChangeLinkParams(TypedDict): + type: Literal["email_change"] + email: str + new_email: str + options: NotRequired[GenerateLinkParamsOptions] -class UserAttributesDict(TypedDict, total=False): - """Dict version of `UserAttributes`""" - email: Optional[str] - password: Optional[str] - email_change_token: Optional[str] - data: Optional[Any] +GenerateLinkParams = Union[ + GenerateSignupLinkParams, + GenerateInviteOrMagiclinkParams, + GenerateRecoveryLinkParams, + GenerateEmailChangeLinkParams, +] + +GenerateLinkType = Literal[ + "signup", + "invite", + "magiclink", + "recovery", + "email_change_current", + "email_change_new", +] + + +class GenerateLinkProperties(BaseModel): + """ + The properties related to the email link generated. + """ + + action_link: str + """ + The email link to send to the user. The action_link follows the following format: + + auth/v1/verify?type={verification_type}&token={hashed_token}&redirect_to={redirect_to} + """ + email_otp: str + """ + The raw email OTP. + You should send this in the email if you want your users to verify using an + OTP instead of the action link. + """ + hashed_token: str + """ + The hashed token appended to the action link. + """ + redirect_to: str + """ + The URL appended to the action link. + """ + verification_type: GenerateLinkType + """ + The verification type that the email link is associated to. + """ + + +class GenerateLinkResponse(BaseModel): + properties: GenerateLinkProperties + user: User diff --git a/tests/_async/test_api_with_auto_confirm_disabled.py b/tests/_async/test_api_with_auto_confirm_disabled.py index 3cd458dd..c780d5bf 100644 --- a/tests/_async/test_api_with_auto_confirm_disabled.py +++ b/tests/_async/test_api_with_auto_confirm_disabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import AsyncGoTrueAPI -from gotrue.constants import COOKIE_OPTIONS -from gotrue.types import CookieOptions, LinkType, User +from ...gotrue import AsyncGoTrueAPI +from ...gotrue.constants import COOKIE_OPTIONS +from ...gotrue.types import CookieOptions, GenerateLinkType, User GOTRUE_URL = "http://localhost:9999" TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 @@ -47,7 +47,7 @@ async def test_sign_up_with_email_and_password(api: AsyncGoTrueAPI): async def test_generate_sign_up_link(api: AsyncGoTrueAPI): try: response = await api.generate_link( - type=LinkType.signup, + type=GenerateLinkType.signup, email=email2, password=password2, redirect_to="http://localhost:9999/welcome", @@ -64,7 +64,7 @@ async def test_generate_sign_up_link(api: AsyncGoTrueAPI): async def test_generate_magic_link(api: AsyncGoTrueAPI): try: response = await api.generate_link( - type=LinkType.magiclink, + type=GenerateLinkType.magiclink, email=email3, redirect_to="http://localhost:9999/welcome", ) @@ -76,7 +76,7 @@ async def test_generate_magic_link(api: AsyncGoTrueAPI): async def test_generate_invite_link(api: AsyncGoTrueAPI): try: response = await api.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=email3, redirect_to="http://localhost:9999/welcome", ) @@ -89,7 +89,7 @@ async def test_generate_invite_link(api: AsyncGoTrueAPI): async def test_generate_recovery_link(api: AsyncGoTrueAPI): try: response = await api.generate_link( - type=LinkType.recovery, + type=GenerateLinkType.recovery, email=email, redirect_to="http://localhost:9999/welcome", ) diff --git a/tests/_async/test_api_with_auto_confirm_enabled.py b/tests/_async/test_api_with_auto_confirm_enabled.py index 13ff0411..cf7e6faa 100644 --- a/tests/_async/test_api_with_auto_confirm_enabled.py +++ b/tests/_async/test_api_with_auto_confirm_enabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import AsyncGoTrueAPI -from gotrue.constants import COOKIE_OPTIONS -from gotrue.types import CookieOptions, Session, User +from ...gotrue import AsyncGoTrueAPI +from ...gotrue.constants import COOKIE_OPTIONS +from ...gotrue.types import CookieOptions, Session, User GOTRUE_URL = "http://localhost:9998" TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjQyMjMyNzUwfQ.TUR8Zu05TtNR25L42soA2trZpc4oBR8-9Pv5r5bvls8" # noqa: E501 diff --git a/tests/_async/test_client_with_auto_confirm_disabled.py b/tests/_async/test_client_with_auto_confirm_disabled.py index c48d597d..4ba87a4f 100644 --- a/tests/_async/test_client_with_auto_confirm_disabled.py +++ b/tests/_async/test_client_with_auto_confirm_disabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import AsyncGoTrueClient -from gotrue.exceptions import APIError -from gotrue.types import User +from ...gotrue import AsyncGoTrueClient +from ...gotrue.errors import APIError +from ...gotrue.types import User GOTRUE_URL = "http://localhost:9999" TEST_TWILIO = False diff --git a/tests/_async/test_client_with_auto_confirm_enabled.py b/tests/_async/test_client_with_auto_confirm_enabled.py index be4eb104..e8dc566d 100644 --- a/tests/_async/test_client_with_auto_confirm_enabled.py +++ b/tests/_async/test_client_with_auto_confirm_enabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import AsyncGoTrueClient -from gotrue.exceptions import APIError -from gotrue.types import Session, User, UserAttributes +from ...gotrue import AsyncGoTrueClient +from ...gotrue.errors import APIError +from ...gotrue.types import Session, User, UserAttributes GOTRUE_URL = "http://localhost:9998" TEST_TWILIO = False diff --git a/tests/_async/test_client_with_sign_ups_disabled.py b/tests/_async/test_client_with_sign_ups_disabled.py index cae4bbd8..1557bb13 100644 --- a/tests/_async/test_client_with_sign_ups_disabled.py +++ b/tests/_async/test_client_with_sign_ups_disabled.py @@ -3,10 +3,10 @@ import pytest from faker import Faker -from gotrue import AsyncGoTrueAPI, AsyncGoTrueClient -from gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS -from gotrue.exceptions import APIError -from gotrue.types import CookieOptions, LinkType, User, UserAttributes +from ...gotrue import AsyncGoTrueAPI, AsyncGoTrueClient +from ...gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS +from ...gotrue.errors import APIError +from ...gotrue.types import CookieOptions, GenerateLinkType, User, UserAttributes GOTRUE_URL = "http://localhost:9997" AUTH_ADMIN_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 @@ -57,7 +57,7 @@ async def test_generate_link_should_be_able_to_generate_multiple_links( ): try: response = await auth_admin.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=invited_user, redirect_to="http://localhost:9997", ) @@ -77,7 +77,7 @@ async def test_generate_link_should_be_able_to_generate_multiple_links( assert response.identities == [] user = response response = await auth_admin.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=invited_user, ) assert isinstance(response, User) diff --git a/tests/_async/test_provider.py b/tests/_async/test_provider.py index 9f0df26f..ac7c9866 100644 --- a/tests/_async/test_provider.py +++ b/tests/_async/test_provider.py @@ -2,8 +2,8 @@ import pytest -from gotrue import AsyncGoTrueClient -from gotrue.types import Provider +from ...gotrue import AsyncGoTrueClient +from ...gotrue.types import Provider GOTRUE_URL = "http://localhost:9999" diff --git a/tests/_async/test_subscriptions.py b/tests/_async/test_subscriptions.py index 33b9533e..c4780294 100644 --- a/tests/_async/test_subscriptions.py +++ b/tests/_async/test_subscriptions.py @@ -1,7 +1,7 @@ import pytest -from gotrue import AsyncGoTrueClient -from gotrue.types import Subscription +from ...gotrue import AsyncGoTrueClient +from ...gotrue.types import Subscription GOTRUE_URL = "http://localhost:9999" diff --git a/tests/_sync/test_api_with_auto_confirm_disabled.py b/tests/_sync/test_api_with_auto_confirm_disabled.py index b87f489c..2f4a07c5 100644 --- a/tests/_sync/test_api_with_auto_confirm_disabled.py +++ b/tests/_sync/test_api_with_auto_confirm_disabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import SyncGoTrueAPI -from gotrue.constants import COOKIE_OPTIONS -from gotrue.types import CookieOptions, LinkType, User +from ...gotrue import SyncGoTrueAPI +from ...gotrue.constants import COOKIE_OPTIONS +from ...gotrue.types import CookieOptions, GenerateLinkType, User GOTRUE_URL = "http://localhost:9999" TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 @@ -47,7 +47,7 @@ def test_sign_up_with_email_and_password(api: SyncGoTrueAPI): def test_generate_sign_up_link(api: SyncGoTrueAPI): try: response = api.generate_link( - type=LinkType.signup, + type=GenerateLinkType.signup, email=email2, password=password2, redirect_to="http://localhost:9999/welcome", @@ -64,7 +64,7 @@ def test_generate_sign_up_link(api: SyncGoTrueAPI): def test_generate_magic_link(api: SyncGoTrueAPI): try: response = api.generate_link( - type=LinkType.magiclink, + type=GenerateLinkType.magiclink, email=email3, redirect_to="http://localhost:9999/welcome", ) @@ -76,7 +76,7 @@ def test_generate_magic_link(api: SyncGoTrueAPI): def test_generate_invite_link(api: SyncGoTrueAPI): try: response = api.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=email3, redirect_to="http://localhost:9999/welcome", ) @@ -89,7 +89,7 @@ def test_generate_invite_link(api: SyncGoTrueAPI): def test_generate_recovery_link(api: SyncGoTrueAPI): try: response = api.generate_link( - type=LinkType.recovery, + type=GenerateLinkType.recovery, email=email, redirect_to="http://localhost:9999/welcome", ) diff --git a/tests/_sync/test_api_with_auto_confirm_enabled.py b/tests/_sync/test_api_with_auto_confirm_enabled.py index 578646a8..67edaa81 100644 --- a/tests/_sync/test_api_with_auto_confirm_enabled.py +++ b/tests/_sync/test_api_with_auto_confirm_enabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import SyncGoTrueAPI -from gotrue.constants import COOKIE_OPTIONS -from gotrue.types import CookieOptions, Session, User +from ...gotrue import SyncGoTrueAPI +from ...gotrue.constants import COOKIE_OPTIONS +from ...gotrue.types import CookieOptions, Session, User GOTRUE_URL = "http://localhost:9998" TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjQyMjMyNzUwfQ.TUR8Zu05TtNR25L42soA2trZpc4oBR8-9Pv5r5bvls8" # noqa: E501 diff --git a/tests/_sync/test_client_with_auto_confirm_disabled.py b/tests/_sync/test_client_with_auto_confirm_disabled.py index b9a0a4d0..587b5d87 100644 --- a/tests/_sync/test_client_with_auto_confirm_disabled.py +++ b/tests/_sync/test_client_with_auto_confirm_disabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import SyncGoTrueClient -from gotrue.exceptions import APIError -from gotrue.types import User +from ...gotrue import SyncGoTrueClient +from ...gotrue.errors import APIError +from ...gotrue.types import User GOTRUE_URL = "http://localhost:9999" TEST_TWILIO = False diff --git a/tests/_sync/test_client_with_auto_confirm_enabled.py b/tests/_sync/test_client_with_auto_confirm_enabled.py index e3a96f81..72407be2 100644 --- a/tests/_sync/test_client_with_auto_confirm_enabled.py +++ b/tests/_sync/test_client_with_auto_confirm_enabled.py @@ -3,9 +3,9 @@ import pytest from faker import Faker -from gotrue import SyncGoTrueClient -from gotrue.exceptions import APIError -from gotrue.types import Session, User, UserAttributes +from ...gotrue import SyncGoTrueClient +from ...gotrue.errors import APIError +from ...gotrue.types import Session, User, UserAttributes GOTRUE_URL = "http://localhost:9998" TEST_TWILIO = False diff --git a/tests/_sync/test_client_with_sign_ups_disabled.py b/tests/_sync/test_client_with_sign_ups_disabled.py index 0e0cb9c8..b5b59302 100644 --- a/tests/_sync/test_client_with_sign_ups_disabled.py +++ b/tests/_sync/test_client_with_sign_ups_disabled.py @@ -3,10 +3,10 @@ import pytest from faker import Faker -from gotrue import SyncGoTrueAPI, SyncGoTrueClient -from gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS -from gotrue.exceptions import APIError -from gotrue.types import CookieOptions, LinkType, User, UserAttributes +from ...gotrue import SyncGoTrueAPI, SyncGoTrueClient +from ...gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS +from ...gotrue.errors import APIError +from ...gotrue.types import CookieOptions, GenerateLinkType, User, UserAttributes GOTRUE_URL = "http://localhost:9997" AUTH_ADMIN_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 @@ -57,7 +57,7 @@ def test_generate_link_should_be_able_to_generate_multiple_links( ): try: response = auth_admin.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=invited_user, redirect_to="http://localhost:9997", ) @@ -77,7 +77,7 @@ def test_generate_link_should_be_able_to_generate_multiple_links( assert response.identities == [] user = response response = auth_admin.generate_link( - type=LinkType.invite, + type=GenerateLinkType.invite, email=invited_user, ) assert isinstance(response, User) diff --git a/tests/_sync/test_provider.py b/tests/_sync/test_provider.py index bb6dcc38..cc233bf2 100644 --- a/tests/_sync/test_provider.py +++ b/tests/_sync/test_provider.py @@ -2,8 +2,8 @@ import pytest -from gotrue import SyncGoTrueClient -from gotrue.types import Provider +from ...gotrue import SyncGoTrueClient +from ...gotrue.types import Provider GOTRUE_URL = "http://localhost:9999" diff --git a/tests/_sync/test_subscriptions.py b/tests/_sync/test_subscriptions.py index c9df5808..3c0f7238 100644 --- a/tests/_sync/test_subscriptions.py +++ b/tests/_sync/test_subscriptions.py @@ -1,7 +1,7 @@ import pytest -from gotrue import SyncGoTrueClient -from gotrue.types import Subscription +from ...gotrue import SyncGoTrueClient +from ...gotrue.types import Subscription GOTRUE_URL = "http://localhost:9999" From 9e4a1f2c7caba255cb9e31eb546f28a9a469b205 Mon Sep 17 00:00:00 2001 From: "sourcery-ai[bot]" <58596630+sourcery-ai[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 02:03:25 -0400 Subject: [PATCH 12/35] 'Refactored by Sourcery' (#175) Co-authored-by: Sourcery AI <> --- gotrue/_async/gotrue_client.py | 16 +++++++--------- gotrue/_sync/gotrue_client.py | 16 +++++++--------- .../test_client_with_auto_confirm_disabled.py | 5 +---- .../test_client_with_auto_confirm_disabled.py | 5 +---- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index a19772ef..d41006c2 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -331,9 +331,11 @@ async def get_session(self) -> Union[Session, None]: if current_session.expires_at else False ) - if not has_expired: - return current_session - return await self._call_refresh_token(current_session.refresh_token) + return ( + await self._call_refresh_token(current_session.refresh_token) + if has_expired + else current_session + ) async def get_user(self, jwt: Union[str, None] = None) -> UserResponse: """ @@ -582,7 +584,6 @@ async def _save_session(self, session: Session) -> None: self._in_memory_session = session expire_at = session.expires_at if expire_at: - pass time_now = round(time()) expire_in = expire_at - time_now refresh_duration_before_expires = ( @@ -648,8 +649,7 @@ def _get_valid_session( except ValueError: return None try: - session = Session.parse_obj(data) - return session + return Session.parse_obj(data) except Exception: return None @@ -658,9 +658,7 @@ def _get_param( query_params: Dict[str, List[str]], name: str, ) -> Union[str, None]: - if name in query_params: - return query_params[name][0] - return None + return query_params[name][0] if name in query_params else None def _is_implicit_grant_flow(self, url: str) -> bool: result = urlparse(url) diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index 15571e19..b4a77bf6 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -331,9 +331,11 @@ def get_session(self) -> Union[Session, None]: if current_session.expires_at else False ) - if not has_expired: - return current_session - return self._call_refresh_token(current_session.refresh_token) + return ( + self._call_refresh_token(current_session.refresh_token) + if has_expired + else current_session + ) def get_user(self, jwt: Union[str, None] = None) -> UserResponse: """ @@ -582,7 +584,6 @@ def _save_session(self, session: Session) -> None: self._in_memory_session = session expire_at = session.expires_at if expire_at: - pass time_now = round(time()) expire_in = expire_at - time_now refresh_duration_before_expires = ( @@ -648,8 +649,7 @@ def _get_valid_session( except ValueError: return None try: - session = Session.parse_obj(data) - return session + return Session.parse_obj(data) except Exception: return None @@ -658,9 +658,7 @@ def _get_param( query_params: Dict[str, List[str]], name: str, ) -> Union[str, None]: - if name in query_params: - return query_params[name][0] - return None + return query_params[name][0] if name in query_params else None def _is_implicit_grant_flow(self, url: str) -> bool: result = urlparse(url) diff --git a/tests/_async/test_client_with_auto_confirm_disabled.py b/tests/_async/test_client_with_auto_confirm_disabled.py index 4ba87a4f..5225a6e9 100644 --- a/tests/_async/test_client_with_auto_confirm_disabled.py +++ b/tests/_async/test_client_with_auto_confirm_disabled.py @@ -79,10 +79,7 @@ async def test_sign_in(client: AsyncGoTrueClient): async def test_sign_in_with_the_wrong_password(client: AsyncGoTrueClient): expected_error_message = "Invalid login credentials" try: - await client.sign_in( - email=email, - password=password + "2", - ) + await client.sign_in(email=email, password=f"{password}2") assert False except APIError as e: assert e.msg == expected_error_message diff --git a/tests/_sync/test_client_with_auto_confirm_disabled.py b/tests/_sync/test_client_with_auto_confirm_disabled.py index 587b5d87..b4c2d325 100644 --- a/tests/_sync/test_client_with_auto_confirm_disabled.py +++ b/tests/_sync/test_client_with_auto_confirm_disabled.py @@ -79,10 +79,7 @@ def test_sign_in(client: SyncGoTrueClient): def test_sign_in_with_the_wrong_password(client: SyncGoTrueClient): expected_error_message = "Invalid login credentials" try: - client.sign_in( - email=email, - password=password + "2", - ) + client.sign_in(email=email, password=f"{password}2") assert False except APIError as e: assert e.msg == expected_error_message From 6ffa53227d991c2135b5a1fd5202fea97e8d6097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Tue, 18 Oct 2022 06:07:42 +0000 Subject: [PATCH 13/35] fix: implement the reset_password_email method --- gotrue/_async/gotrue_base_api.py | 1 + gotrue/_async/gotrue_client.py | 12 +++++++++++- gotrue/_sync/gotrue_base_api.py | 1 + gotrue/_sync/gotrue_client.py | 12 +++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py index 89ed39d7..cd33b279 100644 --- a/gotrue/_async/gotrue_base_api.py +++ b/gotrue/_async/gotrue_base_api.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload from pydantic import BaseModel diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index d41006c2..88d93f3a 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -462,7 +462,17 @@ async def reset_password_email( """ Sends a password reset request to an email address. """ - raise NotImplementedError + await self._request( + "POST", + "recover", + body={ + "email": email, + "gotrue_meta_security": { + "captcha_token": options.get("captcha_token"), + }, + }, + redirect_to=options.get("redirect_to"), + ) # Private methods diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py index a1868fce..fa6750c4 100644 --- a/gotrue/_sync/gotrue_base_api.py +++ b/gotrue/_sync/gotrue_base_api.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload from pydantic import BaseModel diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index b4a77bf6..ce95ed19 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -462,7 +462,17 @@ def reset_password_email( """ Sends a password reset request to an email address. """ - raise NotImplementedError + self._request( + "POST", + "recover", + body={ + "email": email, + "gotrue_meta_security": { + "captcha_token": options.get("captcha_token"), + }, + }, + redirect_to=options.get("redirect_to"), + ) # Private methods From 332f782db88921ef30a5362e054fd39d26166d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Tue, 18 Oct 2022 06:17:31 +0000 Subject: [PATCH 14/35] fix: insert default content type header --- gotrue/_async/gotrue_base_api.py | 2 ++ gotrue/_sync/gotrue_base_api.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py index cd33b279..f8eddbce 100644 --- a/gotrue/_async/gotrue_base_api.py +++ b/gotrue/_async/gotrue_base_api.py @@ -78,6 +78,8 @@ async def _request( ) -> Union[T, None]: url = f"{self._url}/{path}" headers = {**self._headers, **(headers or {})} + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json;charset=UTF-8" if jwt: headers["Authorization"] = f"Bearer {jwt}" query = query or {} diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py index fa6750c4..bab2305c 100644 --- a/gotrue/_sync/gotrue_base_api.py +++ b/gotrue/_sync/gotrue_base_api.py @@ -78,6 +78,8 @@ def _request( ) -> Union[T, None]: url = f"{self._url}/{path}" headers = {**self._headers, **(headers or {})} + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json;charset=UTF-8" if jwt: headers["Authorization"] = f"Bearer {jwt}" query = query or {} From c3ff22a0a8758585ee77a68381041b77ac25a234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Fri, 21 Oct 2022 00:19:42 +0000 Subject: [PATCH 15/35] feat: implement decode_jwt_payload in helpers --- gotrue/helpers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gotrue/helpers.py b/gotrue/helpers.py index 53a5cf7b..6aa73025 100644 --- a/gotrue/helpers.py +++ b/gotrue/helpers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from base64 import b64decode +from json import loads from typing import Any, Union, cast from httpx import HTTPStatusError @@ -74,3 +76,11 @@ def handle_exception(exception: Exception) -> AuthError: return AuthApiError(get_error_message(json), error.response.status_code or 500) except Exception as e: return AuthUnknownError(get_error_message(error), e) + + +def decode_jwt_payload(token: str) -> Any: + parts = token.split(".") + if len(parts) != 3: + raise ValueError("JWT is not valid: not a JWT structure") + base64Url = parts[1] + return loads(b64decode(base64Url).decode("utf-8")) From 4c2b4437d0b99c6f19ec251e245909d86ca69f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Fri, 21 Oct 2022 04:26:31 +0000 Subject: [PATCH 16/35] feat: add mfa --- gotrue/_async/gotrue_admin_api.py | 28 +++ gotrue/_async/gotrue_admin_mfa_api.py | 32 +++ gotrue/_async/gotrue_base_api.py | 23 +- gotrue/_async/gotrue_client.py | 130 +++++++++- gotrue/_async/gotrue_mfa_api.py | 82 +++++++ gotrue/_sync/gotrue_admin_api.py | 28 +++ gotrue/_sync/gotrue_admin_mfa_api.py | 32 +++ gotrue/_sync/gotrue_base_api.py | 23 +- gotrue/_sync/gotrue_client.py | 130 +++++++++- gotrue/_sync/gotrue_mfa_api.py | 82 +++++++ gotrue/types.py | 326 ++++++++++++++++++++++---- 11 files changed, 843 insertions(+), 73 deletions(-) create mode 100644 gotrue/_async/gotrue_admin_mfa_api.py create mode 100644 gotrue/_async/gotrue_mfa_api.py create mode 100644 gotrue/_sync/gotrue_admin_mfa_api.py create mode 100644 gotrue/_sync/gotrue_mfa_api.py diff --git a/gotrue/_async/gotrue_admin_api.py b/gotrue/_async/gotrue_admin_api.py index 7a568cf1..29dc4343 100644 --- a/gotrue/_async/gotrue_admin_api.py +++ b/gotrue/_async/gotrue_admin_api.py @@ -6,12 +6,17 @@ from ..http_clients import AsyncClient from ..types import ( AdminUserAttributes, + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, GenerateLinkParams, GenerateLinkResponse, Options, User, UserResponse, ) +from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI from .gotrue_base_api import AsyncGoTrueBaseAPI @@ -29,6 +34,9 @@ def __init__( headers=headers, http_client=http_client, ) + self.mfa = AsyncGoTrueAdminMFAAPI() + self.mfa.list_factors = self._list_factors + self.mfa.delete_factor = self._delete_factor async def sign_out(self, jwt: str) -> None: """ @@ -142,3 +150,23 @@ async def delete_user(self, id: str) -> UserResponse: f"admin/users/{id}", xform=parse_user_response, ) + + async def _list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + return await self._request( + "GET", + f"admin/users/{params.get('user_id')}/factors", + xform=AuthMFAAdminListFactorsResponse.parse_obj, + ) + + async def _delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + return await self._request( + "DELETE", + f"admin/users/{params.get('user_id')}/factors/{params.get('factor_id')}", + xform=AuthMFAAdminDeleteFactorResponse.parse_obj, + ) diff --git a/gotrue/_async/gotrue_admin_mfa_api.py b/gotrue/_async/gotrue_admin_mfa_api.py new file mode 100644 index 00000000..4fd429c2 --- /dev/null +++ b/gotrue/_async/gotrue_admin_mfa_api.py @@ -0,0 +1,32 @@ +from ..types import ( + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, +) + + +class AsyncGoTrueAdminMFAAPI: + """ + Contains the full multi-factor authentication administration API. + """ + + async def list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + """ + Lists all factors attached to a user. + """ + raise NotImplementedError() + + async def delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + """ + Deletes a factor on a user. This will log the user out of all active + sessions (if the deleted factor was verified). There's no need to delete + unverified factors. + """ + raise NotImplementedError() diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py index f8eddbce..757b9c89 100644 --- a/gotrue/_async/gotrue_base_api.py +++ b/gotrue/_async/gotrue_base_api.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload +from httpx import Response from pydantic import BaseModel from typing_extensions import Self @@ -43,7 +44,7 @@ async def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: Literal[False] = False, xform: Callable[[Any], T], ) -> T: ... @@ -59,7 +60,23 @@ async def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: Literal[True], + xform: Callable[[Response], T], + ) -> T: + ... + + @overload + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: bool = False, ) -> None: ... @@ -73,7 +90,7 @@ async def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: bool = False, xform: Union[Callable[[Any], T], None] = None, ) -> Union[T, None]: url = f"{self._url}/{path}" diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index 88d93f3a..75efca59 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -1,6 +1,5 @@ from __future__ import annotations -from base64 import b64decode from json import loads from time import time from typing import Callable, Dict, List, Tuple, Union @@ -21,12 +20,24 @@ AuthRetryableError, AuthSessionMissingError, ) -from ..helpers import parse_auth_response, parse_user_response +from ..helpers import decode_jwt_payload, parse_auth_response, parse_user_response from ..http_clients import AsyncClient from ..timer import Timer from ..types import ( AuthChangeEvent, + AuthenticatorAssuranceLevels, + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, AuthResponse, + DecodedJWTDict, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, OAuthResponse, Options, Provider, @@ -42,6 +53,7 @@ ) from .gotrue_admin_api import AsyncGoTrueAdminAPI from .gotrue_base_api import AsyncGoTrueBaseAPI +from .gotrue_mfa_api import AsyncGoTrueMFAAPI from .storage import AsyncMemoryStorage, AsyncSupportedStorage @@ -77,6 +89,15 @@ def __init__( headers=self._headers, http_client=self._http_client, ) + self.mfa = AsyncGoTrueMFAAPI() + self.mfa.challenge = self._challenge + self.mfa.enroll = self._enroll + self.mfa.get_authenticator_assurance_level = ( + self._get_authenticator_assurance_level + ) + self.mfa.list_factors = self._list_factors + self.mfa.unenroll = self._unenroll + self.mfa.verify = self._verify # Initializations @@ -389,10 +410,10 @@ async def set_session(self, access_token: str, refresh_token: str) -> AuthRespon has_expired = True session: Union[Session, None] = None if access_token and access_token.split(".")[1]: - json_raw = b64decode(access_token.split(".")[1] + "===").decode("utf-8") - payload = loads(json_raw) - if payload.get("exp"): - expires_at = int(payload.get("exp")) + payload = self._decode_jwt(access_token) + exp = payload.get("exp") + if exp: + expires_at = int(exp) has_expired = expires_at <= time_now if has_expired: if not refresh_token: @@ -474,6 +495,94 @@ async def reset_password_email( redirect_to=options.get("redirect_to"), ) + # MFA methods + + async def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + response = await self._request( + "POST", + "factors", + body=params, + jwt=session.access_token, + xform=AuthMFAEnrollResponse.parse_obj, + ) + if response.totp.qr_code: + response.totp.qr_code = f"data:image/svg+xml;utf-8,{response.totp.qr_code}" + return response + + async def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + return await self._request( + "POST", + f"factors/{params.get('factor_id')}/challenge", + jwt=session.access_token, + xform=AuthMFAChallengeResponse.parse_obj, + ) + + async def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + response = await self._request( + "POST", + f"factors/{params.get('factor_id')}/verify", + body=params, + jwt=session.access_token, + xform=AuthMFAVerifyResponse.parse_obj, + ) + session = Session.parse_obj(response.dict()) + await self._save_session(session) + self._notify_all_subscribers("MFA_CHALLENGE_VERIFIED", session) + return response + + async def _unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + return await self._request( + "DELETE", + f"factors/{params.get('factor_id')}", + jwt=session.access_token, + xform=AuthMFAUnenrollResponse.parse_obj, + ) + + async def _list_factors(self) -> AuthMFAListFactorsResponse: + response = await self.get_user() + all = response.user.factors or [] + totp = [f for f in all if f.factor_type == "totp" and f.status == "verified"] + return AuthMFAListFactorsResponse(all=all, totp=totp) + + async def _get_authenticator_assurance_level( + self, + ) -> AuthMFAGetAuthenticatorAssuranceLevelResponse: + session = await self.get_session() + if not session: + return AuthMFAGetAuthenticatorAssuranceLevelResponse( + current_level=None, + next_level=None, + current_authentication_methods=[], + ) + payload = self._decode_jwt(session.access_token) + current_level: Union[AuthenticatorAssuranceLevels, None] = None + if payload.get("aal"): + current_level = payload.get("aal") + next_level = current_level + verified_factors = [ + f for f in session.user.factors or [] if f.status == "verified" + ] + if verified_factors: + next_level = "aal2" + current_authentication_methods = payload.get("amr") or [] + return AuthMFAGetAuthenticatorAssuranceLevelResponse( + current_level=current_level, + next_level=next_level, + current_authentication_methods=current_authentication_methods, + ) + # Private methods async def _remove_session(self) -> None: @@ -685,7 +794,8 @@ def _get_url_for_provider( query = urlencode(params) return f"{self._url}/authorize?{query}" - -async def test(): - client = AsyncGoTrueClient() - await client.initialize() + def _decode_jwt(self, jwt: str) -> DecodedJWTDict: + """ + Decodes a JWT (without performing any validation). + """ + return decode_jwt_payload(jwt) diff --git a/gotrue/_async/gotrue_mfa_api.py b/gotrue/_async/gotrue_mfa_api.py new file mode 100644 index 00000000..feb6e0be --- /dev/null +++ b/gotrue/_async/gotrue_mfa_api.py @@ -0,0 +1,82 @@ +from ..types import ( + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, +) + + +class AsyncGoTrueMFAAPI: + """ + Contains the full multi-factor authentication API. + """ + + async def enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + """ + Starts the enrollment process for a new Multi-Factor Authentication + factor. This method creates a new factor in the 'unverified' state. + Present the QR code or secret to the user and ask them to add it to their + authenticator app. Ask the user to provide you with an authenticator code + from their app and verify it by calling challenge and then verify. + + The first successful verification of an unverified factor activates the + factor. All other sessions are logged out and the current one gets an + `aal2` authenticator level. + """ + raise NotImplementedError() + + async def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: + """ + Prepares a challenge used to verify that a user has access to a MFA + factor. Provide the challenge ID and verification code by calling `verify`. + """ + raise NotImplementedError() + + async def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: + """ + Verifies a verification code against a challenge. The verification code is + provided by the user by entering a code seen in their authenticator app. + """ + raise NotImplementedError() + + async def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: + """ + Unenroll removes a MFA factor. Unverified factors can safely be ignored + and it's not necessary to unenroll them. Unenrolling a verified MFA factor + cannot be done from a session with an `aal1` authenticator level. + """ + raise NotImplementedError() + + async def list_factors(self) -> AuthMFAListFactorsResponse: + """ + Returns the list of MFA factors enabled for this user. For most use cases + you should consider using `get_authenticator_assurance_level`. + + This uses a cached version of the factors and avoids incurring a network call. + If you need to update this list, call `get_user` first. + """ + raise NotImplementedError() + + async def get_authenticator_assurance_level( + self, + ) -> AuthMFAGetAuthenticatorAssuranceLevelResponse: + """ + Returns the Authenticator Assurance Level (AAL) for the active session. + + - `aal1` (or `null`) means that the user's identity has been verified only + with a conventional login (email+password, OTP, magic link, social login, + etc.). + - `aal2` means that the user's identity has been verified both with a + conventional login and at least one MFA factor. + + Although this method returns a promise, it's fairly quick (microseconds) + and rarely uses the network. You can use this to check whether the current + user needs to be shown a screen to verify their MFA factors. + """ + raise NotImplementedError() diff --git a/gotrue/_sync/gotrue_admin_api.py b/gotrue/_sync/gotrue_admin_api.py index 00576d58..2fdf2094 100644 --- a/gotrue/_sync/gotrue_admin_api.py +++ b/gotrue/_sync/gotrue_admin_api.py @@ -6,12 +6,17 @@ from ..http_clients import SyncClient from ..types import ( AdminUserAttributes, + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, GenerateLinkParams, GenerateLinkResponse, Options, User, UserResponse, ) +from .gotrue_admin_mfa_api import SyncGoTrueAdminMFAAPI from .gotrue_base_api import SyncGoTrueBaseAPI @@ -29,6 +34,9 @@ def __init__( headers=headers, http_client=http_client, ) + self.mfa = SyncGoTrueAdminMFAAPI() + self.mfa.list_factors = self._list_factors + self.mfa.delete_factor = self._delete_factor def sign_out(self, jwt: str) -> None: """ @@ -142,3 +150,23 @@ def delete_user(self, id: str) -> UserResponse: f"admin/users/{id}", xform=parse_user_response, ) + + def _list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + return self._request( + "GET", + f"admin/users/{params.get('user_id')}/factors", + xform=AuthMFAAdminListFactorsResponse.parse_obj, + ) + + def _delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + return self._request( + "DELETE", + f"admin/users/{params.get('user_id')}/factors/{params.get('factor_id')}", + xform=AuthMFAAdminDeleteFactorResponse.parse_obj, + ) diff --git a/gotrue/_sync/gotrue_admin_mfa_api.py b/gotrue/_sync/gotrue_admin_mfa_api.py new file mode 100644 index 00000000..51c1681b --- /dev/null +++ b/gotrue/_sync/gotrue_admin_mfa_api.py @@ -0,0 +1,32 @@ +from ..types import ( + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, +) + + +class SyncGoTrueAdminMFAAPI: + """ + Contains the full multi-factor authentication administration API. + """ + + def list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + """ + Lists all factors attached to a user. + """ + raise NotImplementedError() + + def delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + """ + Deletes a factor on a user. This will log the user out of all active + sessions (if the deleted factor was verified). There's no need to delete + unverified factors. + """ + raise NotImplementedError() diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py index bab2305c..99a78b87 100644 --- a/gotrue/_sync/gotrue_base_api.py +++ b/gotrue/_sync/gotrue_base_api.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload +from httpx import Response from pydantic import BaseModel from typing_extensions import Self @@ -43,7 +44,7 @@ def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: Literal[False] = False, xform: Callable[[Any], T], ) -> T: ... @@ -59,7 +60,23 @@ def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: Literal[True], + xform: Callable[[Response], T], + ) -> T: + ... + + @overload + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Union[str, None] = None, + redirect_to: Union[str, None] = None, + headers: Union[Dict[str, str], None] = None, + query: Union[Dict[str, str], None] = None, + body: Union[Any, None] = None, + no_resolve_json: bool = False, ) -> None: ... @@ -73,7 +90,7 @@ def _request( headers: Union[Dict[str, str], None] = None, query: Union[Dict[str, str], None] = None, body: Union[Any, None] = None, - no_resolve_json: Union[bool, None] = None, + no_resolve_json: bool = False, xform: Union[Callable[[Any], T], None] = None, ) -> Union[T, None]: url = f"{self._url}/{path}" diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index ce95ed19..471cfcbf 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -1,6 +1,5 @@ from __future__ import annotations -from base64 import b64decode from json import loads from time import time from typing import Callable, Dict, List, Tuple, Union @@ -21,12 +20,24 @@ AuthRetryableError, AuthSessionMissingError, ) -from ..helpers import parse_auth_response, parse_user_response +from ..helpers import decode_jwt_payload, parse_auth_response, parse_user_response from ..http_clients import SyncClient from ..timer import Timer from ..types import ( AuthChangeEvent, + AuthenticatorAssuranceLevels, + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, AuthResponse, + DecodedJWTDict, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, OAuthResponse, Options, Provider, @@ -42,6 +53,7 @@ ) from .gotrue_admin_api import SyncGoTrueAdminAPI from .gotrue_base_api import SyncGoTrueBaseAPI +from .gotrue_mfa_api import SyncGoTrueMFAAPI from .storage import SyncMemoryStorage, SyncSupportedStorage @@ -77,6 +89,15 @@ def __init__( headers=self._headers, http_client=self._http_client, ) + self.mfa = SyncGoTrueMFAAPI() + self.mfa.challenge = self._challenge + self.mfa.enroll = self._enroll + self.mfa.get_authenticator_assurance_level = ( + self._get_authenticator_assurance_level + ) + self.mfa.list_factors = self._list_factors + self.mfa.unenroll = self._unenroll + self.mfa.verify = self._verify # Initializations @@ -389,10 +410,10 @@ def set_session(self, access_token: str, refresh_token: str) -> AuthResponse: has_expired = True session: Union[Session, None] = None if access_token and access_token.split(".")[1]: - json_raw = b64decode(access_token.split(".")[1] + "===").decode("utf-8") - payload = loads(json_raw) - if payload.get("exp"): - expires_at = int(payload.get("exp")) + payload = self._decode_jwt(access_token) + exp = payload.get("exp") + if exp: + expires_at = int(exp) has_expired = expires_at <= time_now if has_expired: if not refresh_token: @@ -474,6 +495,94 @@ def reset_password_email( redirect_to=options.get("redirect_to"), ) + # MFA methods + + def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + response = self._request( + "POST", + "factors", + body=params, + jwt=session.access_token, + xform=AuthMFAEnrollResponse.parse_obj, + ) + if response.totp.qr_code: + response.totp.qr_code = f"data:image/svg+xml;utf-8,{response.totp.qr_code}" + return response + + def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + return self._request( + "POST", + f"factors/{params.get('factor_id')}/challenge", + jwt=session.access_token, + xform=AuthMFAChallengeResponse.parse_obj, + ) + + def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + response = self._request( + "POST", + f"factors/{params.get('factor_id')}/verify", + body=params, + jwt=session.access_token, + xform=AuthMFAVerifyResponse.parse_obj, + ) + session = Session.parse_obj(response.dict()) + self._save_session(session) + self._notify_all_subscribers("MFA_CHALLENGE_VERIFIED", session) + return response + + def _unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + return self._request( + "DELETE", + f"factors/{params.get('factor_id')}", + jwt=session.access_token, + xform=AuthMFAUnenrollResponse.parse_obj, + ) + + def _list_factors(self) -> AuthMFAListFactorsResponse: + response = self.get_user() + all = response.user.factors or [] + totp = [f for f in all if f.factor_type == "totp" and f.status == "verified"] + return AuthMFAListFactorsResponse(all=all, totp=totp) + + def _get_authenticator_assurance_level( + self, + ) -> AuthMFAGetAuthenticatorAssuranceLevelResponse: + session = self.get_session() + if not session: + return AuthMFAGetAuthenticatorAssuranceLevelResponse( + current_level=None, + next_level=None, + current_authentication_methods=[], + ) + payload = self._decode_jwt(session.access_token) + current_level: Union[AuthenticatorAssuranceLevels, None] = None + if payload.get("aal"): + current_level = payload.get("aal") + next_level = current_level + verified_factors = [ + f for f in session.user.factors or [] if f.status == "verified" + ] + if verified_factors: + next_level = "aal2" + current_authentication_methods = payload.get("amr") or [] + return AuthMFAGetAuthenticatorAssuranceLevelResponse( + current_level=current_level, + next_level=next_level, + current_authentication_methods=current_authentication_methods, + ) + # Private methods def _remove_session(self) -> None: @@ -685,7 +794,8 @@ def _get_url_for_provider( query = urlencode(params) return f"{self._url}/authorize?{query}" - -def test(): - client = SyncGoTrueClient() - client.initialize() + def _decode_jwt(self, jwt: str) -> DecodedJWTDict: + """ + Decodes a JWT (without performing any validation). + """ + return decode_jwt_payload(jwt) diff --git a/gotrue/_sync/gotrue_mfa_api.py b/gotrue/_sync/gotrue_mfa_api.py new file mode 100644 index 00000000..78b3df99 --- /dev/null +++ b/gotrue/_sync/gotrue_mfa_api.py @@ -0,0 +1,82 @@ +from ..types import ( + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, +) + + +class SyncGoTrueMFAAPI: + """ + Contains the full multi-factor authentication API. + """ + + def enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + """ + Starts the enrollment process for a new Multi-Factor Authentication + factor. This method creates a new factor in the 'unverified' state. + Present the QR code or secret to the user and ask them to add it to their + authenticator app. Ask the user to provide you with an authenticator code + from their app and verify it by calling challenge and then verify. + + The first successful verification of an unverified factor activates the + factor. All other sessions are logged out and the current one gets an + `aal2` authenticator level. + """ + raise NotImplementedError() + + def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: + """ + Prepares a challenge used to verify that a user has access to a MFA + factor. Provide the challenge ID and verification code by calling `verify`. + """ + raise NotImplementedError() + + def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: + """ + Verifies a verification code against a challenge. The verification code is + provided by the user by entering a code seen in their authenticator app. + """ + raise NotImplementedError() + + def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: + """ + Unenroll removes a MFA factor. Unverified factors can safely be ignored + and it's not necessary to unenroll them. Unenrolling a verified MFA factor + cannot be done from a session with an `aal1` authenticator level. + """ + raise NotImplementedError() + + def list_factors(self) -> AuthMFAListFactorsResponse: + """ + Returns the list of MFA factors enabled for this user. For most use cases + you should consider using `get_authenticator_assurance_level`. + + This uses a cached version of the factors and avoids incurring a network call. + If you need to update this list, call `get_user` first. + """ + raise NotImplementedError() + + def get_authenticator_assurance_level( + self, + ) -> AuthMFAGetAuthenticatorAssuranceLevelResponse: + """ + Returns the Authenticator Assurance Level (AAL) for the active session. + + - `aal1` (or `null`) means that the user's identity has been verified only + with a conventional login (email+password, OTP, magic link, social login, + etc.). + - `aal2` means that the user's identity has been verified both with a + conventional login and at least one MFA factor. + + Although this method returns a promise, it's fairly quick (microseconds) + and rarely uses the network. You can use this to check whether the current + user needs to be shown a screen to verify their MFA factors. + """ + raise NotImplementedError() diff --git a/gotrue/types.py b/gotrue/types.py index 59d785bd..3ebd968f 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -26,6 +26,8 @@ "workos", ] +AuthChangeEventMFA = Literal["MFA_CHALLENGE_VERIFIED"] + AuthChangeEvent = Literal[ "PASSWORD_RECOVERY", "SIGNED_IN", @@ -33,12 +35,32 @@ "TOKEN_REFRESHED", "USER_UPDATED", "USER_DELETED", + AuthChangeEventMFA, ] -class Options(TypedDict, total=False): - redirect_to: str - data: Any +class AMREntry(BaseModel): + """ + An authentication methord reference (AMR) entry. + + An entry designates what method was used by the user to verify their + identity and at what time. + """ + + method: Union[Literal["password", "otp", "oauth", "mfa/totp"], str] + """ + Authentication method name. + """ + timestamp: int + """ + Timestamp when the method was successfully used. Represents number of + seconds since 1st January 1970 (UNIX epoch) in UTC. + """ + + +class Options(TypedDict): + redirect_to: NotRequired[str] + data: NotRequired[Any] class AuthResponse(BaseModel): @@ -102,6 +124,32 @@ class UserIdentity(BaseModel): updated_at: Union[datetime, None] = None +class Factor(BaseModel): + """ + A MFA factor. + """ + + id: str + """ + ID of the factor. + """ + friendly_name: Union[str, None] = None + """ + Friendly name of the factor, useful to disambiguate between multiple factors. + """ + factor_type: Union[Literal["totp"], str] + """ + Type of factor. Only `totp` supported with this version but may change in + future versions. + """ + status: Literal["verified", "unverified"] + """ + Factor's status. + """ + created_at: datetime + updated_at: datetime + + class User(BaseModel): id: str app_metadata: Dict[str, Any] @@ -123,21 +171,22 @@ class User(BaseModel): role: Union[str, None] = None updated_at: Union[datetime, None] = None identities: Union[List[UserIdentity], None] = None + factors: Union[List[Factor], None] = None -class UserAttributes(TypedDict, total=False): - email: str - phone: str - password: str - data: Any +class UserAttributes(TypedDict): + email: NotRequired[str] + phone: NotRequired[str] + password: NotRequired[str] + data: NotRequired[Any] -class AdminUserAttributes(UserAttributes, TypedDict, total=False): - user_metadata: Any - app_metadata: Any - email_confirm: bool - phone_confirm: bool - ban_duration: Union[str, Literal["none"]] +class AdminUserAttributes(UserAttributes, TypedDict): + user_metadata: NotRequired[Any] + app_metadata: NotRequired[Any] + email_confirm: NotRequired[bool] + phone_confirm: NotRequired[bool] + ban_duration: NotRequired[Union[str, Literal["none"]]] class Subscription(BaseModel): @@ -155,10 +204,16 @@ class Subscription(BaseModel): """ -class SignUpWithEmailAndPasswordCredentialsOptions(TypedDict, total=False): - email_redirect_to: str - data: Any - captcha_token: str +class UpdatableFactorAttributes(TypedDict): + friendly_name: str + + +class SignUpWithEmailAndPasswordCredentialsOptions( + TypedDict, +): + email_redirect_to: NotRequired[str] + data: NotRequired[Any] + captcha_token: NotRequired[str] class SignUpWithEmailAndPasswordCredentials(TypedDict): @@ -167,9 +222,9 @@ class SignUpWithEmailAndPasswordCredentials(TypedDict): options: NotRequired[SignUpWithEmailAndPasswordCredentialsOptions] -class SignUpWithPhoneAndPasswordCredentialsOptions(TypedDict, total=False): - data: Any - captcha_token: str +class SignUpWithPhoneAndPasswordCredentialsOptions(TypedDict): + data: NotRequired[Any] + captcha_token: NotRequired[str] class SignUpWithPhoneAndPasswordCredentials(TypedDict): @@ -184,8 +239,8 @@ class SignUpWithPhoneAndPasswordCredentials(TypedDict): ] -class SignInWithPasswordCredentialsOptions(TypedDict, total=False): - captcha_token: str +class SignInWithPasswordCredentialsOptions(TypedDict): + captcha_token: NotRequired[str] class SignInWithEmailAndPasswordCredentials(TypedDict): @@ -206,11 +261,11 @@ class SignInWithPhoneAndPasswordCredentials(TypedDict): ] -class SignInWithEmailAndPasswordlessCredentialsOptions(TypedDict, total=False): - email_redirect_to: str - should_create_user: bool - data: Any - captcha_token: str +class SignInWithEmailAndPasswordlessCredentialsOptions(TypedDict): + email_redirect_to: NotRequired[str] + should_create_user: NotRequired[bool] + data: NotRequired[Any] + captcha_token: NotRequired[str] class SignInWithEmailAndPasswordlessCredentials(TypedDict): @@ -218,10 +273,10 @@ class SignInWithEmailAndPasswordlessCredentials(TypedDict): options: NotRequired[SignInWithEmailAndPasswordlessCredentialsOptions] -class SignInWithPhoneAndPasswordlessCredentialsOptions(TypedDict, total=False): - should_create_user: bool - data: Any - captcha_token: str +class SignInWithPhoneAndPasswordlessCredentialsOptions(TypedDict): + should_create_user: NotRequired[bool] + data: NotRequired[Any] + captcha_token: NotRequired[str] class SignInWithPhoneAndPasswordlessCredentials(TypedDict): @@ -235,10 +290,10 @@ class SignInWithPhoneAndPasswordlessCredentials(TypedDict): ] -class SignInWithOAuthCredentialsOptions(TypedDict, total=False): - redirect_to: str - scopes: str - query_params: Dict[str, str] +class SignInWithOAuthCredentialsOptions(TypedDict): + redirect_to: NotRequired[str] + scopes: NotRequired[str] + query_params: NotRequired[Dict[str, str]] class SignInWithOAuthCredentials(TypedDict): @@ -246,9 +301,9 @@ class SignInWithOAuthCredentials(TypedDict): options: NotRequired[SignInWithOAuthCredentialsOptions] -class VerifyOtpParamsOptions(TypedDict, total=False): - redirect_to: str - captcha_token: str +class VerifyOtpParamsOptions(TypedDict): + redirect_to: NotRequired[str] + captcha_token: NotRequired[str] class VerifyEmailOtpParams(TypedDict): @@ -280,16 +335,12 @@ class VerifyMobileOtpParams(TypedDict): ] -class GenerateLinkParamsOptions(TypedDict, total=False): - redirect_to: str +class GenerateLinkParamsOptions(TypedDict): + redirect_to: NotRequired[str] -class GenerateLinkParamsWithDataOptions( - GenerateLinkParamsOptions, - TypedDict, - total=False, -): - data: Any +class GenerateLinkParamsWithDataOptions(GenerateLinkParamsOptions, TypedDict): + data: NotRequired[Any] class GenerateSignupLinkParams(TypedDict): @@ -335,6 +386,181 @@ class GenerateEmailChangeLinkParams(TypedDict): ] +class MFAEnrollParams(TypedDict): + factor_type: Literal["totp"] + issuer: NotRequired[str] + friendly_name: NotRequired[str] + + +class MFAUnenrollParams(TypedDict): + factor_id: str + """ + ID of the factor being unenrolled. + """ + + +class MFAVerifyParams(TypedDict): + factor_id: str + """ + ID of the factor being verified. + """ + challenge_id: str + """ + ID of the challenge being verified. + """ + code: str + """ + Verification code provided by the user. + """ + + +class MFAChallengeParams(TypedDict): + factor_id: str + """ + ID of the factor to be challenged. + """ + + +class AuthMFAVerifyResponse(BaseModel): + access_token: str + """ + New access token (JWT) after successful verification. + """ + token_type: str + """ + Type of token, typically `Bearer`. + """ + expires_in: int + """ + Number of seconds in which the access token will expire. + """ + refresh_token: str + """ + Refresh token you can use to obtain new access tokens when expired. + """ + user: User + """ + Updated user profile. + """ + + +class AuthMFAEnrollResponseTotp(BaseModel): + qr_code: str + """ + Contains a QR code encoding the authenticator URI. You can + convert it to a URL by prepending `data:image/svg+xml;utf-8,` to + the value. Avoid logging this value to the console. + """ + secret: str + """ + The TOTP secret (also encoded in the QR code). Show this secret + in a password-style field to the user, in case they are unable to + scan the QR code. Avoid logging this value to the console. + """ + uri: str + """ + The authenticator URI encoded within the QR code, should you need + to use it. Avoid loggin this value to the console. + """ + + +class AuthMFAEnrollResponse(BaseModel): + id: str + """ + ID of the factor that was just enrolled (in an unverified state). + """ + type: Literal["totp"] + """ + Type of MFA factor. Only `totp` supported for now. + """ + totp: AuthMFAEnrollResponseTotp + """ + TOTP enrollment information. + """ + + +class AuthMFAUnenrollResponse(BaseModel): + id: str + """ + ID of the factor that was successfully unenrolled. + """ + + +class AuthMFAChallengeResponse(BaseModel): + id: str + """ + ID of the newly created challenge. + """ + expires_at: int + """ + Timestamp in UNIX seconds when this challenge will no longer be usable. + """ + + +class AuthMFAListFactorsResponse(BaseModel): + all: List[Factor] + """ + All available factors (verified and unverified). + """ + totp: List[Factor] + """ + Only verified TOTP factors. (A subset of `all`.) + """ + + +AuthenticatorAssuranceLevels = Literal["aal1", "aal2"] + + +class AuthMFAGetAuthenticatorAssuranceLevelResponse(BaseModel): + current_level: Union[AuthenticatorAssuranceLevels, None] = None + """ + Current AAL level of the session. + """ + next_level: Union[AuthenticatorAssuranceLevels, None] = None + """ + Next possible AAL level for the session. If the next level is higher + than the current one, the user should go through MFA. + """ + current_authentication_methods: List[AMREntry] + """ + A list of all authentication methods attached to this session. Use + the information here to detect the last time a user verified a + factor, for example if implementing a step-up scenario. + """ + + +class AuthMFAAdminDeleteFactorResponse(BaseModel): + id: str + """ + ID of the factor that was successfully deleted. + """ + + +class AuthMFAAdminDeleteFactorParams(TypedDict): + id: str + """ + ID of the MFA factor to delete. + """ + user_id: str + """ + ID of the user whose factor is being deleted. + """ + + +class AuthMFAAdminListFactorsResponse(BaseModel): + factors: List[Factor] + """ + All factors attached to the user. + """ + + +class AuthMFAAdminListFactorsParams(TypedDict): + user_id: str + """ + ID of the user for which to list all MFA factors. + """ + + class GenerateLinkProperties(BaseModel): """ The properties related to the email link generated. @@ -369,3 +595,9 @@ class GenerateLinkProperties(BaseModel): class GenerateLinkResponse(BaseModel): properties: GenerateLinkProperties user: User + + +class DecodedJWTDict(TypedDict): + exp: NotRequired[int] + aal: NotRequired[Union[AuthenticatorAssuranceLevels, None]] + amr: NotRequired[Union[List[AMREntry], None]] From d5a092095145459c4c67321915cabfb723170909 Mon Sep 17 00:00:00 2001 From: "sourcery-ai[bot]" <58596630+sourcery-ai[bot]@users.noreply.github.com> Date: Fri, 21 Oct 2022 00:28:00 -0400 Subject: [PATCH 17/35] 'Refactored by Sourcery' (#176) Co-authored-by: Sourcery AI <> --- gotrue/_async/gotrue_client.py | 4 +--- gotrue/_sync/gotrue_client.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index 75efca59..350bcc19 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -570,12 +570,10 @@ async def _get_authenticator_assurance_level( current_level: Union[AuthenticatorAssuranceLevels, None] = None if payload.get("aal"): current_level = payload.get("aal") - next_level = current_level verified_factors = [ f for f in session.user.factors or [] if f.status == "verified" ] - if verified_factors: - next_level = "aal2" + next_level = "aal2" if verified_factors else current_level current_authentication_methods = payload.get("amr") or [] return AuthMFAGetAuthenticatorAssuranceLevelResponse( current_level=current_level, diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index 471cfcbf..bb4d54de 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -570,12 +570,10 @@ def _get_authenticator_assurance_level( current_level: Union[AuthenticatorAssuranceLevels, None] = None if payload.get("aal"): current_level = payload.get("aal") - next_level = current_level verified_factors = [ f for f in session.user.factors or [] if f.status == "verified" ] - if verified_factors: - next_level = "aal2" + next_level = "aal2" if verified_factors else current_level current_authentication_methods = payload.get("amr") or [] return AuthMFAGetAuthenticatorAssuranceLevelResponse( current_level=current_level, From b7142060c99d06270bdcde9ecc2f823c33cf11b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 29 Oct 2022 04:59:20 +0000 Subject: [PATCH 18/35] feat: mfa challenge and verify and refresh session --- gotrue/_async/gotrue_client.py | 43 ++++++++++++++++++++++++++++++++- gotrue/_async/gotrue_mfa_api.py | 12 +++++++++ gotrue/_sync/gotrue_client.py | 43 ++++++++++++++++++++++++++++++++- gotrue/_sync/gotrue_mfa_api.py | 12 +++++++++ gotrue/types.py | 12 +++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index 75efca59..290cd63d 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -34,6 +34,7 @@ AuthMFAVerifyResponse, AuthResponse, DecodedJWTDict, + MFAChallengeAndVerifyParams, MFAChallengeParams, MFAEnrollParams, MFAUnenrollParams, @@ -91,6 +92,7 @@ def __init__( ) self.mfa = AsyncGoTrueMFAAPI() self.mfa.challenge = self._challenge + self.mfa.challenge_and_verify = self._challenge_and_verify self.mfa.enroll = self._enroll self.mfa.get_authenticator_assurance_level = ( self._get_authenticator_assurance_level @@ -137,7 +139,7 @@ async def sign_up( password = credentials.get("password") options = credentials.get("options", {}) redirect_to = options.get("redirect_to") - data = options.get("data") + data = options.get("data") or {} captcha_token = options.get("captcha_token") if email: response = await self._request( @@ -189,6 +191,7 @@ async def sign_in_with_password( phone = credentials.get("phone") password = credentials.get("password") options = credentials.get("options", {}) + data = options.get("data") or {} captcha_token = options.get("captcha_token") if email: response = await self._request( @@ -197,6 +200,7 @@ async def sign_in_with_password( body={ "email": email, "password": password, + "data": data, "gotrue_meta_security": { "captcha_token": captcha_token, }, @@ -213,6 +217,7 @@ async def sign_in_with_password( body={ "phone": phone, "password": password, + "data": data, "gotrue_meta_security": { "captcha_token": captcha_token, }, @@ -436,6 +441,25 @@ async def set_session(self, access_token: str, refresh_token: str) -> AuthRespon self._notify_all_subscribers("TOKEN_REFRESHED", session) return AuthResponse(session=session, user=response.user) + async def refresh_session( + self, refresh_token: Union[str, None] = None + ) -> AuthResponse: + """ + Returns a new session, regardless of expiry status. + + Takes in an optional current session. If not passed in, then refreshSession() + will attempt to retrieve it from getSession(). If the current session's + refresh token is invalid, an error will be thrown. + """ + if not refresh_token: + session = await self.get_session() + if session: + refresh_token = session.refresh_token + if not refresh_token: + raise AuthSessionMissingError() + session = await self._call_refresh_token(refresh_token) + return AuthResponse(session=session, user=session.user) + async def sign_out(self) -> None: """ Inside a browser context, `sign_out` will remove the logged in user from the @@ -523,6 +547,23 @@ async def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespon xform=AuthMFAChallengeResponse.parse_obj, ) + async def _challenge_and_verify( + self, + params: MFAChallengeAndVerifyParams, + ) -> AuthMFAVerifyResponse: + response = await self._challenge( + { + "factor_id": params.get("factor_id"), + } + ) + return await self._verify( + { + "factor_id": params.get("factor_id"), + "challenge_id": response.id, + "code": params.get("code"), + } + ) + async def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: session = await self.get_session() if not session: diff --git a/gotrue/_async/gotrue_mfa_api.py b/gotrue/_async/gotrue_mfa_api.py index feb6e0be..815f0195 100644 --- a/gotrue/_async/gotrue_mfa_api.py +++ b/gotrue/_async/gotrue_mfa_api.py @@ -5,6 +5,7 @@ AuthMFAListFactorsResponse, AuthMFAUnenrollResponse, AuthMFAVerifyResponse, + MFAChallengeAndVerifyParams, MFAChallengeParams, MFAEnrollParams, MFAUnenrollParams, @@ -38,6 +39,17 @@ async def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeRespons """ raise NotImplementedError() + async def challenge_and_verify( + self, + params: MFAChallengeAndVerifyParams, + ) -> AuthMFAVerifyResponse: + """ + Helper method which creates a challenge and immediately uses the given code + to verify against it thereafter. The verification code is provided by the + user by entering a code seen in their authenticator app. + """ + raise NotImplementedError() + async def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: """ Verifies a verification code against a challenge. The verification code is diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index 471cfcbf..985cdc77 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -34,6 +34,7 @@ AuthMFAVerifyResponse, AuthResponse, DecodedJWTDict, + MFAChallengeAndVerifyParams, MFAChallengeParams, MFAEnrollParams, MFAUnenrollParams, @@ -91,6 +92,7 @@ def __init__( ) self.mfa = SyncGoTrueMFAAPI() self.mfa.challenge = self._challenge + self.mfa.challenge_and_verify = self._challenge_and_verify self.mfa.enroll = self._enroll self.mfa.get_authenticator_assurance_level = ( self._get_authenticator_assurance_level @@ -137,7 +139,7 @@ def sign_up( password = credentials.get("password") options = credentials.get("options", {}) redirect_to = options.get("redirect_to") - data = options.get("data") + data = options.get("data") or {} captcha_token = options.get("captcha_token") if email: response = self._request( @@ -189,6 +191,7 @@ def sign_in_with_password( phone = credentials.get("phone") password = credentials.get("password") options = credentials.get("options", {}) + data = options.get("data") or {} captcha_token = options.get("captcha_token") if email: response = self._request( @@ -197,6 +200,7 @@ def sign_in_with_password( body={ "email": email, "password": password, + "data": data, "gotrue_meta_security": { "captcha_token": captcha_token, }, @@ -213,6 +217,7 @@ def sign_in_with_password( body={ "phone": phone, "password": password, + "data": data, "gotrue_meta_security": { "captcha_token": captcha_token, }, @@ -436,6 +441,25 @@ def set_session(self, access_token: str, refresh_token: str) -> AuthResponse: self._notify_all_subscribers("TOKEN_REFRESHED", session) return AuthResponse(session=session, user=response.user) + def refresh_session( + self, refresh_token: Union[str, None] = None + ) -> AuthResponse: + """ + Returns a new session, regardless of expiry status. + + Takes in an optional current session. If not passed in, then refreshSession() + will attempt to retrieve it from getSession(). If the current session's + refresh token is invalid, an error will be thrown. + """ + if not refresh_token: + session = self.get_session() + if session: + refresh_token = session.refresh_token + if not refresh_token: + raise AuthSessionMissingError() + session = self._call_refresh_token(refresh_token) + return AuthResponse(session=session, user=session.user) + def sign_out(self) -> None: """ Inside a browser context, `sign_out` will remove the logged in user from the @@ -523,6 +547,23 @@ def _challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: xform=AuthMFAChallengeResponse.parse_obj, ) + def _challenge_and_verify( + self, + params: MFAChallengeAndVerifyParams, + ) -> AuthMFAVerifyResponse: + response = self._challenge( + { + "factor_id": params.get("factor_id"), + } + ) + return self._verify( + { + "factor_id": params.get("factor_id"), + "challenge_id": response.id, + "code": params.get("code"), + } + ) + def _verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: session = self.get_session() if not session: diff --git a/gotrue/_sync/gotrue_mfa_api.py b/gotrue/_sync/gotrue_mfa_api.py index 78b3df99..0e936f10 100644 --- a/gotrue/_sync/gotrue_mfa_api.py +++ b/gotrue/_sync/gotrue_mfa_api.py @@ -5,6 +5,7 @@ AuthMFAListFactorsResponse, AuthMFAUnenrollResponse, AuthMFAVerifyResponse, + MFAChallengeAndVerifyParams, MFAChallengeParams, MFAEnrollParams, MFAUnenrollParams, @@ -38,6 +39,17 @@ def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: """ raise NotImplementedError() + def challenge_and_verify( + self, + params: MFAChallengeAndVerifyParams, + ) -> AuthMFAVerifyResponse: + """ + Helper method which creates a challenge and immediately uses the given code + to verify against it thereafter. The verification code is provided by the + user by entering a code seen in their authenticator app. + """ + raise NotImplementedError() + def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: """ Verifies a verification code against a challenge. The verification code is diff --git a/gotrue/types.py b/gotrue/types.py index 3ebd968f..1cb0c0cc 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -240,6 +240,7 @@ class SignUpWithPhoneAndPasswordCredentials(TypedDict): class SignInWithPasswordCredentialsOptions(TypedDict): + data: NotRequired[Any] captcha_token: NotRequired[str] @@ -421,6 +422,17 @@ class MFAChallengeParams(TypedDict): """ +class MFAChallengeAndVerifyParams(TypedDict): + factor_id: str + """ + ID of the factor being verified. + """ + code: str + """ + Verification code provided by the user. + """ + + class AuthMFAVerifyResponse(BaseModel): access_token: str """ From 6fbd3de4247f38ddd4af76422729658a8f0fb2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 29 Oct 2022 21:06:07 +0000 Subject: [PATCH 19/35] chore: implement clients and utils for testing --- gotrue/constants.py | 4 +- tests/_async/clients.py | 105 ++++ tests/_async/old/.gitignore | 1 + .../test_api_with_auto_confirm_disabled.py | 98 ---- .../test_api_with_auto_confirm_enabled.py | 62 --- .../test_client_with_auto_confirm_disabled.py | 117 ----- .../test_client_with_auto_confirm_enabled.py | 463 ------------------ .../test_client_with_sign_ups_disabled.py | 125 ----- tests/_async/test_provider.py | 61 --- tests/_async/test_subscriptions.py | 35 -- tests/_async/test_utils.py | 33 ++ tests/_async/utils.py | 81 +++ tests/_sync/__init__.py | 0 .../test_api_with_auto_confirm_disabled.py | 98 ---- .../test_api_with_auto_confirm_enabled.py | 62 --- .../test_client_with_auto_confirm_disabled.py | 117 ----- .../test_client_with_auto_confirm_enabled.py | 457 ----------------- .../test_client_with_sign_ups_disabled.py | 125 ----- tests/_sync/test_provider.py | 61 --- tests/_sync/test_subscriptions.py | 35 -- 20 files changed, 223 insertions(+), 1917 deletions(-) create mode 100644 tests/_async/clients.py create mode 100644 tests/_async/old/.gitignore delete mode 100644 tests/_async/test_api_with_auto_confirm_disabled.py delete mode 100644 tests/_async/test_api_with_auto_confirm_enabled.py delete mode 100644 tests/_async/test_client_with_auto_confirm_disabled.py delete mode 100644 tests/_async/test_client_with_auto_confirm_enabled.py delete mode 100644 tests/_async/test_client_with_sign_ups_disabled.py delete mode 100644 tests/_async/test_provider.py delete mode 100644 tests/_async/test_subscriptions.py create mode 100644 tests/_async/test_utils.py create mode 100644 tests/_async/utils.py delete mode 100644 tests/_sync/__init__.py delete mode 100644 tests/_sync/test_api_with_auto_confirm_disabled.py delete mode 100644 tests/_sync/test_api_with_auto_confirm_enabled.py delete mode 100644 tests/_sync/test_client_with_auto_confirm_disabled.py delete mode 100644 tests/_sync/test_client_with_auto_confirm_enabled.py delete mode 100644 tests/_sync/test_client_with_sign_ups_disabled.py delete mode 100644 tests/_sync/test_provider.py delete mode 100644 tests/_sync/test_subscriptions.py diff --git a/gotrue/constants.py b/gotrue/constants.py index ad001f27..904deb1d 100644 --- a/gotrue/constants.py +++ b/gotrue/constants.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import Dict + from . import __version__ GOTRUE_URL = "http://localhost:9999" -DEFAULT_HEADERS = { +DEFAULT_HEADERS: Dict[str, str] = { "X-Client-Info": f"gotrue-py/{__version__}", } EXPIRY_MARGIN = 10 # seconds diff --git a/tests/_async/clients.py b/tests/_async/clients.py new file mode 100644 index 00000000..e3001d01 --- /dev/null +++ b/tests/_async/clients.py @@ -0,0 +1,105 @@ +from gotrue import AsyncGoTrueAdminAPI, AsyncGoTrueClient +from jwt import encode + +SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 +SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 +SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 + +GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF = ( + f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT}" +) +GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON = ( + f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT}" +) +GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF = ( + f"http://localhost:{SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT}" +) + +GOTRUE_JWT_SECRET = "37c304f8-51aa-419a-a1af-06154e63707a" + +AUTH_ADMIN_JWT = encode( + { + "sub": "1234567890", + "role": "supabase_admin", + }, + GOTRUE_JWT_SECRET, +) + +auth_client = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, +) + +auth_client_with_session = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=False, +) + +auth_subscription_client = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, +) + + +client_api_auto_confirm_enabled_client = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, +) + +client_api_auto_confirm_off_signups_enabled_client = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, +) + +client_api_auto_confirm_disabled_client = AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, +) + +auth_admin_api_auto_confirm_enabled_client = AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, +) + +auth_admin_api_auto_confirm_disabled_client = AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, +) + +SERVICE_ROLE_JWT = encode( + { + "role": "service_role", + }, + GOTRUE_JWT_SECRET, +) + +service_role_api_client = AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, +) + +service_role_api_client_with_sms = AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, +) + +service_role_api_client_no_sms = AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, +) diff --git a/tests/_async/old/.gitignore b/tests/_async/old/.gitignore new file mode 100644 index 00000000..8e5bbf04 --- /dev/null +++ b/tests/_async/old/.gitignore @@ -0,0 +1 @@ +*.py \ No newline at end of file diff --git a/tests/_async/test_api_with_auto_confirm_disabled.py b/tests/_async/test_api_with_auto_confirm_disabled.py deleted file mode 100644 index c780d5bf..00000000 --- a/tests/_async/test_api_with_auto_confirm_disabled.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import AsyncIterable - -import pytest -from faker import Faker - -from ...gotrue import AsyncGoTrueAPI -from ...gotrue.constants import COOKIE_OPTIONS -from ...gotrue.types import CookieOptions, GenerateLinkType, User - -GOTRUE_URL = "http://localhost:9999" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 - - -@pytest.fixture(name="api") -async def create_api() -> AsyncIterable[AsyncGoTrueAPI]: - async with AsyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -fake = Faker() - -email = f"api_ac_disabled_{fake.email().lower()}" -password = fake.password() - - -async def test_sign_up_with_email_and_password(api: AsyncGoTrueAPI): - try: - response = await api.sign_up_with_email( - email=email, - password=password, - redirect_to="http://localhost:9999/welcome", - data={"status": "alpha"}, - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -email2 = f"api_generate_link_signup_{fake.email().lower()}" -password2 = fake.password() - - -async def test_generate_sign_up_link(api: AsyncGoTrueAPI): - try: - response = await api.generate_link( - type=GenerateLinkType.signup, - email=email2, - password=password2, - redirect_to="http://localhost:9999/welcome", - data={"status": "alpha"}, - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -email3 = f"api_generate_link_signup_{fake.email().lower()}" - - -async def test_generate_magic_link(api: AsyncGoTrueAPI): - try: - response = await api.generate_link( - type=GenerateLinkType.magiclink, - email=email3, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -async def test_generate_invite_link(api: AsyncGoTrueAPI): - try: - response = await api.generate_link( - type=GenerateLinkType.invite, - email=email3, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -async def test_generate_recovery_link(api: AsyncGoTrueAPI): - try: - response = await api.generate_link( - type=GenerateLinkType.recovery, - email=email, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_api_with_auto_confirm_enabled.py b/tests/_async/test_api_with_auto_confirm_enabled.py deleted file mode 100644 index cf7e6faa..00000000 --- a/tests/_async/test_api_with_auto_confirm_enabled.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import AsyncIterable, Optional - -import pytest -from faker import Faker - -from ...gotrue import AsyncGoTrueAPI -from ...gotrue.constants import COOKIE_OPTIONS -from ...gotrue.types import CookieOptions, Session, User - -GOTRUE_URL = "http://localhost:9998" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjQyMjMyNzUwfQ.TUR8Zu05TtNR25L42soA2trZpc4oBR8-9Pv5r5bvls8" # noqa: E501 - - -@pytest.fixture(name="api") -async def create_api() -> AsyncIterable[AsyncGoTrueAPI]: - async with AsyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -fake = Faker() - -email = f"api_ac_enabled_{fake.email().lower()}" -password = fake.password() -valid_session: Optional[Session] = None - - -async def test_sign_up_with_email(api: AsyncGoTrueAPI): - global valid_session - try: - response = await api.sign_up_with_email( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, Session) - valid_session = response - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email.__name__]) -async def test_get_user(api: AsyncGoTrueAPI): - try: - jwt = valid_session.access_token if valid_session else "" - response = await api.get_user(jwt=jwt) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user.__name__]) -async def test_delete_user(api: AsyncGoTrueAPI): - try: - jwt = valid_session.access_token if valid_session else "" - user = await api.get_user(jwt=jwt) - await api.delete_user(uid=str(user.id), jwt=TOKEN) - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_client_with_auto_confirm_disabled.py b/tests/_async/test_client_with_auto_confirm_disabled.py deleted file mode 100644 index 5225a6e9..00000000 --- a/tests/_async/test_client_with_auto_confirm_disabled.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing import AsyncIterable - -import pytest -from faker import Faker - -from ...gotrue import AsyncGoTrueClient -from ...gotrue.errors import APIError -from ...gotrue.types import User - -GOTRUE_URL = "http://localhost:9999" -TEST_TWILIO = False - - -@pytest.fixture(name="client") -async def create_client() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = fake.email().lower() -password = fake.password() -phone = fake.phone_number() # set test number here - - -async def test_sign_up_with_email_and_password(client: AsyncGoTrueClient): - try: - response = await client.sign_up( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, User) - assert not response.email_confirmed_at - assert not response.last_sign_in_at - assert response.email == email - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -async def test_sign_up_with_the_same_user_twice_should_throw_an_error( - client: AsyncGoTrueClient, -): - expected_error_message = "For security purposes, you can only request this after" - try: - await client.sign_up( - email=email, - password=password, - ) - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -async def test_sign_in(client: AsyncGoTrueClient): - expected_error_message = "Email not confirmed" - try: - await client.sign_in( - email=email, - password=password, - ) - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -async def test_sign_in_with_the_wrong_password(client: AsyncGoTrueClient): - expected_error_message = "Invalid login credentials" - try: - await client.sign_in(email=email, password=f"{password}2") - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.skipif(not TEST_TWILIO, reason="Twilio is not available") -async def test_sign_up_with_phone_and_password(client: AsyncGoTrueClient): - try: - response = await client.sign_up( - phone=phone, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, User) - assert not response.phone_confirmed_at - assert not response.email_confirmed_at - assert not response.last_sign_in_at - assert response.phone == phone - except Exception as e: - assert False, str(e) - - -@pytest.mark.skipif(not TEST_TWILIO, reason="Twilio is not available") -@pytest.mark.depends(on=[test_sign_up_with_phone_and_password.__name__]) -async def test_verify_mobile_otp_errors_on_bad_token(client: AsyncGoTrueClient): - expected_error_message = "Otp has expired or is invalid" - try: - await client.verify_otp(phone=phone, token="123456") - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_client_with_auto_confirm_enabled.py b/tests/_async/test_client_with_auto_confirm_enabled.py deleted file mode 100644 index e8dc566d..00000000 --- a/tests/_async/test_client_with_auto_confirm_enabled.py +++ /dev/null @@ -1,463 +0,0 @@ -from typing import AsyncIterable, Optional - -import pytest -from faker import Faker - -from ...gotrue import AsyncGoTrueClient -from ...gotrue.errors import APIError -from ...gotrue.types import Session, User, UserAttributes - -GOTRUE_URL = "http://localhost:9998" -TEST_TWILIO = False - - -@pytest.fixture(name="client") -async def create_client() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=True, - ) as client: - yield client - - -@pytest.fixture(name="client_with_session") -async def create_client_with_session() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -@pytest.fixture(name="new_client") -async def create_new_client() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = f"client_ac_enabled_{fake.email().lower()}" -set_session_email = f"client_ac_session_{fake.email().lower()}" -refresh_token_email = f"client_refresh_token_signin_{fake.email().lower()}" -password = fake.password() -access_token: Optional[str] = None - - -async def test_sign_up(client: AsyncGoTrueClient): - try: - response = await client.sign_up( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, Session) - global access_token - access_token = response.access_token - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - assert response.user - assert response.user.id - assert response.user.email == email - assert response.user.email_confirmed_at - assert response.user.last_sign_in_at - assert response.user.created_at - assert response.user.updated_at - assert response.user.app_metadata - assert response.user.app_metadata.get("provider") == "email" - assert response.user.user_metadata - assert response.user.user_metadata.get("status") == "alpha" - except Exception as e: - assert False, str(e) - - -async def test_set_session_should_return_no_error( - client_with_session: AsyncGoTrueClient, -): - try: - response = await client_with_session.sign_up( - email=set_session_email, - password=password, - ) - assert isinstance(response, Session) - assert response.refresh_token - await client_with_session.set_session(refresh_token=response.refresh_token) - data = {"hello": "world"} - response = await client_with_session.update( - attributes=UserAttributes(data=data) - ) - assert response.user_metadata == data - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -async def test_sign_up_the_same_user_twice_should_throw_an_error( - client: AsyncGoTrueClient, -): - expected_error_message = "User already registered" - try: - await client.sign_up( - email=email, - password=password, - ) - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -async def test_set_auth_should_set_the_auth_headers_on_a_new_client( - new_client: AsyncGoTrueClient, -): - try: - assert access_token - await new_client.set_auth(access_token=access_token) - assert new_client.current_session - assert new_client.current_session.access_token == access_token - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends( - on=[test_set_auth_should_set_the_auth_headers_on_a_new_client.__name__] -) -async def test_set_auth_should_set_the_auth_headers_on_a_new_client_and_recover( - new_client: AsyncGoTrueClient, -): - try: - assert access_token - await new_client.init_recover() - await new_client.set_auth(access_token=access_token) - assert new_client.current_session - assert new_client.current_session.access_token == access_token - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -async def test_sign_in(client: AsyncGoTrueClient): - try: - response = await client.sign_in(email=email, password=password) - assert isinstance(response, Session) - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - assert response.user - assert response.user.id - assert response.user.email == email - assert response.user.email_confirmed_at - assert response.user.last_sign_in_at - assert response.user.created_at - assert response.user.updated_at - assert response.user.app_metadata - assert response.user.app_metadata.get("provider") == "email" - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_refresh_token(client_with_session: AsyncGoTrueClient): - try: - response = await client_with_session.sign_up( - email=refresh_token_email, - password=password, - ) - assert isinstance(response, Session) - assert response.refresh_token - response2 = await client_with_session.sign_in( - refresh_token=response.refresh_token - ) - assert isinstance(response2, Session) - assert response2.access_token - assert response2.refresh_token - assert response2.expires_in - assert response2.expires_at - assert response2.user - assert response2.user.id - assert response2.user.email == refresh_token_email - assert response2.user.email_confirmed_at - assert response2.user.last_sign_in_at - assert response2.user.created_at - assert response2.user.updated_at - assert response2.user.app_metadata - assert response2.user.app_metadata.get("provider") == "email" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -async def test_get_user(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = client.user() - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.app_metadata - provider = response.app_metadata.get("provider") - assert provider == "email" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -async def test_get_session(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = client.session() - assert isinstance(response, Session) - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -async def test_update_user(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = await client.update( - attributes=UserAttributes(data={"hello": "world"}) - ) - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -async def test_update_user_dict(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = await client.update(attributes={"data": {"hello": "world"}}) - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_update_user.__name__]) -async def test_get_user_after_update(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = client.user() - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user_after_update.__name__]) -async def test_sign_out(client: AsyncGoTrueClient): - try: - await client.init_recover() - await client.sign_out() - response = client.session() - assert response is None - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_out.__name__]) -async def test_get_user_after_sign_out(client: AsyncGoTrueClient): - try: - await client.init_recover() - response = client.user() - assert not response - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_out.__name__]) -async def test_get_update_user_after_sign_out(client: AsyncGoTrueClient): - expected_error_message = "Not logged in." - try: - await client.init_recover() - await client.update(attributes=UserAttributes(data={"hello": "world"})) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user_after_sign_out.__name__]) -async def test_sign_in_with_the_wrong_password(client: AsyncGoTrueClient): - try: - await client.sign_in(email=email, password=f"{password}2") - assert False - except APIError: - pass - except Exception as e: - assert False, str(e) - - -async def test_sign_up_with_password_none(client: AsyncGoTrueClient): - expected_error_message = "Password must be defined, can't be None." - try: - await client.sign_up(email=email) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -async def test_sign_up_with_email_and_phone_none(client: AsyncGoTrueClient): - expected_error_message = "Email or phone must be defined, both can't be None." - try: - await client.sign_up(password=password) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_all_nones(client: AsyncGoTrueClient): - expected_error_message = ( - "Email, phone, refresh_token, or provider must be defined, " - "all can't be None." - ) - try: - await client.sign_in() - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_magic_link(client: AsyncGoTrueClient): - try: - response = await client.sign_in(email=email) - assert response is None - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -async def test_get_session_from_url(client: AsyncGoTrueClient): - try: - assert access_token - dummy_url = ( - "https://localhost" - f"?access_token={access_token}" - "&refresh_token=refresh_token" - "&token_type=bearer" - "&expires_in=3600" - "&type=recovery" - ) - response = await client.get_session_from_url(url=dummy_url, store_session=True) - assert isinstance(response, Session) - except Exception as e: - assert False, str(e) - - -async def test_get_session_from_url_errors(client: AsyncGoTrueClient): - try: - dummy_url = "https://localhost" - error_description = fake.email() - try: - await client.get_session_from_url( - url=f"{dummy_url}?error_description={error_description}" - ) - - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == error_description - try: - await client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No access_token detected." - dummy_url += "?access_token=access_token" - try: - await client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No refresh_token detected." - dummy_url += "&refresh_token=refresh_token" - try: - await client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No token_type detected." - dummy_url += "&token_type=bearer" - try: - await client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No expires_in detected." - dummy_url += "&expires_in=str" - try: - await client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "Invalid expires_in." - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_update_user_after_sign_out.__name__]) -async def test_refresh_session(client: AsyncGoTrueClient): - try: - response = await client.sign_in(email=email, password=password) - assert isinstance(response, Session) - assert response.refresh_token - response = await client.set_session(refresh_token=response.refresh_token) - assert isinstance(response, Session) - response = await client.refresh_session() - assert isinstance(response, Session) - await client.sign_out() - try: - await client.refresh_session() - assert False - except ValueError as e: - assert str(e) == "Not logged in." - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_client_with_sign_ups_disabled.py b/tests/_async/test_client_with_sign_ups_disabled.py deleted file mode 100644 index 1557bb13..00000000 --- a/tests/_async/test_client_with_sign_ups_disabled.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import AsyncIterable - -import pytest -from faker import Faker - -from ...gotrue import AsyncGoTrueAPI, AsyncGoTrueClient -from ...gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS -from ...gotrue.errors import APIError -from ...gotrue.types import CookieOptions, GenerateLinkType, User, UserAttributes - -GOTRUE_URL = "http://localhost:9997" -AUTH_ADMIN_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 - - -@pytest.fixture(name="auth_admin") -async def create_auth_admin() -> AsyncIterable[AsyncGoTrueAPI]: - async with AsyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {AUTH_ADMIN_TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -@pytest.fixture(name="client") -async def create_client() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = fake.email().lower() -password = fake.password() - - -async def test_sign_up(client: AsyncGoTrueClient): - expected_error_message = "Signups not allowed for this instance" - try: - await client.sign_up(email=email, password=password) - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -invited_user = fake.email().lower() - - -async def test_generate_link_should_be_able_to_generate_multiple_links( - auth_admin: AsyncGoTrueAPI, -): - try: - response = await auth_admin.generate_link( - type=GenerateLinkType.invite, - email=invited_user, - redirect_to="http://localhost:9997", - ) - assert isinstance(response, User) - assert response.email == invited_user - assert response.action_link - assert "http://localhost:9997/?token=" in response.action_link - assert response.app_metadata - assert response.app_metadata.get("provider") == "email" - providers = response.app_metadata.get("providers") - assert providers - assert isinstance(providers, list) - assert len(providers) == 1 - assert providers[0] == "email" - assert response.role == "" - assert response.user_metadata == {} - assert response.identities == [] - user = response - response = await auth_admin.generate_link( - type=GenerateLinkType.invite, - email=invited_user, - ) - assert isinstance(response, User) - assert response.email == invited_user - assert response.action_link - assert "http://localhost:9997/?token=" in response.action_link - assert response.app_metadata - assert response.app_metadata.get("provider") == "email" - providers = response.app_metadata.get("providers") - assert providers - assert isinstance(providers, list) - assert len(providers) == 1 - assert providers[0] == "email" - assert response.role == "" - assert response.user_metadata == {} - assert response.identities == [] - user_again = response - assert user.id == user_again.id - except Exception as e: - assert False, str(e) - - -email2 = fake.email().lower() - - -async def test_create_user(auth_admin: AsyncGoTrueAPI): - try: - attributes = UserAttributes(email=email2) - response = await auth_admin.create_user(attributes=attributes) - assert isinstance(response, User) - assert response.email == email2 - response = await auth_admin.list_users() - user = next((u for u in response if u.email == email2), None) - assert user - assert user.email == email2 - except Exception as e: - assert False, str(e) - - -def test_default_headers(client: AsyncGoTrueClient): - """Test client for existing default headers""" - default_key = "X-Client-Info" - assert default_key in DEFAULT_HEADERS - assert default_key in client.api.headers - assert client.api.headers[default_key] == DEFAULT_HEADERS[default_key] diff --git a/tests/_async/test_provider.py b/tests/_async/test_provider.py deleted file mode 100644 index ac7c9866..00000000 --- a/tests/_async/test_provider.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import AsyncIterable - -import pytest - -from ...gotrue import AsyncGoTrueClient -from ...gotrue.types import Provider - -GOTRUE_URL = "http://localhost:9999" - - -@pytest.fixture(name="client") -async def create_client() -> AsyncIterable[AsyncGoTrueClient]: - async with AsyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -async def test_sign_in_with_provider(client: AsyncGoTrueClient): - try: - response = await client.sign_in(provider=Provider.google) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_provider_can_append_a_redirect_url( - client: AsyncGoTrueClient, -): - try: - response = await client.sign_in( - provider=Provider.google, - redirect_to="https://localhost:9000/welcome", - ) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_provider_can_append_scopes(client: AsyncGoTrueClient): - try: - response = await client.sign_in(provider=Provider.google, scopes="repo") - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -async def test_sign_in_with_provider_can_append_multiple_options( - client: AsyncGoTrueClient, -): - try: - response = await client.sign_in( - provider=Provider.google, - redirect_to="https://localhost:9000/welcome", - scopes="repo", - ) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_subscriptions.py b/tests/_async/test_subscriptions.py deleted file mode 100644 index c4780294..00000000 --- a/tests/_async/test_subscriptions.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from ...gotrue import AsyncGoTrueClient -from ...gotrue.types import Subscription - -GOTRUE_URL = "http://localhost:9999" - - -@pytest.fixture(name="client") -async def create_client() -> AsyncGoTrueClient: - async with AsyncGoTrueClient(url=GOTRUE_URL) as client: - return client - - -@pytest.fixture(name="subscription") -def create_subscription(client: AsyncGoTrueClient): - return client.on_auth_state_change( - callback=lambda _, __: print("Auth state changed") - ) - - -def test_subscribe_a_listener(client: AsyncGoTrueClient, subscription: Subscription): - try: - assert len(client.state_change_emitters) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_subscribe_a_listener.__name__]) -def test_unsubscribe_a_listener(client: AsyncGoTrueClient, subscription: Subscription): - try: - subscription.unsubscribe() - assert not len(client.state_change_emitters) - except Exception as e: - assert False, str(e) diff --git a/tests/_async/test_utils.py b/tests/_async/test_utils.py new file mode 100644 index 00000000..480bbc15 --- /dev/null +++ b/tests/_async/test_utils.py @@ -0,0 +1,33 @@ +from time import time + +from .utils import create_new_user_with_email, mock_user_credentials, mock_user_metadata + + +def test_mock_user_credentials_has_email(): + credentials = mock_user_credentials() + assert credentials.get("email") + assert credentials.get("password") + + +def test_mock_user_credentials_has_phone(): + credentials = mock_user_credentials() + assert credentials.get("phone") + assert credentials.get("password") + + +async def test_create_new_user_with_email(): + email = f"user+{int(time())}@example.com" + user = await create_new_user_with_email(email=email) + assert user.email == email + + +def test_mock_user_metadata(): + user_metadata = mock_user_metadata() + assert user_metadata + assert user_metadata.get("profile_image") + + +def test_mock_app_metadata(): + app_metadata = mock_user_metadata() + assert app_metadata + assert app_metadata.get("roles") diff --git a/tests/_async/utils.py b/tests/_async/utils.py new file mode 100644 index 00000000..1a9c1ed7 --- /dev/null +++ b/tests/_async/utils.py @@ -0,0 +1,81 @@ +from random import random +from time import time +from typing import Union + +from faker import Faker +from jwt import encode +from typing_extensions import NotRequired, TypedDict + +from ...gotrue.types import User +from .clients import GOTRUE_JWT_SECRET, service_role_api_client + +fake = Faker() + + +def mock_access_token() -> str: + return encode( + { + "sub": "1234567890", + "role": "anon_key", + }, + GOTRUE_JWT_SECRET, + ) + + +class OptionalCredentials(TypedDict): + email: NotRequired[Union[str, None]] + phone: NotRequired[Union[str, None]] + password: NotRequired[Union[str, None]] + + +class Credentials(TypedDict): + email: str + phone: str + password: str + + +def mock_user_credentials( + options: OptionalCredentials = {}, +) -> Credentials: + rand_numbers = str(time()) + return { + "email": options.get("email") or fake.email(), + "phone": options.get("phone") or f"1{rand_numbers[-11:]}", + "password": options.get("password") or fake.password(), + } + + +def mock_verification_otp() -> str: + return str(int(100000 + random() * 900000)) + + +def mock_user_metadata(): + return { + "profile_image": fake.url(), + } + + +def mock_app_metadata(): + return { + "roles": ["editor", "publisher"], + } + + +async def create_new_user_with_email( + *, + email: Union[str, None] = None, + password: Union[str, None] = None, +) -> User: + credentials = mock_user_credentials( + { + "email": email, + "password": password, + } + ) + response = await service_role_api_client.create_user( + { + "email": credentials["email"], + "password": credentials["password"], + } + ) + return response.user diff --git a/tests/_sync/__init__.py b/tests/_sync/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/_sync/test_api_with_auto_confirm_disabled.py b/tests/_sync/test_api_with_auto_confirm_disabled.py deleted file mode 100644 index 2f4a07c5..00000000 --- a/tests/_sync/test_api_with_auto_confirm_disabled.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import Iterable - -import pytest -from faker import Faker - -from ...gotrue import SyncGoTrueAPI -from ...gotrue.constants import COOKIE_OPTIONS -from ...gotrue.types import CookieOptions, GenerateLinkType, User - -GOTRUE_URL = "http://localhost:9999" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 - - -@pytest.fixture(name="api") -def create_api() -> Iterable[SyncGoTrueAPI]: - with SyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -fake = Faker() - -email = f"api_ac_disabled_{fake.email().lower()}" -password = fake.password() - - -def test_sign_up_with_email_and_password(api: SyncGoTrueAPI): - try: - response = api.sign_up_with_email( - email=email, - password=password, - redirect_to="http://localhost:9999/welcome", - data={"status": "alpha"}, - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -email2 = f"api_generate_link_signup_{fake.email().lower()}" -password2 = fake.password() - - -def test_generate_sign_up_link(api: SyncGoTrueAPI): - try: - response = api.generate_link( - type=GenerateLinkType.signup, - email=email2, - password=password2, - redirect_to="http://localhost:9999/welcome", - data={"status": "alpha"}, - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -email3 = f"api_generate_link_signup_{fake.email().lower()}" - - -def test_generate_magic_link(api: SyncGoTrueAPI): - try: - response = api.generate_link( - type=GenerateLinkType.magiclink, - email=email3, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -def test_generate_invite_link(api: SyncGoTrueAPI): - try: - response = api.generate_link( - type=GenerateLinkType.invite, - email=email3, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -def test_generate_recovery_link(api: SyncGoTrueAPI): - try: - response = api.generate_link( - type=GenerateLinkType.recovery, - email=email, - redirect_to="http://localhost:9999/welcome", - ) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) diff --git a/tests/_sync/test_api_with_auto_confirm_enabled.py b/tests/_sync/test_api_with_auto_confirm_enabled.py deleted file mode 100644 index 67edaa81..00000000 --- a/tests/_sync/test_api_with_auto_confirm_enabled.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Iterable, Optional - -import pytest -from faker import Faker - -from ...gotrue import SyncGoTrueAPI -from ...gotrue.constants import COOKIE_OPTIONS -from ...gotrue.types import CookieOptions, Session, User - -GOTRUE_URL = "http://localhost:9998" -TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjQyMjMyNzUwfQ.TUR8Zu05TtNR25L42soA2trZpc4oBR8-9Pv5r5bvls8" # noqa: E501 - - -@pytest.fixture(name="api") -def create_api() -> Iterable[SyncGoTrueAPI]: - with SyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -fake = Faker() - -email = f"api_ac_enabled_{fake.email().lower()}" -password = fake.password() -valid_session: Optional[Session] = None - - -def test_sign_up_with_email(api: SyncGoTrueAPI): - global valid_session - try: - response = api.sign_up_with_email( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, Session) - valid_session = response - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email.__name__]) -def test_get_user(api: SyncGoTrueAPI): - try: - jwt = valid_session.access_token if valid_session else "" - response = api.get_user(jwt=jwt) - assert isinstance(response, User) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user.__name__]) -def test_delete_user(api: SyncGoTrueAPI): - try: - jwt = valid_session.access_token if valid_session else "" - user = api.get_user(jwt=jwt) - api.delete_user(uid=str(user.id), jwt=TOKEN) - except Exception as e: - assert False, str(e) diff --git a/tests/_sync/test_client_with_auto_confirm_disabled.py b/tests/_sync/test_client_with_auto_confirm_disabled.py deleted file mode 100644 index b4c2d325..00000000 --- a/tests/_sync/test_client_with_auto_confirm_disabled.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing import Iterable - -import pytest -from faker import Faker - -from ...gotrue import SyncGoTrueClient -from ...gotrue.errors import APIError -from ...gotrue.types import User - -GOTRUE_URL = "http://localhost:9999" -TEST_TWILIO = False - - -@pytest.fixture(name="client") -def create_client() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = fake.email().lower() -password = fake.password() -phone = fake.phone_number() # set test number here - - -def test_sign_up_with_email_and_password(client: SyncGoTrueClient): - try: - response = client.sign_up( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, User) - assert not response.email_confirmed_at - assert not response.last_sign_in_at - assert response.email == email - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -def test_sign_up_with_the_same_user_twice_should_throw_an_error( - client: SyncGoTrueClient, -): - expected_error_message = "For security purposes, you can only request this after" - try: - client.sign_up( - email=email, - password=password, - ) - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -def test_sign_in(client: SyncGoTrueClient): - expected_error_message = "Email not confirmed" - try: - client.sign_in( - email=email, - password=password, - ) - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up_with_email_and_password.__name__]) -def test_sign_in_with_the_wrong_password(client: SyncGoTrueClient): - expected_error_message = "Invalid login credentials" - try: - client.sign_in(email=email, password=f"{password}2") - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.skipif(not TEST_TWILIO, reason="Twilio is not available") -def test_sign_up_with_phone_and_password(client: SyncGoTrueClient): - try: - response = client.sign_up( - phone=phone, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, User) - assert not response.phone_confirmed_at - assert not response.email_confirmed_at - assert not response.last_sign_in_at - assert response.phone == phone - except Exception as e: - assert False, str(e) - - -@pytest.mark.skipif(not TEST_TWILIO, reason="Twilio is not available") -@pytest.mark.depends(on=[test_sign_up_with_phone_and_password.__name__]) -def test_verify_mobile_otp_errors_on_bad_token(client: SyncGoTrueClient): - expected_error_message = "Otp has expired or is invalid" - try: - client.verify_otp(phone=phone, token="123456") - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) diff --git a/tests/_sync/test_client_with_auto_confirm_enabled.py b/tests/_sync/test_client_with_auto_confirm_enabled.py deleted file mode 100644 index 72407be2..00000000 --- a/tests/_sync/test_client_with_auto_confirm_enabled.py +++ /dev/null @@ -1,457 +0,0 @@ -from typing import Iterable, Optional - -import pytest -from faker import Faker - -from ...gotrue import SyncGoTrueClient -from ...gotrue.errors import APIError -from ...gotrue.types import Session, User, UserAttributes - -GOTRUE_URL = "http://localhost:9998" -TEST_TWILIO = False - - -@pytest.fixture(name="client") -def create_client() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=True, - ) as client: - yield client - - -@pytest.fixture(name="client_with_session") -def create_client_with_session() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -@pytest.fixture(name="new_client") -def create_new_client() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = f"client_ac_enabled_{fake.email().lower()}" -set_session_email = f"client_ac_session_{fake.email().lower()}" -refresh_token_email = f"client_refresh_token_signin_{fake.email().lower()}" -password = fake.password() -access_token: Optional[str] = None - - -def test_sign_up(client: SyncGoTrueClient): - try: - response = client.sign_up( - email=email, - password=password, - data={"status": "alpha"}, - ) - assert isinstance(response, Session) - global access_token - access_token = response.access_token - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - assert response.user - assert response.user.id - assert response.user.email == email - assert response.user.email_confirmed_at - assert response.user.last_sign_in_at - assert response.user.created_at - assert response.user.updated_at - assert response.user.app_metadata - assert response.user.app_metadata.get("provider") == "email" - assert response.user.user_metadata - assert response.user.user_metadata.get("status") == "alpha" - except Exception as e: - assert False, str(e) - - -def test_set_session_should_return_no_error( - client_with_session: SyncGoTrueClient, -): - try: - response = client_with_session.sign_up( - email=set_session_email, - password=password, - ) - assert isinstance(response, Session) - assert response.refresh_token - client_with_session.set_session(refresh_token=response.refresh_token) - data = {"hello": "world"} - response = client_with_session.update(attributes=UserAttributes(data=data)) - assert response.user_metadata == data - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -def test_sign_up_the_same_user_twice_should_throw_an_error( - client: SyncGoTrueClient, -): - expected_error_message = "User already registered" - try: - client.sign_up( - email=email, - password=password, - ) - assert False - except APIError as e: - assert expected_error_message in e.msg - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -def test_set_auth_should_set_the_auth_headers_on_a_new_client( - new_client: SyncGoTrueClient, -): - try: - assert access_token - new_client.set_auth(access_token=access_token) - assert new_client.current_session - assert new_client.current_session.access_token == access_token - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends( - on=[test_set_auth_should_set_the_auth_headers_on_a_new_client.__name__] -) -def test_set_auth_should_set_the_auth_headers_on_a_new_client_and_recover( - new_client: SyncGoTrueClient, -): - try: - assert access_token - new_client.init_recover() - new_client.set_auth(access_token=access_token) - assert new_client.current_session - assert new_client.current_session.access_token == access_token - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -def test_sign_in(client: SyncGoTrueClient): - try: - response = client.sign_in(email=email, password=password) - assert isinstance(response, Session) - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - assert response.user - assert response.user.id - assert response.user.email == email - assert response.user.email_confirmed_at - assert response.user.last_sign_in_at - assert response.user.created_at - assert response.user.updated_at - assert response.user.app_metadata - assert response.user.app_metadata.get("provider") == "email" - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_refresh_token(client_with_session: SyncGoTrueClient): - try: - response = client_with_session.sign_up( - email=refresh_token_email, - password=password, - ) - assert isinstance(response, Session) - assert response.refresh_token - response2 = client_with_session.sign_in(refresh_token=response.refresh_token) - assert isinstance(response2, Session) - assert response2.access_token - assert response2.refresh_token - assert response2.expires_in - assert response2.expires_at - assert response2.user - assert response2.user.id - assert response2.user.email == refresh_token_email - assert response2.user.email_confirmed_at - assert response2.user.last_sign_in_at - assert response2.user.created_at - assert response2.user.updated_at - assert response2.user.app_metadata - assert response2.user.app_metadata.get("provider") == "email" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -def test_get_user(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.user() - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.app_metadata - provider = response.app_metadata.get("provider") - assert provider == "email" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -def test_get_session(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.session() - assert isinstance(response, Session) - assert response.access_token - assert response.refresh_token - assert response.expires_in - assert response.expires_at - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -def test_update_user(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.update(attributes=UserAttributes(data={"hello": "world"})) - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_in.__name__]) -def test_update_user_dict(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.update(attributes={"data": {"hello": "world"}}) - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_update_user.__name__]) -def test_get_user_after_update(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.user() - assert isinstance(response, User) - assert response.id - assert response.email == email - assert response.email_confirmed_at - assert response.last_sign_in_at - assert response.created_at - assert response.updated_at - assert response.user_metadata - assert response.user_metadata.get("hello") == "world" - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user_after_update.__name__]) -def test_sign_out(client: SyncGoTrueClient): - try: - client.init_recover() - client.sign_out() - response = client.session() - assert response is None - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_out.__name__]) -def test_get_user_after_sign_out(client: SyncGoTrueClient): - try: - client.init_recover() - response = client.user() - assert not response - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_out.__name__]) -def test_get_update_user_after_sign_out(client: SyncGoTrueClient): - expected_error_message = "Not logged in." - try: - client.init_recover() - client.update(attributes=UserAttributes(data={"hello": "world"})) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_user_after_sign_out.__name__]) -def test_sign_in_with_the_wrong_password(client: SyncGoTrueClient): - try: - client.sign_in(email=email, password=f"{password}2") - assert False - except APIError: - pass - except Exception as e: - assert False, str(e) - - -def test_sign_up_with_password_none(client: SyncGoTrueClient): - expected_error_message = "Password must be defined, can't be None." - try: - client.sign_up(email=email) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -def test_sign_up_with_email_and_phone_none(client: SyncGoTrueClient): - expected_error_message = "Email or phone must be defined, both can't be None." - try: - client.sign_up(password=password) - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_all_nones(client: SyncGoTrueClient): - expected_error_message = ( - "Email, phone, refresh_token, or provider must be defined, " - "all can't be None." - ) - try: - client.sign_in() - assert False - except ValueError as e: - assert str(e) == expected_error_message - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_magic_link(client: SyncGoTrueClient): - try: - response = client.sign_in(email=email) - assert response is None - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_sign_up.__name__]) -def test_get_session_from_url(client: SyncGoTrueClient): - try: - assert access_token - dummy_url = ( - "https://localhost" - f"?access_token={access_token}" - "&refresh_token=refresh_token" - "&token_type=bearer" - "&expires_in=3600" - "&type=recovery" - ) - response = client.get_session_from_url(url=dummy_url, store_session=True) - assert isinstance(response, Session) - except Exception as e: - assert False, str(e) - - -def test_get_session_from_url_errors(client: SyncGoTrueClient): - try: - dummy_url = "https://localhost" - error_description = fake.email() - try: - client.get_session_from_url( - url=f"{dummy_url}?error_description={error_description}" - ) - - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == error_description - try: - client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No access_token detected." - dummy_url += "?access_token=access_token" - try: - client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No refresh_token detected." - dummy_url += "&refresh_token=refresh_token" - try: - client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No token_type detected." - dummy_url += "&token_type=bearer" - try: - client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "No expires_in detected." - dummy_url += "&expires_in=str" - try: - client.get_session_from_url(url=dummy_url) - assert False - except APIError as e: - assert e.code == 400 - assert e.msg == "Invalid expires_in." - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_get_update_user_after_sign_out.__name__]) -def test_refresh_session(client: SyncGoTrueClient): - try: - response = client.sign_in(email=email, password=password) - assert isinstance(response, Session) - assert response.refresh_token - response = client.set_session(refresh_token=response.refresh_token) - assert isinstance(response, Session) - response = client.refresh_session() - assert isinstance(response, Session) - client.sign_out() - try: - client.refresh_session() - assert False - except ValueError as e: - assert str(e) == "Not logged in." - except Exception as e: - assert False, str(e) diff --git a/tests/_sync/test_client_with_sign_ups_disabled.py b/tests/_sync/test_client_with_sign_ups_disabled.py deleted file mode 100644 index b5b59302..00000000 --- a/tests/_sync/test_client_with_sign_ups_disabled.py +++ /dev/null @@ -1,125 +0,0 @@ -from typing import Iterable - -import pytest -from faker import Faker - -from ...gotrue import SyncGoTrueAPI, SyncGoTrueClient -from ...gotrue.constants import COOKIE_OPTIONS, DEFAULT_HEADERS -from ...gotrue.errors import APIError -from ...gotrue.types import CookieOptions, GenerateLinkType, User, UserAttributes - -GOTRUE_URL = "http://localhost:9997" -AUTH_ADMIN_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwicm9sZSI6InN1cGFiYXNlX2FkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.0sOtTSTfPv5oPZxsjvBO249FI4S4p0ymHoIZ6H6z9Y8" # noqa: E501 - - -@pytest.fixture(name="auth_admin") -def create_auth_admin() -> Iterable[SyncGoTrueAPI]: - with SyncGoTrueAPI( - url=GOTRUE_URL, - headers={"Authorization": f"Bearer {AUTH_ADMIN_TOKEN}"}, - cookie_options=CookieOptions.parse_obj(COOKIE_OPTIONS), - ) as api: - yield api - - -@pytest.fixture(name="client") -def create_client() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -fake = Faker() - -email = fake.email().lower() -password = fake.password() - - -def test_sign_up(client: SyncGoTrueClient): - expected_error_message = "Signups not allowed for this instance" - try: - client.sign_up(email=email, password=password) - assert False - except APIError as e: - assert e.msg == expected_error_message - except Exception as e: - assert False, str(e) - - -invited_user = fake.email().lower() - - -def test_generate_link_should_be_able_to_generate_multiple_links( - auth_admin: SyncGoTrueAPI, -): - try: - response = auth_admin.generate_link( - type=GenerateLinkType.invite, - email=invited_user, - redirect_to="http://localhost:9997", - ) - assert isinstance(response, User) - assert response.email == invited_user - assert response.action_link - assert "http://localhost:9997/?token=" in response.action_link - assert response.app_metadata - assert response.app_metadata.get("provider") == "email" - providers = response.app_metadata.get("providers") - assert providers - assert isinstance(providers, list) - assert len(providers) == 1 - assert providers[0] == "email" - assert response.role == "" - assert response.user_metadata == {} - assert response.identities == [] - user = response - response = auth_admin.generate_link( - type=GenerateLinkType.invite, - email=invited_user, - ) - assert isinstance(response, User) - assert response.email == invited_user - assert response.action_link - assert "http://localhost:9997/?token=" in response.action_link - assert response.app_metadata - assert response.app_metadata.get("provider") == "email" - providers = response.app_metadata.get("providers") - assert providers - assert isinstance(providers, list) - assert len(providers) == 1 - assert providers[0] == "email" - assert response.role == "" - assert response.user_metadata == {} - assert response.identities == [] - user_again = response - assert user.id == user_again.id - except Exception as e: - assert False, str(e) - - -email2 = fake.email().lower() - - -def test_create_user(auth_admin: SyncGoTrueAPI): - try: - attributes = UserAttributes(email=email2) - response = auth_admin.create_user(attributes=attributes) - assert isinstance(response, User) - assert response.email == email2 - response = auth_admin.list_users() - user = next((u for u in response if u.email == email2), None) - assert user - assert user.email == email2 - except Exception as e: - assert False, str(e) - - -def test_default_headers(client: SyncGoTrueClient): - """Test client for existing default headers""" - default_key = "X-Client-Info" - assert default_key in DEFAULT_HEADERS - assert default_key in client.api.headers - assert client.api.headers[default_key] == DEFAULT_HEADERS[default_key] diff --git a/tests/_sync/test_provider.py b/tests/_sync/test_provider.py deleted file mode 100644 index cc233bf2..00000000 --- a/tests/_sync/test_provider.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Iterable - -import pytest - -from ...gotrue import SyncGoTrueClient -from ...gotrue.types import Provider - -GOTRUE_URL = "http://localhost:9999" - - -@pytest.fixture(name="client") -def create_client() -> Iterable[SyncGoTrueClient]: - with SyncGoTrueClient( - url=GOTRUE_URL, - auto_refresh_token=False, - persist_session=False, - ) as client: - yield client - - -def test_sign_in_with_provider(client: SyncGoTrueClient): - try: - response = client.sign_in(provider=Provider.google) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_provider_can_append_a_redirect_url( - client: SyncGoTrueClient, -): - try: - response = client.sign_in( - provider=Provider.google, - redirect_to="https://localhost:9000/welcome", - ) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_provider_can_append_scopes(client: SyncGoTrueClient): - try: - response = client.sign_in(provider=Provider.google, scopes="repo") - assert isinstance(response, str) - except Exception as e: - assert False, str(e) - - -def test_sign_in_with_provider_can_append_multiple_options( - client: SyncGoTrueClient, -): - try: - response = client.sign_in( - provider=Provider.google, - redirect_to="https://localhost:9000/welcome", - scopes="repo", - ) - assert isinstance(response, str) - except Exception as e: - assert False, str(e) diff --git a/tests/_sync/test_subscriptions.py b/tests/_sync/test_subscriptions.py deleted file mode 100644 index 3c0f7238..00000000 --- a/tests/_sync/test_subscriptions.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest - -from ...gotrue import SyncGoTrueClient -from ...gotrue.types import Subscription - -GOTRUE_URL = "http://localhost:9999" - - -@pytest.fixture(name="client") -def create_client() -> SyncGoTrueClient: - with SyncGoTrueClient(url=GOTRUE_URL) as client: - return client - - -@pytest.fixture(name="subscription") -def create_subscription(client: SyncGoTrueClient): - return client.on_auth_state_change( - callback=lambda _, __: print("Auth state changed") - ) - - -def test_subscribe_a_listener(client: SyncGoTrueClient, subscription: Subscription): - try: - assert len(client.state_change_emitters) - except Exception as e: - assert False, str(e) - - -@pytest.mark.depends(on=[test_subscribe_a_listener.__name__]) -def test_unsubscribe_a_listener(client: SyncGoTrueClient, subscription: Subscription): - try: - subscription.unsubscribe() - assert not len(client.state_change_emitters) - except Exception as e: - assert False, str(e) From 30fd7512221145363f1ac5fb44fed86cdb08eb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 30 Oct 2022 23:34:19 +0000 Subject: [PATCH 20/35] tests: add tests for create user in admin api --- gotrue/_sync/gotrue_client.py | 4 +- gotrue/types.py | 21 ++++ tests/_async/clients.py | 145 +++++++++++++++----------- tests/_async/old/.gitignore | 1 - tests/_async/test_gotrue_admin_api.py | 61 +++++++++++ tests/_async/test_utils.py | 9 +- tests/_async/utils.py | 9 +- 7 files changed, 179 insertions(+), 71 deletions(-) delete mode 100644 tests/_async/old/.gitignore create mode 100644 tests/_async/test_gotrue_admin_api.py diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index 11940268..30e6cf79 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -441,9 +441,7 @@ def set_session(self, access_token: str, refresh_token: str) -> AuthResponse: self._notify_all_subscribers("TOKEN_REFRESHED", session) return AuthResponse(session=session, user=response.user) - def refresh_session( - self, refresh_token: Union[str, None] = None - ) -> AuthResponse: + def refresh_session(self, refresh_token: Union[str, None] = None) -> AuthResponse: """ Returns a new session, regardless of expiry status. diff --git a/gotrue/types.py b/gotrue/types.py index 1cb0c0cc..51473858 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -613,3 +613,24 @@ class DecodedJWTDict(TypedDict): exp: NotRequired[int] aal: NotRequired[Union[AuthenticatorAssuranceLevels, None]] amr: NotRequired[Union[List[AMREntry], None]] + + +AMREntry.update_forward_refs() +AuthResponse.update_forward_refs() +OAuthResponse.update_forward_refs() +UserResponse.update_forward_refs() +Session.update_forward_refs() +UserIdentity.update_forward_refs() +Factor.update_forward_refs() +User.update_forward_refs() +Subscription.update_forward_refs() +AuthMFAVerifyResponse.update_forward_refs() +AuthMFAEnrollResponseTotp.update_forward_refs() +AuthMFAEnrollResponse.update_forward_refs() +AuthMFAUnenrollResponse.update_forward_refs() +AuthMFAChallengeResponse.update_forward_refs() +AuthMFAListFactorsResponse.update_forward_refs() +AuthMFAGetAuthenticatorAssuranceLevelResponse.update_forward_refs() +AuthMFAAdminDeleteFactorResponse.update_forward_refs() +AuthMFAAdminListFactorsResponse.update_forward_refs() +GenerateLinkProperties.update_forward_refs() diff --git a/tests/_async/clients.py b/tests/_async/clients.py index e3001d01..d62a0366 100644 --- a/tests/_async/clients.py +++ b/tests/_async/clients.py @@ -1,6 +1,7 @@ -from gotrue import AsyncGoTrueAdminAPI, AsyncGoTrueClient from jwt import encode +from gotrue import AsyncGoTrueAdminAPI, AsyncGoTrueClient + SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 @@ -25,56 +26,72 @@ GOTRUE_JWT_SECRET, ) -auth_client = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - persist_session=True, -) -auth_client_with_session = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - persist_session=False, -) +def auth_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) -auth_subscription_client = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - persist_session=True, -) +def auth_client_with_session(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=False, + ) -client_api_auto_confirm_enabled_client = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - auto_refresh_token=False, - persist_session=True, -) -client_api_auto_confirm_off_signups_enabled_client = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - persist_session=True, -) +def auth_subscription_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) -client_api_auto_confirm_disabled_client = AsyncGoTrueClient( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, - auto_refresh_token=False, - persist_session=True, -) -auth_admin_api_auto_confirm_enabled_client = AsyncGoTrueAdminAPI( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, -) +def client_api_auto_confirm_enabled_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) + + +def client_api_auto_confirm_off_signups_enabled_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, + ) + + +def client_api_auto_confirm_disabled_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, + ) + + +def auth_admin_api_auto_confirm_enabled_client(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, + ) + + +def auth_admin_api_auto_confirm_disabled_client(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, + ) -auth_admin_api_auto_confirm_disabled_client = AsyncGoTrueAdminAPI( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - headers={ - "Authorization": f"Bearer {AUTH_ADMIN_JWT}", - }, -) SERVICE_ROLE_JWT = encode( { @@ -83,23 +100,29 @@ GOTRUE_JWT_SECRET, ) -service_role_api_client = AsyncGoTrueAdminAPI( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, - headers={ - "Authorization": f"Bearer {SERVICE_ROLE_JWT}", - }, -) -service_role_api_client_with_sms = AsyncGoTrueAdminAPI( - url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, - headers={ - "Authorization": f"Bearer {SERVICE_ROLE_JWT}", - }, -) +def service_role_api_client(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) -service_role_api_client_no_sms = AsyncGoTrueAdminAPI( - url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, - headers={ - "Authorization": f"Bearer {SERVICE_ROLE_JWT}", - }, -) + +def service_role_api_client_with_sms(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) + + +def service_role_api_client_no_sms(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) diff --git a/tests/_async/old/.gitignore b/tests/_async/old/.gitignore deleted file mode 100644 index 8e5bbf04..00000000 --- a/tests/_async/old/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.py \ No newline at end of file diff --git a/tests/_async/test_gotrue_admin_api.py b/tests/_async/test_gotrue_admin_api.py new file mode 100644 index 00000000..719ba6c1 --- /dev/null +++ b/tests/_async/test_gotrue_admin_api.py @@ -0,0 +1,61 @@ +from .clients import service_role_api_client +from .utils import ( + create_new_user_with_email, + mock_app_metadata, + mock_user_credentials, + mock_user_metadata, +) + + +async def test_create_user_should_create_a_new_user(): + credentials = mock_user_credentials() + response = await create_new_user_with_email(email=credentials.get("email")) + assert response.email == credentials.get("email") + + +async def test_create_user_with_user_metadata(): + user_metadata = mock_user_metadata() + credentials = mock_user_credentials() + response = await service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "user_metadata": user_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert response.user.user_metadata == user_metadata + assert "profile_image" in response.user.user_metadata + + +async def test_create_user_with_app_metadata(): + app_metadata = mock_app_metadata() + credentials = mock_user_credentials() + response = await service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "app_metadata": app_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert "provider" in response.user.app_metadata + assert "providers" in response.user.app_metadata + + +async def test_create_user_with_user_and_app_metadata(): + user_metadata = mock_user_metadata() + app_metadata = mock_app_metadata() + credentials = mock_user_credentials() + response = await service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "user_metadata": user_metadata, + "app_metadata": app_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert "profile_image" in response.user.user_metadata + assert "provider" in response.user.app_metadata + assert "providers" in response.user.app_metadata diff --git a/tests/_async/test_utils.py b/tests/_async/test_utils.py index 480bbc15..f5a144c4 100644 --- a/tests/_async/test_utils.py +++ b/tests/_async/test_utils.py @@ -1,6 +1,11 @@ from time import time -from .utils import create_new_user_with_email, mock_user_credentials, mock_user_metadata +from .utils import ( + create_new_user_with_email, + mock_app_metadata, + mock_user_credentials, + mock_user_metadata, +) def test_mock_user_credentials_has_email(): @@ -28,6 +33,6 @@ def test_mock_user_metadata(): def test_mock_app_metadata(): - app_metadata = mock_user_metadata() + app_metadata = mock_app_metadata() assert app_metadata assert app_metadata.get("roles") diff --git a/tests/_async/utils.py b/tests/_async/utils.py index 1a9c1ed7..9d102d1b 100644 --- a/tests/_async/utils.py +++ b/tests/_async/utils.py @@ -6,10 +6,9 @@ from jwt import encode from typing_extensions import NotRequired, TypedDict -from ...gotrue.types import User -from .clients import GOTRUE_JWT_SECRET, service_role_api_client +from gotrue.types import User -fake = Faker() +from .clients import GOTRUE_JWT_SECRET, service_role_api_client def mock_access_token() -> str: @@ -37,6 +36,7 @@ class Credentials(TypedDict): def mock_user_credentials( options: OptionalCredentials = {}, ) -> Credentials: + fake = Faker() rand_numbers = str(time()) return { "email": options.get("email") or fake.email(), @@ -50,6 +50,7 @@ def mock_verification_otp() -> str: def mock_user_metadata(): + fake = Faker() return { "profile_image": fake.url(), } @@ -72,7 +73,7 @@ async def create_new_user_with_email( "password": password, } ) - response = await service_role_api_client.create_user( + response = await service_role_api_client().create_user( { "email": credentials["email"], "password": credentials["password"], From ec6618b0c4e1ac0137f98debbc02b98e61e8ef60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 30 Oct 2022 23:38:46 +0000 Subject: [PATCH 21/35] fix: use literal from typing extensions --- gotrue/_async/gotrue_base_api.py | 4 ++-- gotrue/_sync/gotrue_base_api.py | 4 ++-- gotrue/types.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py index 757b9c89..7f1a948c 100644 --- a/gotrue/_async/gotrue_base_api.py +++ b/gotrue/_async/gotrue_base_api.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload +from typing import Any, Callable, Dict, TypeVar, Union, overload from httpx import Response from pydantic import BaseModel -from typing_extensions import Self +from typing_extensions import Literal, Self from ..helpers import handle_exception from ..http_clients import AsyncClient diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py index 99a78b87..f7592d1a 100644 --- a/gotrue/_sync/gotrue_base_api.py +++ b/gotrue/_sync/gotrue_base_api.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Any, Callable, Dict, Literal, TypeVar, Union, overload +from typing import Any, Callable, Dict, TypeVar, Union, overload from httpx import Response from pydantic import BaseModel -from typing_extensions import Self +from typing_extensions import Literal, Self from ..helpers import handle_exception from ..http_clients import SyncClient diff --git a/gotrue/types.py b/gotrue/types.py index 51473858..6c61d25e 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -2,10 +2,10 @@ from datetime import datetime from time import time -from typing import Any, Callable, Dict, List, Literal, Union +from typing import Any, Callable, Dict, List, Union from pydantic import BaseModel, root_validator -from typing_extensions import NotRequired, TypedDict +from typing_extensions import Literal, NotRequired, TypedDict Provider = Literal[ "apple", From 33756af49f995017cf3d9fdfd581b4974f13003c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 30 Oct 2022 23:42:59 +0000 Subject: [PATCH 22/35] tests: add python 3.11 and update poetry version --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 324520e3..e89cd29f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9, "3.10"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Clone Repository @@ -20,7 +20,7 @@ jobs: - name: Set up Poetry uses: abatilo/actions-poetry@v2.1.6 with: - poetry-version: 1.1.13 + poetry-version: 1.2.2 - name: Run Tests run: make run_tests - name: Upload Coverage From 45afb351ebe960a292edb77d4ea77bc235ee1563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 30 Oct 2022 23:47:51 +0000 Subject: [PATCH 23/35] fix: respect EXPIRY_MARGIN on getSession --- gotrue/_async/gotrue_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gotrue/_async/gotrue_client.py b/gotrue/_async/gotrue_client.py index d1f61b57..97f5f14c 100644 --- a/gotrue/_async/gotrue_client.py +++ b/gotrue/_async/gotrue_client.py @@ -352,8 +352,9 @@ async def get_session(self) -> Union[Session, None]: current_session = self._in_memory_session if not current_session: return None + time_now = round(time()) has_expired = ( - current_session.expires_at <= time() + current_session.expires_at <= time_now + EXPIRY_MARGIN if current_session.expires_at else False ) From 01385eac0c0ae79ae583b4ade84ab985f5933d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sun, 30 Oct 2022 23:48:41 +0000 Subject: [PATCH 24/35] chore: gen sync files --- gotrue/_sync/gotrue_client.py | 3 +- tests/_sync/__init__.py | 0 tests/_sync/clients.py | 128 +++++++++++++++++++++++++++ tests/_sync/test_gotrue_admin_api.py | 61 +++++++++++++ tests/_sync/test_utils.py | 38 ++++++++ tests/_sync/utils.py | 82 +++++++++++++++++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 tests/_sync/__init__.py create mode 100644 tests/_sync/clients.py create mode 100644 tests/_sync/test_gotrue_admin_api.py create mode 100644 tests/_sync/test_utils.py create mode 100644 tests/_sync/utils.py diff --git a/gotrue/_sync/gotrue_client.py b/gotrue/_sync/gotrue_client.py index 30e6cf79..be893d90 100644 --- a/gotrue/_sync/gotrue_client.py +++ b/gotrue/_sync/gotrue_client.py @@ -352,8 +352,9 @@ def get_session(self) -> Union[Session, None]: current_session = self._in_memory_session if not current_session: return None + time_now = round(time()) has_expired = ( - current_session.expires_at <= time() + current_session.expires_at <= time_now + EXPIRY_MARGIN if current_session.expires_at else False ) diff --git a/tests/_sync/__init__.py b/tests/_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/_sync/clients.py b/tests/_sync/clients.py new file mode 100644 index 00000000..d7dc428f --- /dev/null +++ b/tests/_sync/clients.py @@ -0,0 +1,128 @@ +from jwt import encode + +from gotrue import SyncGoTrueAdminAPI, SyncGoTrueClient + +SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 +SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 +SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 + +GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF = ( + f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT}" +) +GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON = ( + f"http://localhost:{SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT}" +) +GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF = ( + f"http://localhost:{SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT}" +) + +GOTRUE_JWT_SECRET = "37c304f8-51aa-419a-a1af-06154e63707a" + +AUTH_ADMIN_JWT = encode( + { + "sub": "1234567890", + "role": "supabase_admin", + }, + GOTRUE_JWT_SECRET, +) + + +def auth_client(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) + + +def auth_client_with_session(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=False, + ) + + +def auth_subscription_client(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) + + +def client_api_auto_confirm_enabled_client(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) + + +def client_api_auto_confirm_off_signups_enabled_client(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, + ) + + +def client_api_auto_confirm_disabled_client(): + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + auto_refresh_token=False, + persist_session=True, + ) + + +def auth_admin_api_auto_confirm_enabled_client(): + return SyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, + ) + + +def auth_admin_api_auto_confirm_disabled_client(): + return SyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {AUTH_ADMIN_JWT}", + }, + ) + + +SERVICE_ROLE_JWT = encode( + { + "role": "service_role", + }, + GOTRUE_JWT_SECRET, +) + + +def service_role_api_client(): + return SyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) + + +def service_role_api_client_with_sms(): + return SyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) + + +def service_role_api_client_no_sms(): + return SyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_DISABLED_AUTO_CONFIRM_OFF, + headers={ + "Authorization": f"Bearer {SERVICE_ROLE_JWT}", + }, + ) diff --git a/tests/_sync/test_gotrue_admin_api.py b/tests/_sync/test_gotrue_admin_api.py new file mode 100644 index 00000000..413e891e --- /dev/null +++ b/tests/_sync/test_gotrue_admin_api.py @@ -0,0 +1,61 @@ +from .clients import service_role_api_client +from .utils import ( + create_new_user_with_email, + mock_app_metadata, + mock_user_credentials, + mock_user_metadata, +) + + +def test_create_user_should_create_a_new_user(): + credentials = mock_user_credentials() + response = create_new_user_with_email(email=credentials.get("email")) + assert response.email == credentials.get("email") + + +def test_create_user_with_user_metadata(): + user_metadata = mock_user_metadata() + credentials = mock_user_credentials() + response = service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "user_metadata": user_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert response.user.user_metadata == user_metadata + assert "profile_image" in response.user.user_metadata + + +def test_create_user_with_app_metadata(): + app_metadata = mock_app_metadata() + credentials = mock_user_credentials() + response = service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "app_metadata": app_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert "provider" in response.user.app_metadata + assert "providers" in response.user.app_metadata + + +def test_create_user_with_user_and_app_metadata(): + user_metadata = mock_user_metadata() + app_metadata = mock_app_metadata() + credentials = mock_user_credentials() + response = service_role_api_client().create_user( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + "user_metadata": user_metadata, + "app_metadata": app_metadata, + } + ) + assert response.user.email == credentials.get("email") + assert "profile_image" in response.user.user_metadata + assert "provider" in response.user.app_metadata + assert "providers" in response.user.app_metadata diff --git a/tests/_sync/test_utils.py b/tests/_sync/test_utils.py new file mode 100644 index 00000000..23b4ac9c --- /dev/null +++ b/tests/_sync/test_utils.py @@ -0,0 +1,38 @@ +from time import time + +from .utils import ( + create_new_user_with_email, + mock_app_metadata, + mock_user_credentials, + mock_user_metadata, +) + + +def test_mock_user_credentials_has_email(): + credentials = mock_user_credentials() + assert credentials.get("email") + assert credentials.get("password") + + +def test_mock_user_credentials_has_phone(): + credentials = mock_user_credentials() + assert credentials.get("phone") + assert credentials.get("password") + + +def test_create_new_user_with_email(): + email = f"user+{int(time())}@example.com" + user = create_new_user_with_email(email=email) + assert user.email == email + + +def test_mock_user_metadata(): + user_metadata = mock_user_metadata() + assert user_metadata + assert user_metadata.get("profile_image") + + +def test_mock_app_metadata(): + app_metadata = mock_app_metadata() + assert app_metadata + assert app_metadata.get("roles") diff --git a/tests/_sync/utils.py b/tests/_sync/utils.py new file mode 100644 index 00000000..024cd6eb --- /dev/null +++ b/tests/_sync/utils.py @@ -0,0 +1,82 @@ +from random import random +from time import time +from typing import Union + +from faker import Faker +from jwt import encode +from typing_extensions import NotRequired, TypedDict + +from gotrue.types import User + +from .clients import GOTRUE_JWT_SECRET, service_role_api_client + + +def mock_access_token() -> str: + return encode( + { + "sub": "1234567890", + "role": "anon_key", + }, + GOTRUE_JWT_SECRET, + ) + + +class OptionalCredentials(TypedDict): + email: NotRequired[Union[str, None]] + phone: NotRequired[Union[str, None]] + password: NotRequired[Union[str, None]] + + +class Credentials(TypedDict): + email: str + phone: str + password: str + + +def mock_user_credentials( + options: OptionalCredentials = {}, +) -> Credentials: + fake = Faker() + rand_numbers = str(time()) + return { + "email": options.get("email") or fake.email(), + "phone": options.get("phone") or f"1{rand_numbers[-11:]}", + "password": options.get("password") or fake.password(), + } + + +def mock_verification_otp() -> str: + return str(int(100000 + random() * 900000)) + + +def mock_user_metadata(): + fake = Faker() + return { + "profile_image": fake.url(), + } + + +def mock_app_metadata(): + return { + "roles": ["editor", "publisher"], + } + + +def create_new_user_with_email( + *, + email: Union[str, None] = None, + password: Union[str, None] = None, +) -> User: + credentials = mock_user_credentials( + { + "email": email, + "password": password, + } + ) + response = service_role_api_client().create_user( + { + "email": credentials["email"], + "password": credentials["password"], + } + ) + return response.user From a973a1cce3ea90e3c3ece0def70626c68727ae0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 5 Nov 2022 06:26:42 +0000 Subject: [PATCH 25/35] fix: list users method of admin api --- gotrue/_async/gotrue_admin_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gotrue/_async/gotrue_admin_api.py b/gotrue/_async/gotrue_admin_api.py index 29dc4343..c497a24a 100644 --- a/gotrue/_async/gotrue_admin_api.py +++ b/gotrue/_async/gotrue_admin_api.py @@ -104,7 +104,9 @@ async def list_users(self) -> List[User]: return await self._request( "GET", "admin/users", - xform=lambda data: [User.parse_obj(user) for user in data], + xform=lambda data: [User.parse_obj(user) for user in data["users"]] + if "users" in data + else [], ) async def get_user_by_id(self, uid: str) -> UserResponse: From 8c076f0d23411b0ee1861b91739f45069275c630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 5 Nov 2022 06:27:05 +0000 Subject: [PATCH 26/35] test: add test to user fetch methods of admin api --- tests/_async/test_gotrue_admin_api.py | 34 ++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/_async/test_gotrue_admin_api.py b/tests/_async/test_gotrue_admin_api.py index 719ba6c1..1ee020a8 100644 --- a/tests/_async/test_gotrue_admin_api.py +++ b/tests/_async/test_gotrue_admin_api.py @@ -1,4 +1,4 @@ -from .clients import service_role_api_client +from .clients import auth_client_with_session, service_role_api_client from .utils import ( create_new_user_with_email, mock_app_metadata, @@ -59,3 +59,35 @@ async def test_create_user_with_user_and_app_metadata(): assert "profile_image" in response.user.user_metadata assert "provider" in response.user.app_metadata assert "providers" in response.user.app_metadata + + +async def test_list_users_should_return_registered_users(): + credentials = mock_user_credentials() + await create_new_user_with_email(email=credentials.get("email")) + users = await service_role_api_client().list_users() + assert users + emails = [user.email for user in users] + assert emails + assert credentials.get("email") in emails + + +async def test_get_user_fetches_a_user_by_their_access_token(): + credentials = mock_user_credentials() + auth_client_with_session_current_user = auth_client_with_session() + response = await auth_client_with_session_current_user.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert response.session + response = await auth_client_with_session_current_user.get_user() + assert response.user.email == credentials.get("email") + + +async def test_get_user_by_id_should_a_registered_user_given_its_user_identifier(): + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + assert user.id + response = await service_role_api_client().get_user_by_id(user.id) + assert response.user.email == credentials.get("email") From 9db7ce680326c149730ef84cbe9540ad8a735676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Mon, 7 Nov 2022 06:36:38 +0000 Subject: [PATCH 27/35] fix: bugs in admin api and finish tests implementation --- gotrue/_async/gotrue_admin_api.py | 15 +- gotrue/_async/gotrue_admin_mfa_api.py | 4 +- gotrue/_async/gotrue_base_api.py | 6 +- gotrue/_async/gotrue_mfa_api.py | 14 +- gotrue/_sync/gotrue_admin_api.py | 19 ++- gotrue/_sync/gotrue_admin_mfa_api.py | 4 +- gotrue/_sync/gotrue_base_api.py | 6 +- gotrue/_sync/gotrue_mfa_api.py | 14 +- gotrue/types.py | 2 +- tests/_async/test_gotrue_admin_api.py | 182 +++++++++++++++++++++- tests/_async/utils.py | 2 +- tests/_sync/test_gotrue_admin_api.py | 214 +++++++++++++++++++++++++- tests/_sync/utils.py | 2 +- 13 files changed, 440 insertions(+), 44 deletions(-) diff --git a/gotrue/_async/gotrue_admin_api.py b/gotrue/_async/gotrue_admin_api.py index c497a24a..66ea481a 100644 --- a/gotrue/_async/gotrue_admin_api.py +++ b/gotrue/_async/gotrue_admin_api.py @@ -42,7 +42,12 @@ async def sign_out(self, jwt: str) -> None: """ Removes a logged-in session. """ - return await self._request("POST", "logout", jwt=jwt) + return await self._request( + "POST", + "logout", + jwt=jwt, + no_resolve_json=True, + ) async def invite_user_by_email( self, @@ -140,18 +145,14 @@ async def update_user_by_id( xform=parse_user_response, ) - async def delete_user(self, id: str) -> UserResponse: + async def delete_user(self, id: str) -> None: """ Delete a user. Requires a `service_role` key. This function should only be called on a server. Never expose your `service_role` key in the browser. """ - return await self._request( - "DELETE", - f"admin/users/{id}", - xform=parse_user_response, - ) + return await self._request("DELETE", f"admin/users/{id}") async def _list_factors( self, diff --git a/gotrue/_async/gotrue_admin_mfa_api.py b/gotrue/_async/gotrue_admin_mfa_api.py index 4fd429c2..ca812fcd 100644 --- a/gotrue/_async/gotrue_admin_mfa_api.py +++ b/gotrue/_async/gotrue_admin_mfa_api.py @@ -18,7 +18,7 @@ async def list_factors( """ Lists all factors attached to a user. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def delete_factor( self, @@ -29,4 +29,4 @@ async def delete_factor( sessions (if the deleted factor was verified). There's no need to delete unverified factors. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover diff --git a/gotrue/_async/gotrue_base_api.py b/gotrue/_async/gotrue_base_api.py index 7f1a948c..6431b60b 100644 --- a/gotrue/_async/gotrue_base_api.py +++ b/gotrue/_async/gotrue_base_api.py @@ -47,7 +47,7 @@ async def _request( no_resolve_json: Literal[False] = False, xform: Callable[[Any], T], ) -> T: - ... + ... # pragma: no cover @overload async def _request( @@ -63,7 +63,7 @@ async def _request( no_resolve_json: Literal[True], xform: Callable[[Response], T], ) -> T: - ... + ... # pragma: no cover @overload async def _request( @@ -78,7 +78,7 @@ async def _request( body: Union[Any, None] = None, no_resolve_json: bool = False, ) -> None: - ... + ... # pragma: no cover async def _request( self, diff --git a/gotrue/_async/gotrue_mfa_api.py b/gotrue/_async/gotrue_mfa_api.py index 815f0195..a30c4c73 100644 --- a/gotrue/_async/gotrue_mfa_api.py +++ b/gotrue/_async/gotrue_mfa_api.py @@ -30,14 +30,14 @@ async def enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: factor. All other sessions are logged out and the current one gets an `aal2` authenticator level. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: """ Prepares a challenge used to verify that a user has access to a MFA factor. Provide the challenge ID and verification code by calling `verify`. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def challenge_and_verify( self, @@ -48,14 +48,14 @@ async def challenge_and_verify( to verify against it thereafter. The verification code is provided by the user by entering a code seen in their authenticator app. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: """ Verifies a verification code against a challenge. The verification code is provided by the user by entering a code seen in their authenticator app. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: """ @@ -63,7 +63,7 @@ async def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: and it's not necessary to unenroll them. Unenrolling a verified MFA factor cannot be done from a session with an `aal1` authenticator level. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def list_factors(self) -> AuthMFAListFactorsResponse: """ @@ -73,7 +73,7 @@ async def list_factors(self) -> AuthMFAListFactorsResponse: This uses a cached version of the factors and avoids incurring a network call. If you need to update this list, call `get_user` first. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover async def get_authenticator_assurance_level( self, @@ -91,4 +91,4 @@ async def get_authenticator_assurance_level( and rarely uses the network. You can use this to check whether the current user needs to be shown a screen to verify their MFA factors. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover diff --git a/gotrue/_sync/gotrue_admin_api.py b/gotrue/_sync/gotrue_admin_api.py index 2fdf2094..1d367fce 100644 --- a/gotrue/_sync/gotrue_admin_api.py +++ b/gotrue/_sync/gotrue_admin_api.py @@ -42,7 +42,12 @@ def sign_out(self, jwt: str) -> None: """ Removes a logged-in session. """ - return self._request("POST", "logout", jwt=jwt) + return self._request( + "POST", + "logout", + jwt=jwt, + no_resolve_json=True, + ) def invite_user_by_email( self, @@ -104,7 +109,9 @@ def list_users(self) -> List[User]: return self._request( "GET", "admin/users", - xform=lambda data: [User.parse_obj(user) for user in data], + xform=lambda data: [User.parse_obj(user) for user in data["users"]] + if "users" in data + else [], ) def get_user_by_id(self, uid: str) -> UserResponse: @@ -138,18 +145,14 @@ def update_user_by_id( xform=parse_user_response, ) - def delete_user(self, id: str) -> UserResponse: + def delete_user(self, id: str) -> None: """ Delete a user. Requires a `service_role` key. This function should only be called on a server. Never expose your `service_role` key in the browser. """ - return self._request( - "DELETE", - f"admin/users/{id}", - xform=parse_user_response, - ) + return self._request("DELETE", f"admin/users/{id}") def _list_factors( self, diff --git a/gotrue/_sync/gotrue_admin_mfa_api.py b/gotrue/_sync/gotrue_admin_mfa_api.py index 51c1681b..c3fcfc8e 100644 --- a/gotrue/_sync/gotrue_admin_mfa_api.py +++ b/gotrue/_sync/gotrue_admin_mfa_api.py @@ -18,7 +18,7 @@ def list_factors( """ Lists all factors attached to a user. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def delete_factor( self, @@ -29,4 +29,4 @@ def delete_factor( sessions (if the deleted factor was verified). There's no need to delete unverified factors. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover diff --git a/gotrue/_sync/gotrue_base_api.py b/gotrue/_sync/gotrue_base_api.py index f7592d1a..b180e341 100644 --- a/gotrue/_sync/gotrue_base_api.py +++ b/gotrue/_sync/gotrue_base_api.py @@ -47,7 +47,7 @@ def _request( no_resolve_json: Literal[False] = False, xform: Callable[[Any], T], ) -> T: - ... + ... # pragma: no cover @overload def _request( @@ -63,7 +63,7 @@ def _request( no_resolve_json: Literal[True], xform: Callable[[Response], T], ) -> T: - ... + ... # pragma: no cover @overload def _request( @@ -78,7 +78,7 @@ def _request( body: Union[Any, None] = None, no_resolve_json: bool = False, ) -> None: - ... + ... # pragma: no cover def _request( self, diff --git a/gotrue/_sync/gotrue_mfa_api.py b/gotrue/_sync/gotrue_mfa_api.py index 0e936f10..16bec8d5 100644 --- a/gotrue/_sync/gotrue_mfa_api.py +++ b/gotrue/_sync/gotrue_mfa_api.py @@ -30,14 +30,14 @@ def enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: factor. All other sessions are logged out and the current one gets an `aal2` authenticator level. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def challenge(self, params: MFAChallengeParams) -> AuthMFAChallengeResponse: """ Prepares a challenge used to verify that a user has access to a MFA factor. Provide the challenge ID and verification code by calling `verify`. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def challenge_and_verify( self, @@ -48,14 +48,14 @@ def challenge_and_verify( to verify against it thereafter. The verification code is provided by the user by entering a code seen in their authenticator app. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def verify(self, params: MFAVerifyParams) -> AuthMFAVerifyResponse: """ Verifies a verification code against a challenge. The verification code is provided by the user by entering a code seen in their authenticator app. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: """ @@ -63,7 +63,7 @@ def unenroll(self, params: MFAUnenrollParams) -> AuthMFAUnenrollResponse: and it's not necessary to unenroll them. Unenrolling a verified MFA factor cannot be done from a session with an `aal1` authenticator level. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def list_factors(self) -> AuthMFAListFactorsResponse: """ @@ -73,7 +73,7 @@ def list_factors(self) -> AuthMFAListFactorsResponse: This uses a cached version of the factors and avoids incurring a network call. If you need to update this list, call `get_user` first. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def get_authenticator_assurance_level( self, @@ -91,4 +91,4 @@ def get_authenticator_assurance_level( and rarely uses the network. You can use this to check whether the current user needs to be shown a screen to verify their MFA factors. """ - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover diff --git a/gotrue/types.py b/gotrue/types.py index 6c61d25e..74a93136 100644 --- a/gotrue/types.py +++ b/gotrue/types.py @@ -364,7 +364,7 @@ class GenerateRecoveryLinkParams(TypedDict): class GenerateEmailChangeLinkParams(TypedDict): - type: Literal["email_change"] + type: Literal["email_change_current", "email_change_new"] email: str new_email: str options: NotRequired[GenerateLinkParamsOptions] diff --git a/tests/_async/test_gotrue_admin_api.py b/tests/_async/test_gotrue_admin_api.py index 1ee020a8..a70380cf 100644 --- a/tests/_async/test_gotrue_admin_api.py +++ b/tests/_async/test_gotrue_admin_api.py @@ -1,9 +1,17 @@ -from .clients import auth_client_with_session, service_role_api_client +from gotrue.errors import AuthError + +from .clients import ( + auth_client_with_session, + client_api_auto_confirm_disabled_client, + client_api_auto_confirm_off_signups_enabled_client, + service_role_api_client, +) from .utils import ( create_new_user_with_email, mock_app_metadata, mock_user_credentials, mock_user_metadata, + mock_verification_otp, ) @@ -91,3 +99,175 @@ async def test_get_user_by_id_should_a_registered_user_given_its_user_identifier assert user.id response = await service_role_api_client().get_user_by_id(user.id) assert response.user.email == credentials.get("email") + + +async def test_modify_email_using_update_user_by_id(): + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + response = await service_role_api_client().update_user_by_id( + user.id, + { + "email": f"new_{user.email}", + }, + ) + assert response.user.email == f"new_{user.email}" + + +async def test_modify_user_metadata_using_update_user_by_id(): + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + user_metadata = {"favorite_color": "yellow"} + response = await service_role_api_client().update_user_by_id( + user.id, + { + "user_metadata": user_metadata, + }, + ) + assert response.user.email == user.email + assert response.user.user_metadata == user_metadata + + +async def test_modify_app_metadata_using_update_user_by_id(): + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + app_metadata = {"roles": ["admin", "publisher"]} + response = await service_role_api_client().update_user_by_id( + user.id, + { + "app_metadata": app_metadata, + }, + ) + assert response.user.email == user.email + assert "roles" in response.user.app_metadata + + +async def test_modify_confirm_email_using_update_user_by_id(): + credentials = mock_user_credentials() + response = await client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert response.user + assert not response.user.email_confirmed_at + response = await service_role_api_client().update_user_by_id( + response.user.id, + { + "email_confirm": True, + }, + ) + assert response.user.email_confirmed_at + + +async def test_delete_user_should_be_able_delete_an_existing_user(): + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + await service_role_api_client().delete_user(user.id) + users = await service_role_api_client().list_users() + emails = [user.email for user in users] + assert credentials.get("email") not in emails + + +async def test_generate_link_supports_sign_up_with_generate_confirmation_signup_link(): + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + user_metadata = {"status": "alpha"} + response = await service_role_api_client().generate_link( + { + "type": "signup", + "email": credentials.get("email"), + "password": credentials.get("password"), + "options": { + "data": user_metadata, + "redirect_to": redirect_to, + }, + }, + ) + assert response.user.user_metadata == user_metadata + + +async def test_generate_link_supports_updating_emails_with_generate_email_change_links(): # noqa: E501 + credentials = mock_user_credentials() + user = await create_new_user_with_email(email=credentials.get("email")) + assert user.email + assert user.email == credentials.get("email") + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + response = await service_role_api_client().generate_link( + { + "type": "email_change_current", + "email": user.email, + "new_email": credentials.get("email"), + "options": { + "redirect_to": redirect_to, + }, + }, + ) + assert response.user.new_email == credentials.get("email") + + +async def test_invite_user_by_email_creates_a_new_user_with_an_invited_at_timestamp(): + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + user_metadata = {"status": "alpha"} + response = await service_role_api_client().invite_user_by_email( + credentials.get("email"), + { + "data": user_metadata, + "redirect_to": redirect_to, + }, + ) + assert response.user.invited_at + + +async def test_sign_out_with_an_valid_access_token(): + credentials = mock_user_credentials() + response = await auth_client_with_session().sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + }, + ) + assert response.session + response = await service_role_api_client().sign_out(response.session.access_token) + + +async def test_sign_out_with_an_invalid_access_token(): + try: + await service_role_api_client().sign_out("this-is-a-bad-token") + assert False + except AuthError: + pass + + +async def test_verify_otp_with_non_existent_phone_number(): + credentials = mock_user_credentials() + otp = mock_verification_otp() + try: + await client_api_auto_confirm_disabled_client().verify_otp( + { + "phone": credentials.get("phone"), + "token": otp, + "type": "sms", + }, + ) + assert False + except AuthError as e: + assert e.message == "User not found" + + +async def test_verify_otp_with_invalid_phone_number(): + credentials = mock_user_credentials() + otp = mock_verification_otp() + try: + await client_api_auto_confirm_disabled_client().verify_otp( + { + "phone": f"{credentials.get('phone')}-invalid", + "token": otp, + "type": "sms", + }, + ) + assert False + except AuthError as e: + assert e.message == "Invalid phone number format" diff --git a/tests/_async/utils.py b/tests/_async/utils.py index 9d102d1b..4263c6e4 100644 --- a/tests/_async/utils.py +++ b/tests/_async/utils.py @@ -37,7 +37,7 @@ def mock_user_credentials( options: OptionalCredentials = {}, ) -> Credentials: fake = Faker() - rand_numbers = str(time()) + rand_numbers = str(int(time())) return { "email": options.get("email") or fake.email(), "phone": options.get("phone") or f"1{rand_numbers[-11:]}", diff --git a/tests/_sync/test_gotrue_admin_api.py b/tests/_sync/test_gotrue_admin_api.py index 413e891e..34cd9580 100644 --- a/tests/_sync/test_gotrue_admin_api.py +++ b/tests/_sync/test_gotrue_admin_api.py @@ -1,9 +1,17 @@ -from .clients import service_role_api_client +from gotrue.errors import AuthError + +from .clients import ( + auth_client_with_session, + client_api_auto_confirm_disabled_client, + client_api_auto_confirm_off_signups_enabled_client, + service_role_api_client, +) from .utils import ( create_new_user_with_email, mock_app_metadata, mock_user_credentials, mock_user_metadata, + mock_verification_otp, ) @@ -59,3 +67,207 @@ def test_create_user_with_user_and_app_metadata(): assert "profile_image" in response.user.user_metadata assert "provider" in response.user.app_metadata assert "providers" in response.user.app_metadata + + +def test_list_users_should_return_registered_users(): + credentials = mock_user_credentials() + create_new_user_with_email(email=credentials.get("email")) + users = service_role_api_client().list_users() + assert users + emails = [user.email for user in users] + assert emails + assert credentials.get("email") in emails + + +def test_get_user_fetches_a_user_by_their_access_token(): + credentials = mock_user_credentials() + auth_client_with_session_current_user = auth_client_with_session() + response = auth_client_with_session_current_user.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert response.session + response = auth_client_with_session_current_user.get_user() + assert response.user.email == credentials.get("email") + + +def test_get_user_by_id_should_a_registered_user_given_its_user_identifier(): + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + assert user.id + response = service_role_api_client().get_user_by_id(user.id) + assert response.user.email == credentials.get("email") + + +def test_modify_email_using_update_user_by_id(): + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + response = service_role_api_client().update_user_by_id( + user.id, + { + "email": f"new_{user.email}", + }, + ) + assert response.user.email == f"new_{user.email}" + + +def test_modify_user_metadata_using_update_user_by_id(): + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + user_metadata = {"favorite_color": "yellow"} + response = service_role_api_client().update_user_by_id( + user.id, + { + "user_metadata": user_metadata, + }, + ) + assert response.user.email == user.email + assert response.user.user_metadata == user_metadata + + +def test_modify_app_metadata_using_update_user_by_id(): + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + app_metadata = {"roles": ["admin", "publisher"]} + response = service_role_api_client().update_user_by_id( + user.id, + { + "app_metadata": app_metadata, + }, + ) + assert response.user.email == user.email + assert "roles" in response.user.app_metadata + + +def test_modify_confirm_email_using_update_user_by_id(): + credentials = mock_user_credentials() + response = client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert response.user + assert not response.user.email_confirmed_at + response = service_role_api_client().update_user_by_id( + response.user.id, + { + "email_confirm": True, + }, + ) + assert response.user.email_confirmed_at + + +def test_delete_user_should_be_able_delete_an_existing_user(): + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + service_role_api_client().delete_user(user.id) + users = service_role_api_client().list_users() + emails = [user.email for user in users] + assert credentials.get("email") not in emails + + +def test_generate_link_supports_sign_up_with_generate_confirmation_signup_link(): + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + user_metadata = {"status": "alpha"} + response = service_role_api_client().generate_link( + { + "type": "signup", + "email": credentials.get("email"), + "password": credentials.get("password"), + "options": { + "data": user_metadata, + "redirect_to": redirect_to, + }, + }, + ) + assert response.user.user_metadata == user_metadata + + +def test_generate_link_supports_updating_emails_with_generate_email_change_links(): # noqa: E501 + credentials = mock_user_credentials() + user = create_new_user_with_email(email=credentials.get("email")) + assert user.email + assert user.email == credentials.get("email") + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + response = service_role_api_client().generate_link( + { + "type": "email_change_current", + "email": user.email, + "new_email": credentials.get("email"), + "options": { + "redirect_to": redirect_to, + }, + }, + ) + assert response.user.new_email == credentials.get("email") + + +def test_invite_user_by_email_creates_a_new_user_with_an_invited_at_timestamp(): + credentials = mock_user_credentials() + redirect_to = "http://localhost:9999/welcome" + user_metadata = {"status": "alpha"} + response = service_role_api_client().invite_user_by_email( + credentials.get("email"), + { + "data": user_metadata, + "redirect_to": redirect_to, + }, + ) + assert response.user.invited_at + + +def test_sign_out_with_an_valid_access_token(): + credentials = mock_user_credentials() + response = auth_client_with_session().sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + }, + ) + assert response.session + response = service_role_api_client().sign_out(response.session.access_token) + + +def test_sign_out_with_an_invalid_access_token(): + try: + service_role_api_client().sign_out("this-is-a-bad-token") + assert False + except AuthError: + pass + + +def test_verify_otp_with_non_existent_phone_number(): + credentials = mock_user_credentials() + otp = mock_verification_otp() + try: + client_api_auto_confirm_disabled_client().verify_otp( + { + "phone": credentials.get("phone"), + "token": otp, + "type": "sms", + }, + ) + assert False + except AuthError as e: + assert e.message == "User not found" + + +def test_verify_otp_with_invalid_phone_number(): + credentials = mock_user_credentials() + otp = mock_verification_otp() + try: + client_api_auto_confirm_disabled_client().verify_otp( + { + "phone": f"{credentials.get('phone')}-invalid", + "token": otp, + "type": "sms", + }, + ) + assert False + except AuthError as e: + assert e.message == "Invalid phone number format" diff --git a/tests/_sync/utils.py b/tests/_sync/utils.py index 024cd6eb..e85c56fd 100644 --- a/tests/_sync/utils.py +++ b/tests/_sync/utils.py @@ -37,7 +37,7 @@ def mock_user_credentials( options: OptionalCredentials = {}, ) -> Credentials: fake = Faker() - rand_numbers = str(time()) + rand_numbers = str(int(time())) return { "email": options.get("email") or fake.email(), "phone": options.get("phone") or f"1{rand_numbers[-11:]}", From fb033e44208be7f3ae611eefd83d48d379909c44 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 14:33:19 +0800 Subject: [PATCH 28/35] fix: patch merge conflicts on next --- .github/workflows/ci.yml | 4 +- .github/workflows/manual_pypi_publish.yml | 2 +- docs/source/api/api.rst | 5 + docs/source/api/client.rst | 9 + docs/source/api/index.rst | 14 + docs/source/conf.py | 14 +- docs/source/examples/index.rst | 14 + docs/source/gotrue.rst | 53 + docs/source/index.rst | 5 +- docs/source/modules.rst | 7 + docs/source/usage/admin.rst | 19 - docs/source/usage/client.rst | 6 - gotrue/_async/api.py | 642 ++++++++ gotrue/_async/client.py | 648 +++++++++ gotrue/_sync/api.py | 642 ++++++++ gotrue/_sync/client.py | 640 ++++++++ poetry.lock | 1609 +++++++++++---------- pyproject.toml | 14 +- 18 files changed, 3527 insertions(+), 820 deletions(-) create mode 100644 docs/source/api/api.rst create mode 100644 docs/source/api/client.rst create mode 100644 docs/source/api/index.rst create mode 100644 docs/source/examples/index.rst create mode 100644 docs/source/gotrue.rst create mode 100644 docs/source/modules.rst delete mode 100644 docs/source/usage/admin.rst delete mode 100644 docs/source/usage/client.rst create mode 100644 gotrue/_async/api.py create mode 100644 gotrue/_async/client.py create mode 100644 gotrue/_sync/api.py create mode 100644 gotrue/_sync/client.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e89cd29f..8ed573e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Set up Poetry - uses: abatilo/actions-poetry@v2.1.6 + uses: abatilo/actions-poetry@v2.2.0 with: - poetry-version: 1.2.2 + poetry-version: 1.3.2 - name: Run Tests run: make run_tests - name: Upload Coverage diff --git a/.github/workflows/manual_pypi_publish.yml b/.github/workflows/manual_pypi_publish.yml index e2e97a19..2e010b15 100644 --- a/.github/workflows/manual_pypi_publish.yml +++ b/.github/workflows/manual_pypi_publish.yml @@ -21,7 +21,7 @@ jobs: with: python-version: "3.10" - name: Set up Poetry - uses: abatilo/actions-poetry@v2.1.6 + uses: abatilo/actions-poetry@v2.2.0 with: poetry-version: 1.1.13 - name: Install dependencies diff --git a/docs/source/api/api.rst b/docs/source/api/api.rst new file mode 100644 index 00000000..2f2a673d --- /dev/null +++ b/docs/source/api/api.rst @@ -0,0 +1,5 @@ +API +====== + +.. autoclass:: gotrue._async.api.AsyncGoTrueAPI + :inherited-members: diff --git a/docs/source/api/client.rst b/docs/source/api/client.rst new file mode 100644 index 00000000..8ca9e744 --- /dev/null +++ b/docs/source/api/client.rst @@ -0,0 +1,9 @@ +Client +====== + +The entrypoint to the library is the Client class. To interact with the Gotrue API, you make an instance of this class. + + +.. autoclass:: gotrue.AsyncGoTrueClient + :members: + :inherited-members: diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 00000000..00a4f835 --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,14 @@ +API Reference +============= + +.. note:: + The library offers both synchronous and asynchronous clients. + Note that the synchronous and asynchronous classes all provide the exact same interface. + Only the async client and it's methods are documented here. + +.. toctree:: + :maxdepth: 2 + :caption: API Reference: + + Client + API diff --git a/docs/source/conf.py b/docs/source/conf.py index 7281ab77..78ccf251 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,15 +17,21 @@ # -- Project information ----------------------------------------------------- project = "gotrue" -copyright = "2021, Joel Lee" -author = "Joel Lee" +copyright = ( + "2022, Anand Krishna, Daniel Reinón García, Joel Lee, Leynier Gutiérrez González" +) +author = "Anand Krishna, Daniel Reinón García, Joel Lee, Leynier Gutiérrez González" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.extlinks", +] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -33,7 +39,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst new file mode 100644 index 00000000..d3c48433 --- /dev/null +++ b/docs/source/examples/index.rst @@ -0,0 +1,14 @@ +Examples(WIP) +============= + +.. note:: + The library offers both synchronous and asynchronous clients. + Note that the synchronous and asynchronous classes all provide the exact same interface. + Only the async client and it's methods are documented here. + +.. toctree:: + :maxdepth: 2 + :caption: API Reference: + + API + Client diff --git a/docs/source/gotrue.rst b/docs/source/gotrue.rst new file mode 100644 index 00000000..2efeaea4 --- /dev/null +++ b/docs/source/gotrue.rst @@ -0,0 +1,53 @@ +gotrue package +============== + +Submodules +---------- + +gotrue.constants module +----------------------- + +.. automodule:: gotrue.constants + :members: + :undoc-members: + :show-inheritance: + +gotrue.exceptions module +------------------------ + +.. automodule:: gotrue.exceptions + :members: + :undoc-members: + :show-inheritance: + +gotrue.helpers module +--------------------- + +.. automodule:: gotrue.helpers + :members: + :undoc-members: + :show-inheritance: + +gotrue.http\_clients module +--------------------------- + +.. automodule:: gotrue.http_clients + :members: + :undoc-members: + :show-inheritance: + +gotrue.types module +------------------- + +.. automodule:: gotrue.types + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: gotrue + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst index f5a3aea0..db72b04c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,8 +11,9 @@ Welcome to gotrue's documentation! :maxdepth: 2 :caption: Contents: - usage/client - usage/admin + API Reference + Examples + Miscellaneous Modules diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 00000000..8d733c93 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +gotrue-py +========= + +.. toctree:: + :maxdepth: 4 + + gotrue diff --git a/docs/source/usage/admin.rst b/docs/source/usage/admin.rst deleted file mode 100644 index 48749464..00000000 --- a/docs/source/usage/admin.rst +++ /dev/null @@ -1,19 +0,0 @@ -############# -Admin Documentation -############# -This is supposed to be a paragraph - - -This is yet another paragraph - -* This is a bulleted list - - -This is a normal text paragraph. The next paragraph is a code sample:: - - It is not processed in any way, except - that the indentation is removed. - - It can span multiple lines. - -This is a normal text paragraph again. diff --git a/docs/source/usage/client.rst b/docs/source/usage/client.rst deleted file mode 100644 index 7bbb329a..00000000 --- a/docs/source/usage/client.rst +++ /dev/null @@ -1,6 +0,0 @@ -#################### -Client Documentation -#################### -.. py:function:: sign_up(credentials: description) - - Helps a user to sign up for an account on gotrue given credentials dictionary which contains username and password. diff --git a/gotrue/_async/api.py b/gotrue/_async/api.py new file mode 100644 index 00000000..77fbf84a --- /dev/null +++ b/gotrue/_async/api.py @@ -0,0 +1,642 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Union + +from pydantic import parse_obj_as + +from ..exceptions import APIError +from ..helpers import check_response, encode_uri_component +from ..http_clients import AsyncClient +from ..types import ( + CookieOptions, + LinkType, + Provider, + Session, + User, + UserAttributes, + determine_session_or_user_model_from_response, +) + + +class AsyncGoTrueAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + cookie_options: CookieOptions, + http_client: Optional[AsyncClient] = None, + ) -> None: + """Initialise API class.""" + self.url = url + self.headers = headers + self.cookie_options = cookie_options + self.http_client = http_client or AsyncClient() + + async def __aenter__(self) -> AsyncGoTrueAPI: + return self + + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + await self.close() + + async def close(self) -> None: + await self.http_client.aclose() + + async def create_user(self, *, attributes: UserAttributes) -> User: + """Creates a new user. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Parameters + ---------- + attributes: UserAttributes + The data you want to create the user with. + + Returns + ------- + response : User + The created user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = attributes.dict() + url = f"{self.url}/admin/users" + response = await self.http_client.post(url, json=data, headers=headers) + return User.parse_response(response) + + async def list_users(self) -> List[User]: + """Get a list of users. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Returns + ------- + response : List[User] + A list of users + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + url = f"{self.url}/admin/users" + response = await self.http_client.get(url, headers=headers) + check_response(response) + users = response.json().get("users") + if users is None: + raise APIError("No users found in response", 400) + if not isinstance(users, list): + raise APIError("Expected a list of users", 400) + return parse_obj_as(List[User], users) + + async def sign_up_with_email( + self, + *, + email: str, + password: str, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Creates a new user using their email address. + + Parameters + ---------- + email : str + The email address of the user. + password : str + The password of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "password": password, "data": data} + url = f"{self.url}/signup{query_string}" + response = await self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + async def sign_in_with_email( + self, + *, + email: str, + password: str, + redirect_to: Optional[str] = None, + ) -> Session: + """Logs in an existing user using their email address. + + Parameters + ---------- + email : str + The email address of the user. + password : str + The password of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + + headers = self.headers + query_string = "?grant_type=password" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string += f"&redirect_to={redirect_to_encoded}" + data = {"email": email, "password": password} + url = f"{self.url}/token{query_string}" + response = await self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + async def sign_up_with_phone( + self, + *, + phone: str, + password: str, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Signs up a new user using their phone number and a password. + + Parameters + ---------- + phone : str + The phone number of the user. + password : str + The password of the user. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = {"phone": phone, "password": password, "data": data} + url = f"{self.url}/signup" + response = await self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + async def sign_in_with_phone( + self, + *, + phone: str, + password: str, + ) -> Session: + """Logs in an existing user using their phone number and password. + + Parameters + ---------- + phone : str + The phone number of the user. + password : str + The password of the user. + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + data = {"phone": phone, "password": password} + url = f"{self.url}/token?grant_type=password" + headers = self.headers + response = await self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + async def send_magic_link_email( + self, + *, + email: str, + create_user: bool, + redirect_to: Optional[str] = None, + ) -> None: + """Sends a magic login link to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "create_user": create_user} + url = f"{self.url}/magiclink{query_string}" + response = await self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + async def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: + """Sends a mobile OTP via SMS. Will register the account if it doesn't already exist + + Parameters + ---------- + phone : str + The user's phone number WITH international prefix + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = {"phone": phone, "create_user": create_user} + url = f"{self.url}/otp" + response = await self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + async def verify_mobile_otp( + self, + *, + phone: str, + token: str, + redirect_to: Optional[str] = None, + ) -> Union[Session, User]: + """Send User supplied Mobile OTP to be verified + + Parameters + ---------- + phone : str + The user's phone number WITH international prefix + token : str + Token that user was sent to their mobile phone + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = { + "phone": phone, + "token": token, + "type": "sms", + } + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + data["redirect_to"] = redirect_to_encoded + url = f"{self.url}/verify" + response = await self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + async def invite_user_by_email( + self, + *, + email: str, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> User: + """Sends an invite link to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "data": data} + url = f"{self.url}/invite{query_string}" + response = await self.http_client.post(url, json=data, headers=headers) + return User.parse_response(response) + + async def reset_password_for_email( + self, + *, + email: str, + redirect_to: Optional[str] = None, + ) -> None: + """Sends a reset request to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email} + url = f"{self.url}/recover{query_string}" + response = await self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: + """Create temporary object. + + Create a temporary object with all configured headers and adds the + Authorization token to be used on request methods. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + + Returns + ------- + headers : dict of str + The headers required for a successful request statement with the + supabase backend. + """ + headers = {**self.headers, "Authorization": f"Bearer {jwt}"} + return headers + + async def sign_out(self, *, jwt: str) -> None: + """Removes a logged-in session. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/logout" + await self.http_client.post(url, headers=headers) + + async def get_url_for_provider( + self, + *, + provider: Provider, + redirect_to: Optional[str] = None, + scopes: Optional[str] = None, + ) -> str: + """Generates the relevant login URL for a third-party provider. + + Parameters + ---------- + provider : Provider + One of the providers supported by GoTrue. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + scopes : Optional[str] + A space-separated list of scopes granted to the OAuth application. + + Returns + ------- + url : str + The URL to redirect the user to. + + Raises + ------ + APIError + If an error occurs. + """ + url_params = [f"provider={encode_uri_component(provider)}"] + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + url_params.append(f"redirect_to={redirect_to_encoded}") + if scopes: + url_params.append(f"scopes={encode_uri_component(scopes)}") + return f"{self.url}/authorize?{'&'.join(url_params)}" + + async def get_user(self, *, jwt: str) -> User: + """Gets the user details. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/user" + response = await self.http_client.get(url, headers=headers) + return User.parse_response(response) + + async def update_user( + self, + *, + jwt: str, + attributes: UserAttributes, + ) -> User: + """ + Updates the user data. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + attributes : UserAttributes + The data you want to update. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + data = attributes.dict() + url = f"{self.url}/user" + response = await self.http_client.put(url, json=data, headers=headers) + return User.parse_response(response) + + async def delete_user(self, *, uid: str, jwt: str) -> None: + """Delete a user. Requires a `service_role` key. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Parameters + ---------- + uid : str + The user uid you want to remove. + jwt : str + A valid, logged-in JWT. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/admin/users/{uid}" + response = await self.http_client.delete(url, headers=headers) + return check_response(response) + + async def refresh_access_token(self, *, refresh_token: str) -> Session: + """Generates a new JWT. + + Parameters + ---------- + refresh_token : str + A valid refresh token that was returned on login. + + Returns + ------- + response : Session + A session + + Raises + ------ + APIError + If an error occurs. + """ + data = {"refresh_token": refresh_token} + url = f"{self.url}/token?grant_type=refresh_token" + headers = self.headers + response = await self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + async def generate_link( + self, + *, + type: LinkType, + email: str, + password: Optional[str] = None, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """ + Generates links to be sent via email or other. + + Parameters + ---------- + type : LinkType + The link type ("signup" or "magiclink" or "recovery" or "invite"). + email : str + The user's email. + password : Optional[str] + User password. For signup only. + redirect_to : Optional[str] + The link type ("signup" or "magiclink" or "recovery" or "invite"). + data : Optional[Dict[str, Any]] + Optional user metadata. For signup only. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = { + "type": type, + "email": email, + "data": data, + } + if password: + data["password"] = password + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + data["redirect_to"] = redirect_to_encoded + url = f"{self.url}/admin/generate_link" + response = await self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + async def set_auth_cookie(self, *, req, res): + """Stub for parity with JS api.""" + raise NotImplementedError("set_auth_cookie not implemented.") + + async def get_user_by_cookie(self, *, req): + """Stub for parity with JS api.""" + raise NotImplementedError("get_user_by_cookie not implemented.") diff --git a/gotrue/_async/client.py b/gotrue/_async/client.py new file mode 100644 index 00000000..c72fe99e --- /dev/null +++ b/gotrue/_async/client.py @@ -0,0 +1,648 @@ +from __future__ import annotations + +from functools import partial +from json import dumps, loads +from threading import Timer +from time import time +from typing import Any, Callable, Dict, Optional, Tuple, Union, cast +from urllib.parse import parse_qs, urlparse +from uuid import uuid4 + +from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY +from ..exceptions import APIError +from ..types import ( + AuthChangeEvent, + CookieOptions, + Provider, + Session, + Subscription, + User, + UserAttributes, + UserAttributesDict, +) +from .api import AsyncGoTrueAPI +from .storage import AsyncMemoryStorage, AsyncSupportedStorage + + +class AsyncGoTrueClient: + def __init__( + self, + *, + url: str = GOTRUE_URL, + headers: Dict[str, str] = {}, + auto_refresh_token: bool = True, + persist_session: bool = True, + local_storage: AsyncSupportedStorage = AsyncMemoryStorage(), + cookie_options: CookieOptions = CookieOptions.parse_obj(COOKIE_OPTIONS), + api: Optional[AsyncGoTrueAPI] = None, + replace_default_headers: bool = False, + ) -> None: + """Create a new client + + url : str + The URL of the GoTrue server. + headers : Dict[str, str] + Any additional headers to send to the GoTrue server. + auto_refresh_token : bool + Set to "true" if you want to automatically refresh the token before + expiring. + persist_session : bool + Set to "true" if you want to automatically save the user session + into local storage. + local_storage : SupportedStorage + The storage engine to use for persisting the session. + cookie_options : CookieOptions + The options for the cookie. + """ + if url.startswith("http://"): + print( + "Warning:\n\nDO NOT USE HTTP IN PRODUCTION FOR GOTRUE EVER!\n" + "GoTrue REQUIRES HTTPS to work securely." + ) + self.state_change_emitters: Dict[str, Subscription] = {} + self.refresh_token_timer: Optional[Timer] = None + self.current_user: Optional[User] = None + self.current_session: Optional[Session] = None + self.auto_refresh_token = auto_refresh_token + self.persist_session = persist_session + self.local_storage = local_storage + empty_or_default_headers = {} if replace_default_headers else DEFAULT_HEADERS + args = { + "url": url, + "headers": {**empty_or_default_headers, **headers}, + "cookie_options": cookie_options, + } + self.api = api or AsyncGoTrueAPI(**args) + + async def __aenter__(self) -> AsyncGoTrueClient: + return self + + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + await self.close() + + async def close(self) -> None: + await self.api.close() + + async def init_recover(self) -> None: + """Recover the current session from local storage.""" + await self._recover_session() + await self._recover_and_refresh() + + async def sign_up( + self, + *, + email: Optional[str] = None, + phone: Optional[str] = None, + password: Optional[str] = None, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Creates a new user. If email and phone are provided, email will be + used and phone will be ignored. + + Parameters + --------- + email : Optional[str] + The user's email address. + phone : Optional[str] + The user's phone number. + password : Optional[str] + The user's password. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + await self._remove_session() + + if email and password: + response = await self.api.sign_up_with_email( + email=email, + password=password, + redirect_to=redirect_to, + data=data, + ) + elif phone and password: + response = await self.api.sign_up_with_phone( + phone=phone, password=password, data=data + ) + elif not password: + raise ValueError("Password must be defined, can't be None.") + else: + raise ValueError("Email or phone must be defined, both can't be None.") + + if isinstance(response, Session): + # The user has confirmed their email or the underlying DB doesn't + # require email confirmation. + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + async def sign_in( + self, + *, + email: Optional[str] = None, + phone: Optional[str] = None, + password: Optional[str] = None, + refresh_token: Optional[str] = None, + provider: Optional[Provider] = None, + redirect_to: Optional[str] = None, + scopes: Optional[str] = None, + create_user: bool = False, + ) -> Optional[Union[Session, str]]: + """Log in an existing user, or login via a third-party provider. + If email and phone are provided, email will be used and phone will be ignored. + + Parameters + --------- + email : Optional[str] + The user's email address. + phone : Optional[str] + The user's phone number. + password : Optional[str] + The user's password. + refresh_token : Optional[str] + A valid refresh token that was returned on login. + provider : Optional[Provider] + One of the providers supported by GoTrue. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + scopes : Optional[str] + A space-separated list of scopes granted to the OAuth application. + + Returns + ------- + response : Optional[Union[Session, str]] + If only email are provided between the email and password, + None is returned and send magic link to email + + If email and password are provided, a logged-in session is returned. + + If only phone are provided between the phone and password, + None is returned and send message to phone + + If phone and password are provided, a logged-in session is returned. + + If refresh_token is provided, a logged-in session is returned. + + If provider is provided, an redirect URL is returned. + + Otherwise, error is raised. + + Raises + ------ + APIError + If an error occurs. + """ + await self._remove_session() + if email: + if password: + response = await self._handle_email_sign_in( + email=email, + password=password, + redirect_to=redirect_to, + ) + else: + response = await self.api.send_magic_link_email( + email=email, create_user=create_user + ) + elif phone: + if password: + response = await self._handle_phone_sign_in( + phone=phone, password=password + ) + else: + response = await self.api.send_mobile_otp( + phone=phone, create_user=create_user + ) + elif refresh_token: + # current_session and current_user will be updated to latest + # on _call_refresh_token using the passed refresh_token + await self._call_refresh_token(refresh_token=refresh_token) + response = self.current_session + elif provider: + response = await self._handle_provider_sign_in( + provider=provider, + redirect_to=redirect_to, + scopes=scopes, + ) + else: + raise ValueError( + "Email, phone, refresh_token, or provider must be defined, " + "all can't be None." + ) + return response + + async def verify_otp( + self, + *, + phone: str, + token: str, + redirect_to: Optional[str] = None, + ) -> Union[Session, User]: + """Log in a user given a User supplied OTP received via mobile. + + Parameters + ---------- + phone : str + The user's phone number. + token : str + The user's OTP. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + await self._remove_session() + response = await self.api.verify_mobile_otp( + phone=phone, + token=token, + redirect_to=redirect_to, + ) + if isinstance(response, Session): + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def user(self) -> Optional[User]: + """Returns the user data, if there is a logged in user.""" + return self.current_user + + def session(self) -> Optional[Session]: + """Returns the session data, if there is an active session.""" + return self.current_session + + async def refresh_session(self) -> Session: + """Force refreshes the session. + + Force refreshes the session including the user data incase it was + updated in a different session. + """ + if not self.current_session: + raise ValueError("Not logged in.") + return await self._call_refresh_token() + + async def update( + self, *, attributes: Union[UserAttributesDict, UserAttributes] + ) -> User: + """Updates user data, if there is a logged in user. + + Parameters + ---------- + attributes : UserAttributesDict | UserAttributes + Attributes to update, could be: email, password, email_change_token, data + + Returns + ------- + response : User + The updated user data. + + Raises + ------ + APIError + If an error occurs. + """ + if not self.current_session: + raise ValueError("Not logged in.") + + if isinstance(attributes, dict): + attributes_to_update = UserAttributes(**attributes) + else: + attributes_to_update = attributes + + response = await self.api.update_user( + jwt=self.current_session.access_token, + attributes=attributes_to_update, + ) + self.current_session.user = response + await self._save_session(session=self.current_session) + self._notify_all_subscribers(event=AuthChangeEvent.USER_UPDATED) + return response + + async def set_session(self, *, refresh_token: str) -> Session: + """Sets the session data from refresh_token and returns current Session + + Parameters + ---------- + refresh_token : str + A JWT token + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + response = await self.api.refresh_access_token(refresh_token=refresh_token) + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + async def set_auth(self, *, access_token: str) -> Session: + """Overrides the JWT on the current client. The JWT will then be sent in + all subsequent network requests. + + Parameters + ---------- + access_token : str + A JWT token + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + session = Session( + access_token=access_token, + token_type="bearer", + user=None, + expires_in=None, + expires_at=None, + refresh_token=None, + provider_token=None, + ) + if self.current_session: + session.expires_in = self.current_session.expires_in + session.expires_at = self.current_session.expires_at + session.refresh_token = self.current_session.refresh_token + session.provider_token = self.current_session.provider_token + await self._save_session(session=session) + return session + + async def get_session_from_url( + self, + *, + url: str, + store_session: bool = False, + ) -> Session: + """Gets the session data from a URL string. + + Parameters + ---------- + url : str + The URL string. + store_session : bool + Optionally store the session in the browser + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + data = urlparse(url) + query = parse_qs(data.query) + error_description = query.get("error_description") + access_token = query.get("access_token") + expires_in = query.get("expires_in") + refresh_token = query.get("refresh_token") + token_type = query.get("token_type") + if error_description: + raise APIError(error_description[0], 400) + if not access_token or not access_token[0]: + raise APIError("No access_token detected.", 400) + if not refresh_token or not refresh_token[0]: + raise APIError("No refresh_token detected.", 400) + if not token_type or not token_type[0]: + raise APIError("No token_type detected.", 400) + if not expires_in or not expires_in[0]: + raise APIError("No expires_in detected.", 400) + try: + expires_at = round(time()) + int(expires_in[0]) + except ValueError: + raise APIError("Invalid expires_in.", 400) + response = await self.api.get_user(jwt=access_token[0]) + provider_token = query.get("provider_token") + session = Session( + access_token=access_token[0], + token_type=token_type[0], + user=response, + expires_in=int(expires_in[0]), + expires_at=expires_at, + refresh_token=refresh_token[0], + provider_token=provider_token[0] if provider_token else None, + ) + if store_session: + await self._save_session(session=session) + recovery_mode = query.get("type") + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + if recovery_mode and recovery_mode[0] == "recovery": + self._notify_all_subscribers(event=AuthChangeEvent.PASSWORD_RECOVERY) + return session + + async def sign_out(self) -> None: + """Log the user out.""" + access_token: Optional[str] = None + if self.current_session: + access_token = self.current_session.access_token + await self._remove_session() + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_OUT) + if access_token: + await self.api.sign_out(jwt=access_token) + + def _unsubscribe(self, *, id: str) -> None: + """Unsubscribe from a subscription.""" + self.state_change_emitters.pop(id) + + def on_auth_state_change( + self, + *, + callback: Callable[[AuthChangeEvent, Optional[Session]], None], + ) -> Subscription: + """Receive a notification every time an auth event happens. + + Parameters + ---------- + callback : Callable[[AuthChangeEvent, Optional[Session]], None] + The callback to call when an auth event happens. + + Returns + ------- + subscription : Subscription + A subscription object which can be used to unsubscribe itself. + + Raises + ------ + APIError + If an error occurs. + """ + unique_id = uuid4() + subscription = Subscription( + id=unique_id, + callback=callback, + unsubscribe=partial(self._unsubscribe, id=unique_id.hex), + ) + self.state_change_emitters[unique_id.hex] = subscription + return subscription + + async def _handle_email_sign_in( + self, + *, + email: str, + password: str, + redirect_to: Optional[str], + ) -> Session: + """Sign in with email and password.""" + response = await self.api.sign_in_with_email( + email=email, + password=password, + redirect_to=redirect_to, + ) + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + async def _handle_phone_sign_in(self, *, phone: str, password: str) -> Session: + """Sign in with phone and password.""" + response = await self.api.sign_in_with_phone(phone=phone, password=password) + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + async def _handle_provider_sign_in( + self, + *, + provider: Provider, + redirect_to: Optional[str], + scopes: Optional[str], + ) -> str: + """Sign in with provider.""" + return await self.api.get_url_for_provider( + provider=provider, + redirect_to=redirect_to, + scopes=scopes, + ) + + async def _recover_common(self) -> Optional[Tuple[Session, int, int]]: + """Recover common logic""" + json = await self.local_storage.get_item(STORAGE_KEY) + if not json: + return + data = loads(json) + session_raw = data.get("session") + expires_at_raw = data.get("expires_at") + if ( + expires_at_raw + and isinstance(expires_at_raw, int) + and session_raw + and isinstance(session_raw, dict) + ): + session = Session.parse_obj(session_raw) + expires_at = int(expires_at_raw) + time_now = round(time()) + return session, expires_at, time_now + + async def _recover_session(self) -> None: + """Attempts to get the session from LocalStorage""" + result = await self._recover_common() + if not result: + return + session, expires_at, time_now = result + if expires_at >= time_now: + await self._save_session(session=session) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + + async def _recover_and_refresh(self) -> None: + """Recovers the session from LocalStorage and refreshes""" + result = await self._recover_common() + if not result: + return + session, expires_at, time_now = result + if expires_at < time_now and self.auto_refresh_token and session.refresh_token: + try: + await self._call_refresh_token(refresh_token=session.refresh_token) + except APIError: + await self._remove_session() + elif expires_at < time_now or not session or not session.user: + await self._remove_session() + else: + await self._save_session(session=session) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + + async def _call_refresh_token( + self, *, refresh_token: Optional[str] = None + ) -> Session: + if refresh_token is None: + if self.current_session: + refresh_token = self.current_session.refresh_token + else: + raise ValueError("No current session and refresh_token not supplied.") + response = await self.api.refresh_access_token( + refresh_token=cast(str, refresh_token) + ) + await self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.TOKEN_REFRESHED) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def _notify_all_subscribers(self, *, event: AuthChangeEvent) -> None: + """Notify all subscribers that auth event happened.""" + for value in self.state_change_emitters.values(): + value.callback(event, self.current_session) + + async def _save_session(self, *, session: Session) -> None: + """Save session to client.""" + self.current_session = session + self.current_user = session.user + if session.expires_at: + time_now = round(time()) + expire_in = session.expires_at - time_now + refresh_duration_before_expires = 60 if expire_in > 60 else 0.5 + self._start_auto_refresh_token( + value=(expire_in - refresh_duration_before_expires) * 1000 + ) + if self.persist_session and session.expires_at: + await self._persist_session(session=session) + + async def _persist_session(self, *, session: Session) -> None: + data = {"session": session.dict(), "expires_at": session.expires_at} + await self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str)) + + async def _remove_session(self) -> None: + """Remove the session.""" + self.current_session = None + self.current_user = None + if self.refresh_token_timer: + self.refresh_token_timer.cancel() + await self.local_storage.remove_item(STORAGE_KEY) + + def _start_auto_refresh_token(self, *, value: float) -> None: + if self.refresh_token_timer: + self.refresh_token_timer.cancel() + if value <= 0 or not self.auto_refresh_token: + return + self.refresh_token_timer = Timer(value, self._call_refresh_token) + self.refresh_token_timer.start() diff --git a/gotrue/_sync/api.py b/gotrue/_sync/api.py new file mode 100644 index 00000000..abbdc480 --- /dev/null +++ b/gotrue/_sync/api.py @@ -0,0 +1,642 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Union + +from pydantic import parse_obj_as + +from ..exceptions import APIError +from ..helpers import check_response, encode_uri_component +from ..http_clients import SyncClient +from ..types import ( + CookieOptions, + LinkType, + Provider, + Session, + User, + UserAttributes, + determine_session_or_user_model_from_response, +) + + +class SyncGoTrueAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + cookie_options: CookieOptions, + http_client: Optional[SyncClient] = None, + ) -> None: + """Initialise API class.""" + self.url = url + self.headers = headers + self.cookie_options = cookie_options + self.http_client = http_client or SyncClient() + + def __enter__(self) -> SyncGoTrueAPI: + return self + + def __exit__(self, exc_t, exc_v, exc_tb) -> None: + self.close() + + def close(self) -> None: + self.http_client.aclose() + + def create_user(self, *, attributes: UserAttributes) -> User: + """Creates a new user. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Parameters + ---------- + attributes: UserAttributes + The data you want to create the user with. + + Returns + ------- + response : User + The created user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = attributes.dict() + url = f"{self.url}/admin/users" + response = self.http_client.post(url, json=data, headers=headers) + return User.parse_response(response) + + def list_users(self) -> List[User]: + """Get a list of users. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Returns + ------- + response : List[User] + A list of users + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + url = f"{self.url}/admin/users" + response = self.http_client.get(url, headers=headers) + check_response(response) + users = response.json().get("users") + if users is None: + raise APIError("No users found in response", 400) + if not isinstance(users, list): + raise APIError("Expected a list of users", 400) + return parse_obj_as(List[User], users) + + def sign_up_with_email( + self, + *, + email: str, + password: str, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Creates a new user using their email address. + + Parameters + ---------- + email : str + The email address of the user. + password : str + The password of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "password": password, "data": data} + url = f"{self.url}/signup{query_string}" + response = self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + def sign_in_with_email( + self, + *, + email: str, + password: str, + redirect_to: Optional[str] = None, + ) -> Session: + """Logs in an existing user using their email address. + + Parameters + ---------- + email : str + The email address of the user. + password : str + The password of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + + headers = self.headers + query_string = "?grant_type=password" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string += f"&redirect_to={redirect_to_encoded}" + data = {"email": email, "password": password} + url = f"{self.url}/token{query_string}" + response = self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + def sign_up_with_phone( + self, + *, + phone: str, + password: str, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Signs up a new user using their phone number and a password. + + Parameters + ---------- + phone : str + The phone number of the user. + password : str + The password of the user. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = {"phone": phone, "password": password, "data": data} + url = f"{self.url}/signup" + response = self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + def sign_in_with_phone( + self, + *, + phone: str, + password: str, + ) -> Session: + """Logs in an existing user using their phone number and password. + + Parameters + ---------- + phone : str + The phone number of the user. + password : str + The password of the user. + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + data = {"phone": phone, "password": password} + url = f"{self.url}/token?grant_type=password" + headers = self.headers + response = self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + def send_magic_link_email( + self, + *, + email: str, + create_user: bool, + redirect_to: Optional[str] = None, + ) -> None: + """Sends a magic login link to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "create_user": create_user} + url = f"{self.url}/magiclink{query_string}" + response = self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + def send_mobile_otp(self, *, phone: str, create_user: bool) -> None: + """Sends a mobile OTP via SMS. Will register the account if it doesn't already exist + + Parameters + ---------- + phone : str + The user's phone number WITH international prefix + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = {"phone": phone, "create_user": create_user} + url = f"{self.url}/otp" + response = self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + def verify_mobile_otp( + self, + *, + phone: str, + token: str, + redirect_to: Optional[str] = None, + ) -> Union[Session, User]: + """Send User supplied Mobile OTP to be verified + + Parameters + ---------- + phone : str + The user's phone number WITH international prefix + token : str + Token that user was sent to their mobile phone + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = { + "phone": phone, + "token": token, + "type": "sms", + } + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + data["redirect_to"] = redirect_to_encoded + url = f"{self.url}/verify" + response = self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + def invite_user_by_email( + self, + *, + email: str, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> User: + """Sends an invite link to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email, "data": data} + url = f"{self.url}/invite{query_string}" + response = self.http_client.post(url, json=data, headers=headers) + return User.parse_response(response) + + def reset_password_for_email( + self, + *, + email: str, + redirect_to: Optional[str] = None, + ) -> None: + """Sends a reset request to an email address. + + Parameters + ---------- + email : str + The email address of the user. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + query_string = "" + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + query_string = f"?redirect_to={redirect_to_encoded}" + data = {"email": email} + url = f"{self.url}/recover{query_string}" + response = self.http_client.post(url, json=data, headers=headers) + return check_response(response) + + def _create_request_headers(self, *, jwt: str) -> Dict[str, str]: + """Create temporary object. + + Create a temporary object with all configured headers and adds the + Authorization token to be used on request methods. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + + Returns + ------- + headers : dict of str + The headers required for a successful request statement with the + supabase backend. + """ + headers = {**self.headers, "Authorization": f"Bearer {jwt}"} + return headers + + def sign_out(self, *, jwt: str) -> None: + """Removes a logged-in session. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/logout" + self.http_client.post(url, headers=headers) + + def get_url_for_provider( + self, + *, + provider: Provider, + redirect_to: Optional[str] = None, + scopes: Optional[str] = None, + ) -> str: + """Generates the relevant login URL for a third-party provider. + + Parameters + ---------- + provider : Provider + One of the providers supported by GoTrue. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + scopes : Optional[str] + A space-separated list of scopes granted to the OAuth application. + + Returns + ------- + url : str + The URL to redirect the user to. + + Raises + ------ + APIError + If an error occurs. + """ + url_params = [f"provider={encode_uri_component(provider)}"] + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + url_params.append(f"redirect_to={redirect_to_encoded}") + if scopes: + url_params.append(f"scopes={encode_uri_component(scopes)}") + return f"{self.url}/authorize?{'&'.join(url_params)}" + + def get_user(self, *, jwt: str) -> User: + """Gets the user details. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/user" + response = self.http_client.get(url, headers=headers) + return User.parse_response(response) + + def update_user( + self, + *, + jwt: str, + attributes: UserAttributes, + ) -> User: + """ + Updates the user data. + + Parameters + ---------- + jwt : str + A valid, logged-in JWT. + attributes : UserAttributes + The data you want to update. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + data = attributes.dict() + url = f"{self.url}/user" + response = self.http_client.put(url, json=data, headers=headers) + return User.parse_response(response) + + def delete_user(self, *, uid: str, jwt: str) -> None: + """Delete a user. Requires a `service_role` key. + + This function should only be called on a server. + Never expose your `service_role` key in the browser. + + Parameters + ---------- + uid : str + The user uid you want to remove. + jwt : str + A valid, logged-in JWT. + + Returns + ------- + response : User + A user + + Raises + ------ + APIError + If an error occurs. + """ + headers = self._create_request_headers(jwt=jwt) + url = f"{self.url}/admin/users/{uid}" + response = self.http_client.delete(url, headers=headers) + return check_response(response) + + def refresh_access_token(self, *, refresh_token: str) -> Session: + """Generates a new JWT. + + Parameters + ---------- + refresh_token : str + A valid refresh token that was returned on login. + + Returns + ------- + response : Session + A session + + Raises + ------ + APIError + If an error occurs. + """ + data = {"refresh_token": refresh_token} + url = f"{self.url}/token?grant_type=refresh_token" + headers = self.headers + response = self.http_client.post(url, json=data, headers=headers) + return Session.parse_response(response) + + def generate_link( + self, + *, + type: LinkType, + email: str, + password: Optional[str] = None, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """ + Generates links to be sent via email or other. + + Parameters + ---------- + type : LinkType + The link type ("signup" or "magiclink" or "recovery" or "invite"). + email : str + The user's email. + password : Optional[str] + User password. For signup only. + redirect_to : Optional[str] + The link type ("signup" or "magiclink" or "recovery" or "invite"). + data : Optional[Dict[str, Any]] + Optional user metadata. For signup only. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + headers = self.headers + data = { + "type": type, + "email": email, + "data": data, + } + if password: + data["password"] = password + if redirect_to: + redirect_to_encoded = encode_uri_component(redirect_to) + data["redirect_to"] = redirect_to_encoded + url = f"{self.url}/admin/generate_link" + response = self.http_client.post(url, json=data, headers=headers) + SessionOrUserModel = determine_session_or_user_model_from_response(response) + return SessionOrUserModel.parse_response(response) + + def set_auth_cookie(self, *, req, res): + """Stub for parity with JS api.""" + raise NotImplementedError("set_auth_cookie not implemented.") + + def get_user_by_cookie(self, *, req): + """Stub for parity with JS api.""" + raise NotImplementedError("get_user_by_cookie not implemented.") diff --git a/gotrue/_sync/client.py b/gotrue/_sync/client.py new file mode 100644 index 00000000..3ce6b2de --- /dev/null +++ b/gotrue/_sync/client.py @@ -0,0 +1,640 @@ +from __future__ import annotations + +from functools import partial +from json import dumps, loads +from threading import Timer +from time import time +from typing import Any, Callable, Dict, Optional, Tuple, Union, cast +from urllib.parse import parse_qs, urlparse +from uuid import uuid4 + +from ..constants import COOKIE_OPTIONS, DEFAULT_HEADERS, GOTRUE_URL, STORAGE_KEY +from ..exceptions import APIError +from ..types import ( + AuthChangeEvent, + CookieOptions, + Provider, + Session, + Subscription, + User, + UserAttributes, + UserAttributesDict, +) +from .api import SyncGoTrueAPI +from .storage import SyncMemoryStorage, SyncSupportedStorage + + +class SyncGoTrueClient: + def __init__( + self, + *, + url: str = GOTRUE_URL, + headers: Dict[str, str] = {}, + auto_refresh_token: bool = True, + persist_session: bool = True, + local_storage: SyncSupportedStorage = SyncMemoryStorage(), + cookie_options: CookieOptions = CookieOptions.parse_obj(COOKIE_OPTIONS), + api: Optional[SyncGoTrueAPI] = None, + replace_default_headers: bool = False, + ) -> None: + """Create a new client + + url : str + The URL of the GoTrue server. + headers : Dict[str, str] + Any additional headers to send to the GoTrue server. + auto_refresh_token : bool + Set to "true" if you want to automatically refresh the token before + expiring. + persist_session : bool + Set to "true" if you want to automatically save the user session + into local storage. + local_storage : SupportedStorage + The storage engine to use for persisting the session. + cookie_options : CookieOptions + The options for the cookie. + """ + if url.startswith("http://"): + print( + "Warning:\n\nDO NOT USE HTTP IN PRODUCTION FOR GOTRUE EVER!\n" + "GoTrue REQUIRES HTTPS to work securely." + ) + self.state_change_emitters: Dict[str, Subscription] = {} + self.refresh_token_timer: Optional[Timer] = None + self.current_user: Optional[User] = None + self.current_session: Optional[Session] = None + self.auto_refresh_token = auto_refresh_token + self.persist_session = persist_session + self.local_storage = local_storage + empty_or_default_headers = {} if replace_default_headers else DEFAULT_HEADERS + args = { + "url": url, + "headers": {**empty_or_default_headers, **headers}, + "cookie_options": cookie_options, + } + self.api = api or SyncGoTrueAPI(**args) + + def __enter__(self) -> SyncGoTrueClient: + return self + + def __exit__(self, exc_t, exc_v, exc_tb) -> None: + self.close() + + def close(self) -> None: + self.api.close() + + def init_recover(self) -> None: + """Recover the current session from local storage.""" + self._recover_session() + self._recover_and_refresh() + + def sign_up( + self, + *, + email: Optional[str] = None, + phone: Optional[str] = None, + password: Optional[str] = None, + redirect_to: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ) -> Union[Session, User]: + """Creates a new user. If email and phone are provided, email will be + used and phone will be ignored. + + Parameters + --------- + email : Optional[str] + The user's email address. + phone : Optional[str] + The user's phone number. + password : Optional[str] + The user's password. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + data : Optional[Dict[str, Any]] + Optional user metadata. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + self._remove_session() + + if email and password: + response = self.api.sign_up_with_email( + email=email, + password=password, + redirect_to=redirect_to, + data=data, + ) + elif phone and password: + response = self.api.sign_up_with_phone( + phone=phone, password=password, data=data + ) + elif not password: + raise ValueError("Password must be defined, can't be None.") + else: + raise ValueError("Email or phone must be defined, both can't be None.") + + if isinstance(response, Session): + # The user has confirmed their email or the underlying DB doesn't + # require email confirmation. + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def sign_in( + self, + *, + email: Optional[str] = None, + phone: Optional[str] = None, + password: Optional[str] = None, + refresh_token: Optional[str] = None, + provider: Optional[Provider] = None, + redirect_to: Optional[str] = None, + scopes: Optional[str] = None, + create_user: bool = False, + ) -> Optional[Union[Session, str]]: + """Log in an existing user, or login via a third-party provider. + If email and phone are provided, email will be used and phone will be ignored. + + Parameters + --------- + email : Optional[str] + The user's email address. + phone : Optional[str] + The user's phone number. + password : Optional[str] + The user's password. + refresh_token : Optional[str] + A valid refresh token that was returned on login. + provider : Optional[Provider] + One of the providers supported by GoTrue. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + scopes : Optional[str] + A space-separated list of scopes granted to the OAuth application. + + Returns + ------- + response : Optional[Union[Session, str]] + If only email are provided between the email and password, + None is returned and send magic link to email + + If email and password are provided, a logged-in session is returned. + + If only phone are provided between the phone and password, + None is returned and send message to phone + + If phone and password are provided, a logged-in session is returned. + + If refresh_token is provided, a logged-in session is returned. + + If provider is provided, an redirect URL is returned. + + Otherwise, error is raised. + + Raises + ------ + APIError + If an error occurs. + """ + self._remove_session() + if email: + if password: + response = self._handle_email_sign_in( + email=email, + password=password, + redirect_to=redirect_to, + ) + else: + response = self.api.send_magic_link_email( + email=email, create_user=create_user + ) + elif phone: + if password: + response = self._handle_phone_sign_in(phone=phone, password=password) + else: + response = self.api.send_mobile_otp( + phone=phone, create_user=create_user + ) + elif refresh_token: + # current_session and current_user will be updated to latest + # on _call_refresh_token using the passed refresh_token + self._call_refresh_token(refresh_token=refresh_token) + response = self.current_session + elif provider: + response = self._handle_provider_sign_in( + provider=provider, + redirect_to=redirect_to, + scopes=scopes, + ) + else: + raise ValueError( + "Email, phone, refresh_token, or provider must be defined, " + "all can't be None." + ) + return response + + def verify_otp( + self, + *, + phone: str, + token: str, + redirect_to: Optional[str] = None, + ) -> Union[Session, User]: + """Log in a user given a User supplied OTP received via mobile. + + Parameters + ---------- + phone : str + The user's phone number. + token : str + The user's OTP. + redirect_to : Optional[str] + A URL or mobile address to send the user to after they are confirmed. + + Returns + ------- + response : Union[Session, User] + A logged-in session if the server has "autoconfirm" ON + A user if the server has "autoconfirm" OFF + + Raises + ------ + APIError + If an error occurs. + """ + self._remove_session() + response = self.api.verify_mobile_otp( + phone=phone, + token=token, + redirect_to=redirect_to, + ) + if isinstance(response, Session): + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def user(self) -> Optional[User]: + """Returns the user data, if there is a logged in user.""" + return self.current_user + + def session(self) -> Optional[Session]: + """Returns the session data, if there is an active session.""" + return self.current_session + + def refresh_session(self) -> Session: + """Force refreshes the session. + + Force refreshes the session including the user data incase it was + updated in a different session. + """ + if not self.current_session: + raise ValueError("Not logged in.") + return self._call_refresh_token() + + def update(self, *, attributes: Union[UserAttributesDict, UserAttributes]) -> User: + """Updates user data, if there is a logged in user. + + Parameters + ---------- + attributes : UserAttributesDict | UserAttributes + Attributes to update, could be: email, password, email_change_token, data + + Returns + ------- + response : User + The updated user data. + + Raises + ------ + APIError + If an error occurs. + """ + if not self.current_session: + raise ValueError("Not logged in.") + + if isinstance(attributes, dict): + attributes_to_update = UserAttributes(**attributes) + else: + attributes_to_update = attributes + + response = self.api.update_user( + jwt=self.current_session.access_token, + attributes=attributes_to_update, + ) + self.current_session.user = response + self._save_session(session=self.current_session) + self._notify_all_subscribers(event=AuthChangeEvent.USER_UPDATED) + return response + + def set_session(self, *, refresh_token: str) -> Session: + """Sets the session data from refresh_token and returns current Session + + Parameters + ---------- + refresh_token : str + A JWT token + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + response = self.api.refresh_access_token(refresh_token=refresh_token) + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def set_auth(self, *, access_token: str) -> Session: + """Overrides the JWT on the current client. The JWT will then be sent in + all subsequent network requests. + + Parameters + ---------- + access_token : str + A JWT token + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + session = Session( + access_token=access_token, + token_type="bearer", + user=None, + expires_in=None, + expires_at=None, + refresh_token=None, + provider_token=None, + ) + if self.current_session: + session.expires_in = self.current_session.expires_in + session.expires_at = self.current_session.expires_at + session.refresh_token = self.current_session.refresh_token + session.provider_token = self.current_session.provider_token + self._save_session(session=session) + return session + + def get_session_from_url( + self, + *, + url: str, + store_session: bool = False, + ) -> Session: + """Gets the session data from a URL string. + + Parameters + ---------- + url : str + The URL string. + store_session : bool + Optionally store the session in the browser + + Returns + ------- + response : Session + A logged-in session + + Raises + ------ + APIError + If an error occurs. + """ + data = urlparse(url) + query = parse_qs(data.query) + error_description = query.get("error_description") + access_token = query.get("access_token") + expires_in = query.get("expires_in") + refresh_token = query.get("refresh_token") + token_type = query.get("token_type") + if error_description: + raise APIError(error_description[0], 400) + if not access_token or not access_token[0]: + raise APIError("No access_token detected.", 400) + if not refresh_token or not refresh_token[0]: + raise APIError("No refresh_token detected.", 400) + if not token_type or not token_type[0]: + raise APIError("No token_type detected.", 400) + if not expires_in or not expires_in[0]: + raise APIError("No expires_in detected.", 400) + try: + expires_at = round(time()) + int(expires_in[0]) + except ValueError: + raise APIError("Invalid expires_in.", 400) + response = self.api.get_user(jwt=access_token[0]) + provider_token = query.get("provider_token") + session = Session( + access_token=access_token[0], + token_type=token_type[0], + user=response, + expires_in=int(expires_in[0]), + expires_at=expires_at, + refresh_token=refresh_token[0], + provider_token=provider_token[0] if provider_token else None, + ) + if store_session: + self._save_session(session=session) + recovery_mode = query.get("type") + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + if recovery_mode and recovery_mode[0] == "recovery": + self._notify_all_subscribers(event=AuthChangeEvent.PASSWORD_RECOVERY) + return session + + def sign_out(self) -> None: + """Log the user out.""" + access_token: Optional[str] = None + if self.current_session: + access_token = self.current_session.access_token + self._remove_session() + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_OUT) + if access_token: + self.api.sign_out(jwt=access_token) + + def _unsubscribe(self, *, id: str) -> None: + """Unsubscribe from a subscription.""" + self.state_change_emitters.pop(id) + + def on_auth_state_change( + self, + *, + callback: Callable[[AuthChangeEvent, Optional[Session]], None], + ) -> Subscription: + """Receive a notification every time an auth event happens. + + Parameters + ---------- + callback : Callable[[AuthChangeEvent, Optional[Session]], None] + The callback to call when an auth event happens. + + Returns + ------- + subscription : Subscription + A subscription object which can be used to unsubscribe itself. + + Raises + ------ + APIError + If an error occurs. + """ + unique_id = uuid4() + subscription = Subscription( + id=unique_id, + callback=callback, + unsubscribe=partial(self._unsubscribe, id=unique_id.hex), + ) + self.state_change_emitters[unique_id.hex] = subscription + return subscription + + def _handle_email_sign_in( + self, + *, + email: str, + password: str, + redirect_to: Optional[str], + ) -> Session: + """Sign in with email and password.""" + response = self.api.sign_in_with_email( + email=email, + password=password, + redirect_to=redirect_to, + ) + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def _handle_phone_sign_in(self, *, phone: str, password: str) -> Session: + """Sign in with phone and password.""" + response = self.api.sign_in_with_phone(phone=phone, password=password) + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def _handle_provider_sign_in( + self, + *, + provider: Provider, + redirect_to: Optional[str], + scopes: Optional[str], + ) -> str: + """Sign in with provider.""" + return self.api.get_url_for_provider( + provider=provider, + redirect_to=redirect_to, + scopes=scopes, + ) + + def _recover_common(self) -> Optional[Tuple[Session, int, int]]: + """Recover common logic""" + json = self.local_storage.get_item(STORAGE_KEY) + if not json: + return + data = loads(json) + session_raw = data.get("session") + expires_at_raw = data.get("expires_at") + if ( + expires_at_raw + and isinstance(expires_at_raw, int) + and session_raw + and isinstance(session_raw, dict) + ): + session = Session.parse_obj(session_raw) + expires_at = int(expires_at_raw) + time_now = round(time()) + return session, expires_at, time_now + + def _recover_session(self) -> None: + """Attempts to get the session from LocalStorage""" + result = self._recover_common() + if not result: + return + session, expires_at, time_now = result + if expires_at >= time_now: + self._save_session(session=session) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + + def _recover_and_refresh(self) -> None: + """Recovers the session from LocalStorage and refreshes""" + result = self._recover_common() + if not result: + return + session, expires_at, time_now = result + if expires_at < time_now and self.auto_refresh_token and session.refresh_token: + try: + self._call_refresh_token(refresh_token=session.refresh_token) + except APIError: + self._remove_session() + elif expires_at < time_now or not session or not session.user: + self._remove_session() + else: + self._save_session(session=session) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + + def _call_refresh_token(self, *, refresh_token: Optional[str] = None) -> Session: + if refresh_token is None: + if self.current_session: + refresh_token = self.current_session.refresh_token + else: + raise ValueError("No current session and refresh_token not supplied.") + response = self.api.refresh_access_token(refresh_token=cast(str, refresh_token)) + self._save_session(session=response) + self._notify_all_subscribers(event=AuthChangeEvent.TOKEN_REFRESHED) + self._notify_all_subscribers(event=AuthChangeEvent.SIGNED_IN) + return response + + def _notify_all_subscribers(self, *, event: AuthChangeEvent) -> None: + """Notify all subscribers that auth event happened.""" + for value in self.state_change_emitters.values(): + value.callback(event, self.current_session) + + def _save_session(self, *, session: Session) -> None: + """Save session to client.""" + self.current_session = session + self.current_user = session.user + if session.expires_at: + time_now = round(time()) + expire_in = session.expires_at - time_now + refresh_duration_before_expires = 60 if expire_in > 60 else 0.5 + self._start_auto_refresh_token( + value=(expire_in - refresh_duration_before_expires) * 1000 + ) + if self.persist_session and session.expires_at: + self._persist_session(session=session) + + def _persist_session(self, *, session: Session) -> None: + data = {"session": session.dict(), "expires_at": session.expires_at} + self.local_storage.set_item(STORAGE_KEY, dumps(data, default=str)) + + def _remove_session(self) -> None: + """Remove the session.""" + self.current_session = None + self.current_user = None + if self.refresh_token_timer: + self.refresh_token_timer.cancel() + self.local_storage.remove_item(STORAGE_KEY) + + def _start_auto_refresh_token(self, *, value: float) -> None: + if self.refresh_token_timer: + self.refresh_token_timer.cancel() + if value <= 0 or not self.auto_refresh_token: + return + self.refresh_token_timer = Timer(value, self._call_refresh_token) + self.refresh_token_timer.start() diff --git a/poetry.lock b/poetry.lock index 69a8632c..37dc8240 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,16 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "anyio" -version = "3.6.1" +version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false python-versions = ">=3.6.2" +files = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] [package.dependencies] idna = ">=2.8" @@ -14,29 +20,48 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +trio = ["trio (>=0.16,<0.22)"] [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "black" -version = "22.10.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] [package.dependencies] click = ">=8.0.0" @@ -55,11 +80,15 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "5.0.1" +version = "6.0.0" description = "An easy safelist-based HTML-sanitizing tool." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"}, + {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"}, +] [package.dependencies] six = ">=1.9.0" @@ -67,15 +96,18 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] [[package]] name = "cffi" @@ -84,6 +116,72 @@ description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] [package.dependencies] pycparser = "*" @@ -95,17 +193,108 @@ description = "Validate configuration and produce human readable error messages. category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.0.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = "*" +files = [ + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +] [[package]] name = "click" @@ -114,6 +303,10 @@ description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -126,25 +319,86 @@ description = "Logging integration for Click" category = "dev" optional = false python-versions = "*" +files = [ + {file = "click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975"}, + {file = "click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756"}, +] [package.dependencies] click = "*" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "coverage" -version = "6.5.0" +version = "7.1.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, + {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, + {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, + {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, + {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, + {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, + {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, + {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, + {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, + {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, + {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, + {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, + {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, + {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, +] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} @@ -154,30 +408,59 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "38.0.1" +version = "39.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, + {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, + {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, + {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, + {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, + {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, + {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, + {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, + {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, +] [package.dependencies] cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +pep8test = ["black", "ruff"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] -name = "Deprecated" +name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] [package.dependencies] wrapt = ">=1.10,<2" @@ -192,6 +475,10 @@ description = "Distribution utilities" category = "dev" optional = false python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] [[package]] name = "docutils" @@ -200,6 +487,10 @@ description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, + {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, +] [[package]] name = "dotty-dict" @@ -208,14 +499,37 @@ description = "Dictionary wrapper for quick access to deeply nested keys." category = "dev" optional = false python-versions = ">=3.5,<4.0" +files = [ + {file = "dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f"}, + {file = "dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] + +[package.extras] +test = ["pytest (>=6)"] [[package]] -name = "Faker" -version = "15.1.1" +name = "faker" +version = "16.6.1" description = "Faker is a Python package that generates fake data for you." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "Faker-16.6.1-py3-none-any.whl", hash = "sha256:2375d0bbaf405dc4f1cbc771485a78ad952c776798e5c228eef3e7b337f78868"}, + {file = "Faker-16.6.1.tar.gz", hash = "sha256:b76e5d2405470e3d38d37d1bfaa9d9bbf171bdf41c814f5bbd8117b121f6bccb"}, +] [package.dependencies] python-dateutil = ">=2.4" @@ -223,15 +537,19 @@ typing-extensions = {version = ">=3.10.0.1", markers = "python_version < \"3.8\" [[package]] name = "filelock" -version = "3.8.0" +version = "3.9.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] [package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -240,6 +558,10 @@ description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] [package.dependencies] importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} @@ -254,28 +576,40 @@ description = "A backport of fstrings to python<3.6" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63"}, + {file = "future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089"}, +] [package.extras] rewrite = ["tokenize-rt (>=3)"] [[package]] name = "gitdb" -version = "4.0.9" +version = "4.0.10" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] [package.dependencies] smmap = ">=3.0.1,<6" [[package]] -name = "GitPython" -version = "3.1.29" +name = "gitpython" +version = "3.1.30" description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, + {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, +] [package.dependencies] gitdb = ">=4.0.1,<5" @@ -283,24 +617,35 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "h11" -version = "0.12.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "httpcore" -version = "0.15.0" +version = "0.16.3" description = "A minimal low-level HTTP client." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] [package.dependencies] -anyio = ">=3.0.0,<4.0.0" +anyio = ">=3.0,<5.0" certifi = "*" -h11 = ">=0.11,<0.13" +h11 = ">=0.13,<0.15" sniffio = ">=1.0.0,<2.0.0" [package.extras] @@ -309,15 +654,19 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" -version = "0.23.0" +version = "0.23.3" description = "The next generation HTTP client." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.16.0" +httpcore = ">=0.15.0,<0.17.0" rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" @@ -329,11 +678,15 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.5.6" +version = "2.5.16" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "identify-2.5.16-py2.py3-none-any.whl", hash = "sha256:832832a58ecc1b8f33d5e8cb4f7d3db2f5c7fbe922dfee5f958b48fed691501a"}, + {file = "identify-2.5.16.tar.gz", hash = "sha256:c47acedfe6495b1c603ed7e93469b26e839cab38db4155113f36f718f8b3dc47"}, +] [package.extras] license = ["ukkonen"] @@ -345,6 +698,10 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "importlib-metadata" @@ -353,6 +710,10 @@ description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -364,11 +725,15 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] name = "invoke" @@ -377,28 +742,40 @@ description = "Pythonic task execution" category = "dev" optional = false python-versions = "*" +files = [ + {file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"}, + {file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"}, +] [[package]] name = "isort" -version = "5.10.1" +version = "5.11.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.7.0" +files = [ + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, +] [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] -name = "jaraco.classes" +name = "jaraco-classes" version = "3.2.3" description = "Utility functions for Python class constructs" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, + {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, +] [package.dependencies] more-itertools = "*" @@ -414,6 +791,10 @@ description = "Low-level, pure Python DBus protocol wrapper." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] [package.extras] test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] @@ -426,6 +807,10 @@ description = "Store and access your passwords safely." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, + {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, +] [package.dependencies] importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} @@ -445,14 +830,22 @@ description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] [[package]] name = "more-itertools" -version = "8.14.0" +version = "9.0.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, + {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, +] [[package]] name = "mypy-extensions" @@ -461,6 +854,10 @@ description = "Experimental type system extensions for programs checked with the category = "dev" optional = false python-versions = "*" +files = [ + {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"}, +] [[package]] name = "networkx" @@ -469,6 +866,10 @@ description = "Python package for creating and manipulating graphs and networks" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef"}, + {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, +] [package.extras] default = ["matplotlib (>=3.3)", "numpy (>=1.19)", "pandas (>=1.1)", "scipy (>=1.5,!=1.6.1)"] @@ -484,51 +885,71 @@ description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] [package.dependencies] setuptools = "*" [[package]] name = "packaging" -version = "21.3" +version = "23.0" 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" +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pathspec" -version = "0.10.1" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] [[package]] name = "pkginfo" -version = "1.8.3" -description = "Query metadatdata from sdists / bdists / installed packages." +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] [package.extras] -testing = ["coverage", "nose"] +testing = ["pytest", "pytest-cov"] [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} [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)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -537,6 +958,10 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -547,11 +972,15 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.20.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] cfgv = ">=2.0.0" @@ -559,16 +988,7 @@ identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" - -[[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.*" +virtualenv = ">=20.10.0" [[package]] name = "pycodestyle" @@ -577,6 +997,10 @@ description = "Python style guide checker" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] [[package]] name = "pycparser" @@ -585,17 +1009,59 @@ description = "C parser in Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] [[package]] name = "pydantic" -version = "1.10.2" +version = "1.10.4" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"}, + {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"}, + {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"}, + {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"}, + {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"}, + {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"}, + {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"}, + {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"}, + {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"}, + {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"}, + {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"}, + {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"}, + {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"}, + {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"}, + {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"}, + {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"}, + {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"}, + {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"}, + {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"}, + {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"}, + {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"}, + {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"}, + {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"}, + {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"}, + {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"}, + {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"}, + {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"}, + {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"}, + {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"}, + {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"}, + {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"}, + {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"}, + {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"}, + {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"}, + {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"}, + {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"}, +] [package.dependencies] -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.2.0" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] @@ -608,18 +1074,26 @@ description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] [[package]] -name = "PyGithub" -version = "1.56" +name = "pygithub" +version = "1.57" description = "Use the full Github API v3" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "PyGithub-1.57-py3-none-any.whl", hash = "sha256:5822febeac2391f1306c55a99af2bc8f86c8bf82ded000030cd02c18f31b731f"}, + {file = "PyGithub-1.57.tar.gz", hash = "sha256:c273f252b278fb81f1769505cc6921bdb6791e1cebd6ac850cc97dad13c31ff3"}, +] [package.dependencies] deprecated = "*" -pyjwt = ">=2.0" +pyjwt = ">=2.4.0" pynacl = ">=1.4.0" requests = ">=2.14.0" @@ -627,90 +1101,108 @@ requests = ">=2.14.0" integrations = ["cryptography"] [[package]] -name = "Pygments" -version = "2.13.0" +name = "pygments" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] [package.extras] plugins = ["importlib-metadata"] [[package]] -name = "PyJWT" -version = "2.5.0" +name = "pyjwt" +version = "2.6.0" description = "JSON Web Token implementation in Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, + {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, +] [package.extras] -crypto = ["cryptography (>=3.3.1)", "types-cryptography (>=3.3.21)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "types-cryptography (>=3.3.21)", "zope.interface"] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] -name = "PyNaCl" +name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] [package.dependencies] cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] -[[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" +version = "7.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.19.0" +version = "0.20.3" description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, + {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, +] [package.dependencies] pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] @@ -720,6 +1212,10 @@ description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} @@ -735,6 +1231,10 @@ description = "Tests that depend on other tests" category = "dev" optional = false python-versions = "*" +files = [ + {file = "pytest-depends-1.0.1.tar.gz", hash = "sha256:90a28e2b87b75b18abd128c94015248544acac20e4392e9921e5a86f93319dfe"}, + {file = "pytest_depends-1.0.1-py3-none-any.whl", hash = "sha256:a1df072bcc93d77aca3f0946903f5fed8af2d9b0056db1dfc9ed5ac164ab0642"}, +] [package.dependencies] colorama = "*" @@ -749,17 +1249,25 @@ description = "Extensions to the standard Python datetime module" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] [package.dependencies] six = ">=1.5" [[package]] name = "python-gitlab" -version = "3.10.0" +version = "3.12.0" description = "Interact with GitLab API" category = "dev" optional = false python-versions = ">=3.7.0" +files = [ + {file = "python-gitlab-3.12.0.tar.gz", hash = "sha256:567390c2b93690dae62ed9738bf9f221fa45c01378fdf896089dbf7c8a134fbd"}, + {file = "python_gitlab-3.12.0-py3-none-any.whl", hash = "sha256:a5eb36b49783fda34563376674d5251dbbdbd1abd23b287dadf82f67d861b2c1"}, +] [package.dependencies] requests = ">=2.25.0" @@ -771,11 +1279,15 @@ yaml = ["PyYaml (>=5.2)"] [[package]] name = "python-semantic-release" -version = "7.32.1" +version = "7.33.0" description = "Automatic Semantic Versioning for Python projects" category = "dev" optional = false python-versions = "*" +files = [ + {file = "python-semantic-release-7.33.0.tar.gz", hash = "sha256:c559161c091e24f555c021fee63a9a704cc7ae59e774913ba5018fa3c9ce2e89"}, + {file = "python_semantic_release-7.33.0-py3-none-any.whl", hash = "sha256:b1fcc2d3be64e1b8d98228b2e2377f1c664e12eca038754eb8d0453c2ff4b438"}, +] [package.dependencies] click = ">=7,<9" @@ -795,7 +1307,7 @@ wheel = "*" dev = ["black", "isort", "tox"] docs = ["Jinja2 (==3.0.3)", "Sphinx (==1.3.6)"] mypy = ["mypy", "types-requests"] -test = ["coverage (>=5,<6)", "mock (==1.3.0)", "pytest (>=5,<6)", "pytest-mock (>=2,<3)", "pytest-xdist (>=1,<2)", "responses (==0.13.3)"] +test = ["coverage (>=5,<6)", "mock (==1.3.0)", "pytest (>=7,<8)", "pytest-mock (>=2,<3)", "pytest-xdist (>=1,<2)", "responses (==0.13.3)"] [[package]] name = "pywin32-ctypes" @@ -804,56 +1316,114 @@ description = "" category = "dev" optional = false python-versions = "*" +files = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "readme-renderer" +version = "37.3" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "readme_renderer-37.3-py3-none-any.whl", hash = "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"}, + {file = "readme_renderer-37.3.tar.gz", hash = "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273"}, +] + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] -[[package]] -name = "readme-renderer" -version = "37.2" -description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -bleach = ">=2.1.0" -docutils = ">=0.13.1" -Pygments = ">=2.5.1" - -[package.extras] -md = ["cmarkgfm (>=0.8.0)"] - [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "dev" optional = false python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" 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)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.10.0" +version = "0.10.1" description = "A utility belt for advanced users of python-requests" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, + {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, +] [package.dependencies] requests = ">=2.0.1,<3.0.0" @@ -865,6 +1435,10 @@ description = "Validating URI References per RFC 3986" category = "main" optional = false python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] [package.dependencies] idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} @@ -873,12 +1447,16 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} idna2008 = ["idna"] [[package]] -name = "SecretStorage" +name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] [package.dependencies] cryptography = ">=2.0" @@ -891,6 +1469,10 @@ description = "Python helper for Semantic Versioning (http://semver.org/)" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] [[package]] name = "setuptools" @@ -899,6 +1481,10 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "setuptools-58.5.3-py3-none-any.whl", hash = "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf"}, + {file = "setuptools-58.5.3.tar.gz", hash = "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729"}, +] [package.extras] docs = ["furo", "jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-inline-tabs", "sphinxcontrib-towncrier"] @@ -911,6 +1497,10 @@ description = "Python 2 and 3 compatibility utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "smmap" @@ -919,6 +1509,10 @@ description = "A pure Python implementation of a sliding window memory map manag category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] [[package]] name = "sniffio" @@ -927,14 +1521,10 @@ description = "Sniff out which async library your code is running under" category = "main" optional = false python-versions = ">=3.7" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] [[package]] name = "tomli" @@ -943,14 +1533,22 @@ description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "tomlkit" -version = "0.11.5" +version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6" +files = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] [[package]] name = "tqdm" @@ -959,6 +1557,10 @@ description = "Fast, Extensible Progress Meter" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -976,6 +1578,10 @@ description = "Collection of utilities for publishing packages on PyPI" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"}, + {file = "twine-3.8.0.tar.gz", hash = "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19"}, +] [package.dependencies] colorama = ">=0.4.3" @@ -996,6 +1602,32 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] [[package]] name = "typer" @@ -1004,6 +1636,10 @@ description = "Typer, build great CLIs. Easy to code. Based on Python type hints category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "typer-0.4.2-py3-none-any.whl", hash = "sha256:023bae00d1baf358a6cc7cea45851639360bb716de687b42b0a4641cd99173f1"}, + {file = "typer-0.4.2.tar.gz", hash = "sha256:b8261c6c0152dd73478b5ba96ba677e5d6948c715c310f7c91079f311f62ec03"}, +] [package.dependencies] click = ">=7.1.1,<9.0.0" @@ -1021,6 +1657,10 @@ description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] [[package]] name = "unasync" @@ -1029,6 +1669,10 @@ description = "The async transformation code." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +files = [ + {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, + {file = "unasync-0.5.0.tar.gz", hash = "sha256:b675d87cf56da68bd065d3b7a67ac71df85591978d84c53083c20d79a7e5096d"}, +] [[package]] name = "unasync-cli" @@ -1037,6 +1681,10 @@ description = "Command line interface for unasync" category = "dev" optional = false python-versions = ">=3.6.14,<4.0.0" +files = [ + {file = "unasync-cli-0.0.9.tar.gz", hash = "sha256:ca9d8c57ebb68911f8f8f68f243c7f6d0bb246ee3fd14743bc51c8317e276554"}, + {file = "unasync_cli-0.0.9-py3-none-any.whl", hash = "sha256:f96c42fb2862efa555ce6d6415a5983ceb162aa0e45be701656d20a955c7c540"}, +] [package.dependencies] setuptools = ">=58.2.0,<59.0.0" @@ -1045,11 +1693,15 @@ unasync = ">=0.5.0,<0.6.0" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -1063,6 +1715,10 @@ description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, + {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, +] [package.dependencies] distlib = ">=0.3.1,<1" @@ -1081,17 +1737,25 @@ description = "Character encoding aliases for legacy web content" category = "dev" optional = false python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] [[package]] name = "wheel" -version = "0.37.1" +version = "0.38.4" description = "A built-package format for Python" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" +files = [ + {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, + {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, +] [package.extras] -test = ["pytest (>=3.0.0)", "pytest-cov"] +test = ["pytest (>=3.0.0)"] [[package]] name = "wrapt" @@ -1100,637 +1764,7 @@ 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 = "zipp" -version = "3.9.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "0f825c7a0523d8602cd23be5a17bbb4ed8f4f007fd5001700a4ad9b70b0e8f61" - -[metadata.files] -anyio = [ - {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, - {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -black = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, -] -bleach = [ - {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, - {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -click-log = [ - {file = "click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975"}, - {file = "click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, -] -cryptography = [ - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, - {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, - {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, - {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, - {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, - {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, - {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, - {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, - {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, - {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, -] -Deprecated = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -docutils = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] -dotty-dict = [ - {file = "dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f"}, - {file = "dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15"}, -] -Faker = [ - {file = "Faker-15.1.1-py3-none-any.whl", hash = "sha256:096c15e136adb365db24d8c3964fe26bfc68fe060c9385071a339f8c14e09c8a"}, - {file = "Faker-15.1.1.tar.gz", hash = "sha256:a741b77f484215c3aab2604100669657189548f440fcb2ed0f8b7ee21c385629"}, -] -filelock = [ - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, -] -flake8 = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] -future-fstrings = [ - {file = "future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63"}, - {file = "future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089"}, -] -gitdb = [ - {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, - {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, -] -GitPython = [ - {file = "GitPython-3.1.29-py3-none-any.whl", hash = "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f"}, - {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, -] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -httpcore = [ - {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, - {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, -] -httpx = [ - {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, - {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, -] -identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -invoke = [ - {file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"}, - {file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -"jaraco.classes" = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, -] -jeepney = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] -keyring = [ - {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, - {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, -] -mccabe = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -more-itertools = [ - {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, - {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, -] -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"}, -] -networkx = [ - {file = "networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef"}, - {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, -] -pkginfo = [ - {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, - {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, -] -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"}, -] -pre-commit = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydantic = [ - {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, - {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"}, - {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"}, - {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"}, - {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"}, - {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"}, - {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"}, - {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"}, - {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"}, - {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"}, - {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"}, - {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"}, - {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"}, - {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"}, - {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"}, - {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"}, - {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"}, - {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"}, - {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"}, - {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"}, - {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"}, - {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, - {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, -] -pyflakes = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] -PyGithub = [ - {file = "PyGithub-1.56-py3-none-any.whl", hash = "sha256:d15f13d82165306da8a68aefc0f848a6f6432d5febbff13b60a94758ce3ef8b5"}, - {file = "PyGithub-1.56.tar.gz", hash = "sha256:80c6d85cf0f9418ffeb840fd105840af694c4f17e102970badbaf678251f2a01"}, -] -Pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -PyJWT = [ - {file = "PyJWT-2.5.0-py3-none-any.whl", hash = "sha256:8d82e7087868e94dd8d7d418e5088ce64f7daab4b36db654cbaedb46f9d1ca80"}, - {file = "PyJWT-2.5.0.tar.gz", hash = "sha256:e77ab89480905d86998442ac5788f35333fa85f65047a534adc38edf3c88fc3b"}, -] -PyNaCl = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] -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"}, -] -pytest-cov = [ - {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, - {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, -] -pytest-depends = [ - {file = "pytest-depends-1.0.1.tar.gz", hash = "sha256:90a28e2b87b75b18abd128c94015248544acac20e4392e9921e5a86f93319dfe"}, - {file = "pytest_depends-1.0.1-py3-none-any.whl", hash = "sha256:a1df072bcc93d77aca3f0946903f5fed8af2d9b0056db1dfc9ed5ac164ab0642"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] -python-gitlab = [ - {file = "python-gitlab-3.10.0.tar.gz", hash = "sha256:14930a16fdd7f36f67b9373e7d4d4720e8e374800028380289db3306e9f74614"}, - {file = "python_gitlab-3.10.0-py3-none-any.whl", hash = "sha256:6b5a24d0f479c43c1759cb174cf42116ed27cced31284bedc82d584d860fa238"}, -] -python-semantic-release = [ - {file = "python-semantic-release-7.32.1.tar.gz", hash = "sha256:ba47100e4ffa74c006529d6a8c22ca98f85ff4c145e8584eb2b17e6830a5116e"}, - {file = "python_semantic_release-7.32.1-py3-none-any.whl", hash = "sha256:9a29a02b3fb33fc318b07bf77c001c62aed9060d148eca25af7f4aa3afb49f4e"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -PyYAML = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -readme-renderer = [ - {file = "readme_renderer-37.2-py3-none-any.whl", hash = "sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106"}, - {file = "readme_renderer-37.2.tar.gz", hash = "sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -requests-toolbelt = [ - {file = "requests-toolbelt-0.10.0.tar.gz", hash = "sha256:f695d6207931200b46c8ef6addbc8a921fb5d77cc4cd209c2e7d39293fcd2b30"}, - {file = "requests_toolbelt-0.10.0-py2.py3-none-any.whl", hash = "sha256:64c6b8c51b515d123f9f708a29743f44eb70c4479440641ed2df8c4dea56d985"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -SecretStorage = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] -semver = [ - {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, - {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, -] -setuptools = [ - {file = "setuptools-58.5.3-py3-none-any.whl", hash = "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf"}, - {file = "setuptools-58.5.3.tar.gz", hash = "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -smmap = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, -] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] -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.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, - {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, -] -tqdm = [ - {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, - {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, -] -twine = [ - {file = "twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"}, - {file = "twine-3.8.0.tar.gz", hash = "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typer = [ - {file = "typer-0.4.2-py3-none-any.whl", hash = "sha256:023bae00d1baf358a6cc7cea45851639360bb716de687b42b0a4641cd99173f1"}, - {file = "typer-0.4.2.tar.gz", hash = "sha256:b8261c6c0152dd73478b5ba96ba677e5d6948c715c310f7c91079f311f62ec03"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -unasync = [ - {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, - {file = "unasync-0.5.0.tar.gz", hash = "sha256:b675d87cf56da68bd065d3b7a67ac71df85591978d84c53083c20d79a7e5096d"}, -] -unasync-cli = [ - {file = "unasync-cli-0.0.9.tar.gz", hash = "sha256:ca9d8c57ebb68911f8f8f68f243c7f6d0bb246ee3fd14743bc51c8317e276554"}, - {file = "unasync_cli-0.0.9-py3-none-any.whl", hash = "sha256:f96c42fb2862efa555ce6d6415a5983ceb162aa0e45be701656d20a955c7c540"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, -] -virtualenv = [ - {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, - {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, -] -webencodings = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] -wheel = [ - {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, - {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, -] -wrapt = [ +files = [ {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"}, @@ -1796,7 +1830,24 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + +[[package]] +name = "zipp" +version = "3.12.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.12.0-py3-none-any.whl", hash = "sha256:9eb0a4c5feab9b08871db0d672745b53450d7f26992fd1e4653aa43345e97b86"}, + {file = "zipp-3.12.0.tar.gz", hash = "sha256:73efd63936398aac78fd92b6f4865190119d6c91b531532e798977ea8dd402eb"}, ] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "e5322fe5937fb912068f8b83aea01c1a987c510e065947e55fad9e203ff83631" diff --git a/pyproject.toml b/pyproject.toml index c8c34c50..4366272a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,18 +20,18 @@ httpx = "^0.23.0" pydantic = "^1.10.0" [tool.poetry.dev-dependencies] -black = "^22.10.0" -Faker = "^15.1.1" +pytest = "^7.2.0" flake8 = "^5.0.4" -isort = "^5.10.1" -pre-commit = "^2.20.0" +black = "^22.12.0" +isort = "^5.11.4" PyGithub = "^1.55" -pytest = "^7.1.3" -pytest-asyncio = "^0.19.0" +pre-commit = "^2.21.0" pytest-cov = "^4.0.0" pytest-depends = "^1.0.1" -python-semantic-release = "^7.31.4" +pytest-asyncio = "^0.20.3" +Faker = "^16.6.1" unasync-cli = "^0.0.9" +python-semantic-release = "^7.32.2" [tool.semantic_release] version_variable = "gotrue/__init__.py:__version__" From 08231baf14e72772e19f9d70ddd29e1b7387587e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 14:42:41 +0800 Subject: [PATCH 29/35] chore: remove PyGithub as dep --- poetry.lock | 160 +------------------------------------------------ pyproject.toml | 1 - 2 files changed, 1 insertion(+), 160 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37dc8240..43d53f75 100644 --- a/poetry.lock +++ b/poetry.lock @@ -450,24 +450,6 @@ sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] -[[package]] -name = "deprecated" -version = "1.2.13" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, - {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] - [[package]] name = "distlib" version = "0.3.6" @@ -1079,27 +1061,6 @@ files = [ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] -[[package]] -name = "pygithub" -version = "1.57" -description = "Use the full Github API v3" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyGithub-1.57-py3-none-any.whl", hash = "sha256:5822febeac2391f1306c55a99af2bc8f86c8bf82ded000030cd02c18f31b731f"}, - {file = "PyGithub-1.57.tar.gz", hash = "sha256:c273f252b278fb81f1769505cc6921bdb6791e1cebd6ac850cc97dad13c31ff3"}, -] - -[package.dependencies] -deprecated = "*" -pyjwt = ">=2.4.0" -pynacl = ">=1.4.0" -requests = ">=2.14.0" - -[package.extras] -integrations = ["cryptography"] - [[package]] name = "pygments" version = "2.14.0" @@ -1115,51 +1076,6 @@ files = [ [package.extras] plugins = ["importlib-metadata"] -[[package]] -name = "pyjwt" -version = "2.6.0" -description = "JSON Web Token implementation in Python" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, - {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, -] - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - [[package]] name = "pytest" version = "7.2.1" @@ -1757,80 +1673,6 @@ files = [ [package.extras] test = ["pytest (>=3.0.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" -files = [ - {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"}, -] - [[package]] name = "zipp" version = "3.12.0" @@ -1850,4 +1692,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "e5322fe5937fb912068f8b83aea01c1a987c510e065947e55fad9e203ff83631" +content-hash = "40ee57d0db3774fdb23ece7b1b37010c77b74592931b594c33f56b108438dbc6" diff --git a/pyproject.toml b/pyproject.toml index 4366272a..540e516f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ pytest = "^7.2.0" flake8 = "^5.0.4" black = "^22.12.0" isort = "^5.11.4" -PyGithub = "^1.55" pre-commit = "^2.21.0" pytest-cov = "^4.0.0" pytest-depends = "^1.0.1" From 36e8d12e10f5f8494a7e5b00bf443678f6db0e47 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 14:58:43 +0800 Subject: [PATCH 30/35] fix: add jwt package --- poetry.lock | 160 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 43d53f75..e537b113 100644 --- a/poetry.lock +++ b/poetry.lock @@ -450,6 +450,24 @@ sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] + [[package]] name = "distlib" version = "0.3.6" @@ -1061,6 +1079,27 @@ files = [ {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] +[[package]] +name = "pygithub" +version = "1.57" +description = "Use the full Github API v3" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyGithub-1.57-py3-none-any.whl", hash = "sha256:5822febeac2391f1306c55a99af2bc8f86c8bf82ded000030cd02c18f31b731f"}, + {file = "PyGithub-1.57.tar.gz", hash = "sha256:c273f252b278fb81f1769505cc6921bdb6791e1cebd6ac850cc97dad13c31ff3"}, +] + +[package.dependencies] +deprecated = "*" +pyjwt = ">=2.4.0" +pynacl = ">=1.4.0" +requests = ">=2.14.0" + +[package.extras] +integrations = ["cryptography"] + [[package]] name = "pygments" version = "2.14.0" @@ -1076,6 +1115,51 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyjwt" +version = "2.6.0" +description = "JSON Web Token implementation in Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"}, + {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pytest" version = "7.2.1" @@ -1673,6 +1757,80 @@ files = [ [package.extras] test = ["pytest (>=3.0.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" +files = [ + {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"}, +] + [[package]] name = "zipp" version = "3.12.0" @@ -1692,4 +1850,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "40ee57d0db3774fdb23ece7b1b37010c77b74592931b594c33f56b108438dbc6" +content-hash = "fe809003c17176bebda81d9201a3c6acafd3396b61688cba5efa2c3ab5f96f03" diff --git a/pyproject.toml b/pyproject.toml index 540e516f..c509bb89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ pytest-asyncio = "^0.20.3" Faker = "^16.6.1" unasync-cli = "^0.0.9" python-semantic-release = "^7.32.2" +pygithub = "^1.57" [tool.semantic_release] version_variable = "gotrue/__init__.py:__version__" From 34b8fb1aaf383a149efaa8f5832cbc82782b91d4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 15:15:49 +0800 Subject: [PATCH 31/35] fix: regenerate lock --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index e537b113..08773229 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1850,4 +1850,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "fe809003c17176bebda81d9201a3c6acafd3396b61688cba5efa2c3ab5f96f03" +content-hash = "8e8b315dcd43ad1b19334654d6f01ec9e6f1e79120148fe7fe0040c4cdd09254" diff --git a/pyproject.toml b/pyproject.toml index c509bb89..0f32b8b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ pytest-asyncio = "^0.20.3" Faker = "^16.6.1" unasync-cli = "^0.0.9" python-semantic-release = "^7.32.2" -pygithub = "^1.57" +PyGithub = "^1.57" [tool.semantic_release] version_variable = "gotrue/__init__.py:__version__" From 1e51fce51b32cada87d94efc8cf11f6a1aa6b282 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 15:29:44 +0800 Subject: [PATCH 32/35] fix: move dependency --- poetry.lock | 2 +- pyproject.toml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 08773229..eb5112b2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1850,4 +1850,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "8e8b315dcd43ad1b19334654d6f01ec9e6f1e79120148fe7fe0040c4cdd09254" +content-hash = "c8c04c0e10209459449ddc085fa381610bc90aff94180fa426cdad8a1d46fc3f" diff --git a/pyproject.toml b/pyproject.toml index 0f32b8b5..bcf1f623 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,9 @@ pytest-asyncio = "^0.20.3" Faker = "^16.6.1" unasync-cli = "^0.0.9" python-semantic-release = "^7.32.2" -PyGithub = "^1.57" + +[tool.poetry.group.dev.dependencies] +pygithub = "^1.57" [tool.semantic_release] version_variable = "gotrue/__init__.py:__version__" From 7f505016f04c1befc9db75a7f2dd71089b224eb4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 15:50:18 +0800 Subject: [PATCH 33/35] fix: bump isort and python version --- .pre-commit-config.yaml | 2 +- poetry.lock | 130 ++++++++++++---------------------------- pyproject.toml | 4 +- 3 files changed, 42 insertions(+), 94 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b9f01a6..29056451 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: args: ["--fix=lf"] - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort args: diff --git a/poetry.lock b/poetry.lock index eb5112b2..555138f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,7 +15,6 @@ files = [ [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] @@ -69,7 +68,6 @@ mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -310,7 +308,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "click-log" @@ -533,7 +530,6 @@ files = [ [package.dependencies] python-dateutil = ">=2.4" -typing-extensions = {version = ">=3.10.0.1", markers = "python_version < \"3.8\""} [[package]] name = "filelock" @@ -564,7 +560,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" @@ -613,7 +608,6 @@ files = [ [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} [[package]] name = "h11" @@ -627,9 +621,6 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [[package]] name = "httpcore" version = "0.16.3" @@ -705,23 +696,23 @@ files = [ [[package]] name = "importlib-metadata" -version = "4.2.0" +version = "6.0.0" description = "Read metadata from Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, + {file = "importlib_metadata-6.0.0-py3-none-any.whl", hash = "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad"}, + {file = "importlib_metadata-6.0.0.tar.gz", hash = "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -749,19 +740,19 @@ files = [ [[package]] name = "isort" -version = "5.11.4" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, - {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] [package.extras] -colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] @@ -802,25 +793,26 @@ trio = ["async_generator", "trio"] [[package]] name = "keyring" -version = "23.9.3" +version = "23.13.1" description = "Store and access your passwords safely." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, - {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, + {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, + {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +completion = ["shtab"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] @@ -861,22 +853,22 @@ files = [ [[package]] name = "networkx" -version = "2.6.3" +version = "3.0" description = "Python package for creating and manipulating graphs and networks" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef"}, - {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, + {file = "networkx-3.0-py3-none-any.whl", hash = "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e"}, + {file = "networkx-3.0.tar.gz", hash = "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412"}, ] [package.extras] -default = ["matplotlib (>=3.3)", "numpy (>=1.19)", "pandas (>=1.1)", "scipy (>=1.5,!=1.6.1)"] -developer = ["black (==21.5b1)", "pre-commit (>=2.12)"] -doc = ["nb2plots (>=0.6)", "numpydoc (>=1.1)", "pillow (>=8.2)", "pydata-sphinx-theme (>=0.6,<1.0)", "sphinx (>=4.0,<5.0)", "sphinx-gallery (>=0.9,<1.0)", "texext (>=0.6.6)"] -extra = ["lxml (>=4.5)", "pydot (>=1.4.1)", "pygraphviz (>=1.7)"] -test = ["codecov (>=2.1)", "pytest (>=6.2)", "pytest-cov (>=2.12)"] +default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=0.991)", "pre-commit (>=2.20)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (==5.2.3)", "sphinx-gallery (>=0.11)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "nodeenv" @@ -944,9 +936,6 @@ files = [ {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} - [package.extras] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] @@ -963,9 +952,6 @@ files = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -985,7 +971,6 @@ files = [ [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" @@ -1176,7 +1161,6 @@ files = [ attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -1199,7 +1183,6 @@ files = [ [package.dependencies] pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -1595,40 +1578,6 @@ rfc3986 = ">=1.4.0" tqdm = ">=4.14" urllib3 = ">=1.26.0" -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] - [[package]] name = "typer" version = "0.4.2" @@ -1710,25 +1659,24 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.2" +version = "20.17.1" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, - {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -platformdirs = ">=2,<3" +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "webencodings" @@ -1849,5 +1797,5 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" -python-versions = "^3.7" -content-hash = "c8c04c0e10209459449ddc085fa381610bc90aff94180fa426cdad8a1d46fc3f" +python-versions = "^3.9" +content-hash = "507c422406e20b953aeeac32a73089ece9c706a196374097f833bec114789514" diff --git a/pyproject.toml b/pyproject.toml index bcf1f623..611c47ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.9" httpx = "^0.23.0" pydantic = "^1.10.0" @@ -23,7 +23,7 @@ pydantic = "^1.10.0" pytest = "^7.2.0" flake8 = "^5.0.4" black = "^22.12.0" -isort = "^5.11.4" +isort = "^5.12.0" pre-commit = "^2.21.0" pytest-cov = "^4.0.0" pytest-depends = "^1.0.1" From dc3573c466b139fc7fdb89670f0a2de08da38830 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 15:56:28 +0800 Subject: [PATCH 34/35] chore: update ci --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ed573e7..46e7e73e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.9, "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Clone Repository From 58db1be7939c7aae1d50e7e29dee3a8f110775e4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 29 Jan 2023 17:59:50 +0800 Subject: [PATCH 35/35] fix: reinstate 3.8 --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46e7e73e..ccfa2a41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - name: Clone Repository diff --git a/pyproject.toml b/pyproject.toml index 611c47ef..ce451291 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.8" httpx = "^0.23.0" pydantic = "^1.10.0"