diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e086d619..5a61d359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Run Tests - run: make ci + run: make ci -j - name: Upload coverage to Coveralls uses: coverallsapp/github-action@v2 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d4f4cd1a..72c2693d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,5 +2,6 @@ "src/supabase": "2.18.1", "src/realtime": "2.7.0", "src/functions": "0.10.1", - "src/storage": "0.12.1" + "src/storage": "0.12.1", + "src/auth": "2.12.3" } diff --git a/Makefile b/Makefile index 7d5035f8..61f94945 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,54 @@ -.PHONY: ci, default, pre-commit - -default: - @echo "Available targets are: ci, pre-commit, publish" - -ci: pre-commit - make -C src/functions tests - make -C src/realtime tests - make -C src/storage tests - make -C src/supabase tests - -publish: - uv build --package realtime - uv build --package storage3 - uv build --package supabase_functions - uv build --package supabase - uv publish +.PHONY: ci, default, pre-commit, clean, start-infra, stop-infra + +PACKAGES := functions realtime storage auth supabase +FORALL_PKGS = $(foreach pkg, $(PACKAGES), $(pkg).$(1)) + +help:: + @echo "Available commands" + @echo " help -- (default) print this message" + +ci: pre-commit $(call FORALL_PKGS,tests) +help:: + @echo " ci -- Run tests for all packages, the same way as CI does" pre-commit: uv run pre-commit run --all-files +help:: + @echo " pre-commit -- Run pre-commit on all files" + +clean: $(call FORALL_PKGS,clean) +help:: + @echo " clean -- Delete cache files and coverage reports from tests" + +publish: $(call FORALL_PKGS,build) + uv publish + +# not all packages have infra, so just manually instantiate the ones that do for now +start-infra: realtime.start-infra storage.start-infra auth.start-infra +help:: + @echo " start-infra -- Start all containers necessary for tests. NOTE: it is not necessary to this before running CI tests, they start the infra by themselves" +stop-infra: realtime.stop-infra storage.stop-infra auth.stop-infra +help:: + @echo " stop-infra -- Stop all infra used by tests. NOTE: tests do leave their infra running, so run this to ensure all containers are stopped" + + +realtime.%: + @$(MAKE) -C src/realtime $* + +functions.%: + @$(MAKE) -C src/functions $* + +storage.%: + @$(MAKE) -C src/storage $* + +auth.%: + @$(MAKE) -C src/auth $* + +supabase.%: + @$(MAKE) -C src/supabase $* + +help:: + @echo + @echo "Package specific commands can be ran by prefixing the command with the package name. Examples:" + @echo " realtime.mypy -- runs relatime's mypy target" + @echo " supabase.build -- runs supabase's build target" diff --git a/README.md b/README.md index d93077be..55a1f7c7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Python monorepo for all [Supabase](https://supabase.com) libraries. This is a wo - [realtime](src/realtime/README.md) - [supabase_functions](src/functions/README.md) - [storage3](src/storage/README.md) +- [supabase_auth](src/auth/README.md) Relevant links: diff --git a/pyproject.toml b/pyproject.toml index 5a35fff9..8beb844c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,14 @@ members = [ "src/realtime", "src/functions", "src/supabase", - "src/storage" + "src/storage", + "src/auth" ] [tool.uv.sources] realtime = { workspace = true } supabase_functions = { workspace = true } +supabase_auth = { workspace = true } storage3 = { workspace = true } supabase = { workspace = true } diff --git a/release-please-config.json b/release-please-config.json index 29925656..1e88490d 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,6 +5,10 @@ "changelog-path": "src/realtime/CHANGELOG.md", "release-type": "python" }, + "src/auth": { + "changelog-path": "src/auth/CHANGELOG.md", + "release-type": "python" + }, "src/functions": { "changelog-path": "src/functions/CHANGELOG.md", "release-type": "python" diff --git a/src/auth/.devcontainer/Dockerfile b/src/auth/.devcontainer/Dockerfile new file mode 100644 index 00000000..6a9e8da9 --- /dev/null +++ b/src/auth/.devcontainer/Dockerfile @@ -0,0 +1,21 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT="3.10-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/src/auth/.devcontainer/devcontainer.json b/src/auth/.devcontainer/devcontainer.json new file mode 100644 index 00000000..3ab2c039 --- /dev/null +++ b/src/auth/.devcontainer/devcontainer.json @@ -0,0 +1,87 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/python-3 +{ + "name": "Python 3", + "runArgs": [ + "--init" + ], + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.10-bullseye", + // Options + "NODE_VERSION": "lts/*" + } + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", + "python.analysis.diagnosticMode": "workspace", + "files.exclude": { + "**/.ipynb_checkpoints": true, + "**/.pytest_cache": true, + "**/*pycache*": true + }, + "python.formatting.provider": "black", + "python.linting.flake8Args": [ + "--max-line-length=88", + "--extend-ignore=E203" + ], + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, + "python.sortImports.args": [ + "--multi-line=3", + "--trailing-comma", + "--force-grid-wrap=0", + "--use-parentheses", + "--line-width=88", + ], + "markdownlint.config": { + "MD022": false, + "MD024": false, + "MD032": false, + "MD033": false + } + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-azuretools.vscode-docker", + "donjayamanne.githistory", + "felipecaputo.git-project-manager", + "github.copilot-nightly", + "eamodio.gitlens", + "davidanson.vscode-markdownlint" + ], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "docker-in-docker": "latest", + "git": "latest", + "git-lfs": "latest", + "github-cli": "latest" + } +} diff --git a/src/auth/CHANGELOG.md b/src/auth/CHANGELOG.md new file mode 100644 index 00000000..644af7eb --- /dev/null +++ b/src/auth/CHANGELOG.md @@ -0,0 +1,5883 @@ +# CHANGELOG + +## [2.12.3](https://github.com/supabase/auth-py/compare/v2.12.2...v2.12.3) (2025-07-03) + + +### Bug Fixes + +* add missing email_redirect_to option ([#728](https://github.com/supabase/auth-py/issues/728)) ([7e63772](https://github.com/supabase/auth-py/commit/7e637729af6442ec64a9cff1785e7b099c1b4007)) +* add missing nonce to the update_user method ([#731](https://github.com/supabase/auth-py/issues/731)) ([9db842a](https://github.com/supabase/auth-py/commit/9db842aba268c128db1d59a08de77c2c2c197aa3)) + +## [2.12.2](https://github.com/supabase/auth-py/compare/v2.12.1...v2.12.2) (2025-06-23) + + +### Bug Fixes + +* add py.typed marker for type-checking support ([#721](https://github.com/supabase/auth-py/issues/721)) ([59fddd4](https://github.com/supabase/auth-py/commit/59fddd4faffca19f9f9d5523b72179ee32c90aa6)) +* remove unused jwt key validation ([#725](https://github.com/supabase/auth-py/issues/725)) ([13008b3](https://github.com/supabase/auth-py/commit/13008b3977b1e9ad688c4c8b7f0f6b79523ebca3)) +* sms mfa failure due to missing totp field ([#723](https://github.com/supabase/auth-py/issues/723)) ([8b81588](https://github.com/supabase/auth-py/commit/8b81588638f3cb4c972ba386ddf8d46c38ff712b)) + +## [2.12.1](https://github.com/supabase/auth-py/compare/v2.12.0...v2.12.1) (2025-05-15) + + +### Bug Fixes + +* validate uuid on admin methods and fix wrong factor in delete_factor method ([#706](https://github.com/supabase/auth-py/issues/706)) ([1649956](https://github.com/supabase/auth-py/commit/1649956c3aca418eaa10b2434292b17f40565a8b)) + +## [2.12.0](https://github.com/supabase/auth-py/compare/v2.11.4...v2.12.0) (2025-03-26) + + +### Features + +* add `get_claims` method ([#681](https://github.com/supabase/auth-py/issues/681)) ([f5c9926](https://github.com/supabase/auth-py/commit/f5c992608452b264d2b90277e384bd65fee98ee7)) + + +### Bug Fixes + +* enroll mfa totp ([#693](https://github.com/supabase/auth-py/issues/693)) ([ca9fcf3](https://github.com/supabase/auth-py/commit/ca9fcf3a1a0fbf99987e7e032a0eae542e4fd659)) + +## [2.11.4](https://github.com/supabase/auth-py/compare/v2.11.3...v2.11.4) (2025-02-18) + + +### Bug Fixes + +* Remove unused deprecated files ([#675](https://github.com/supabase/auth-py/issues/675)) ([961628a](https://github.com/supabase/auth-py/commit/961628ada82c0ac4476e8cb89553b8502b506d81)) + +## [2.11.3](https://github.com/supabase/auth-py/compare/v2.11.2...v2.11.3) (2025-01-30) + + +### Bug Fixes + +* move redirect_to into metadata for SSO authentication ([#671](https://github.com/supabase/auth-py/issues/671)) ([6226426](https://github.com/supabase/auth-py/commit/6226426c59449d2cd57765b9dfd199e83089dd8c)) +* sign_out not clearing session when exception raised ([#665](https://github.com/supabase/auth-py/issues/665)) ([81a9d9e](https://github.com/supabase/auth-py/commit/81a9d9e5de91a515766d79a4e0a13d9f89cbfaf4)) + +## [2.11.2](https://github.com/supabase/auth-py/compare/v2.11.1...v2.11.2) (2025-01-24) + + +### Bug Fixes + +* use query params for all httpx requests ([#662](https://github.com/supabase/auth-py/issues/662)) ([a7a9cea](https://github.com/supabase/auth-py/commit/a7a9cea1ceab302aabd07610244a54ce84221006)) + +## [2.11.1](https://github.com/supabase/auth-py/compare/v2.11.0...v2.11.1) (2024-12-30) + + +### Bug Fixes + +* add email_address_invalid error code ([#644](https://github.com/supabase/auth-py/issues/644)) ([4926bd5](https://github.com/supabase/auth-py/commit/4926bd51cb616d7c974f7c6ad34471f2d38c3c88)) +* set timer to be use daemon thread ([#647](https://github.com/supabase/auth-py/issues/647)) ([98de380](https://github.com/supabase/auth-py/commit/98de380bf5b90e594ef6560267d2c14a6001e3da)) + +## [2.11.0](https://github.com/supabase/auth-py/compare/v2.10.0...v2.11.0) (2024-11-22) + + +### Features + +* Check if token is a JWT ([#623](https://github.com/supabase/auth-py/issues/623)) ([f435736](https://github.com/supabase/auth-py/commit/f435736de16dfa68543e76861c2742ca46c05cf6)) + +## [2.10.0](https://github.com/supabase/auth-py/compare/v2.9.3...v2.10.0) (2024-11-03) + + +### Features + +* Check if url is an HTTP URL ([#620](https://github.com/supabase/auth-py/issues/620)) ([f0ccae5](https://github.com/supabase/auth-py/commit/f0ccae5fe6a8c012b8bcaf89bfc767d63feecbae)) + +## [2.9.3](https://github.com/supabase/auth-py/compare/v2.9.2...v2.9.3) (2024-10-18) + + +### Bug Fixes + +* bump minimal version of Python to 3.9 ([#612](https://github.com/supabase/auth-py/issues/612)) ([30938a6](https://github.com/supabase/auth-py/commit/30938a6db97c5ee3eb4c27fd8d38b7437bc07fd7)) +* Types to use Option[T] ([#609](https://github.com/supabase/auth-py/issues/609)) ([4484ea0](https://github.com/supabase/auth-py/commit/4484ea0f61149c75caff879161f63d390bf1ff1c)) + +## [2.9.2](https://github.com/supabase/auth-py/compare/v2.9.1...v2.9.2) (2024-10-06) + + +### Bug Fixes + +* httpx minimum version update ([#606](https://github.com/supabase/auth-py/issues/606)) ([28be757](https://github.com/supabase/auth-py/commit/28be757561977f2138639089e29888e2be6d871e)) + +## [2.9.1](https://github.com/supabase/auth-py/compare/v2.9.0...v2.9.1) (2024-09-30) + + +### Bug Fixes + +* mfa challenge channel field not required ([#603](https://github.com/supabase/auth-py/issues/603)) ([38a8696](https://github.com/supabase/auth-py/commit/38a8696058c4cc770a2a99b0ea1cf9649b2cea8c)) + +## [2.9.0](https://github.com/supabase/auth-py/compare/v2.8.1...v2.9.0) (2024-09-28) + + +### Features + +* Proxy support ([#600](https://github.com/supabase/auth-py/issues/600)) ([574d615](https://github.com/supabase/auth-py/commit/574d615d6c0b93e455cfcb16560f409c021471f1)) + + +### Bug Fixes + +* **deps:** bump pydantic from 2.9.0 to 2.9.2 ([#598](https://github.com/supabase/auth-py/issues/598)) ([c865a98](https://github.com/supabase/auth-py/commit/c865a98cf10248433dff23c3f6a67fc3683766b0)) + +## [2.8.1](https://github.com/supabase/auth-py/compare/v2.8.0...v2.8.1) (2024-09-08) + + +### Bug Fixes + +* **deps-dev:** bump cryptography from 43.0.0 to 43.0.1 ([#592](https://github.com/supabase/auth-py/issues/592)) ([79829d3](https://github.com/supabase/auth-py/commit/79829d3332c7d71c11dbc3adcdb8a89626144c4d)) + +## [2.8.0](https://github.com/supabase/auth-py/compare/v2.7.0...v2.8.0) (2024-09-02) + + +### Features + +* add MFA Phone ([#578](https://github.com/supabase/auth-py/issues/578)) ([b2bb7a1](https://github.com/supabase/auth-py/commit/b2bb7a149b42da0460ace211f6a640d9fa9f13b5)) +* add reauthenticate method ([#586](https://github.com/supabase/auth-py/issues/586)) ([d54feeb](https://github.com/supabase/auth-py/commit/d54feeb21006bfd5e5e1bdc039fac0aadab24e65)) +* add resend method ([#587](https://github.com/supabase/auth-py/issues/587)) ([536ad2c](https://github.com/supabase/auth-py/commit/536ad2c28e1815a8a2f48821081bfca009e198f2)) + + +### Bug Fixes + +* update invite user by email option type ([#588](https://github.com/supabase/auth-py/issues/588)) ([2510230](https://github.com/supabase/auth-py/commit/2510230c4eaa45303b269d029cfe9a0ece6e8a4e)) +* update password reset method name to match js lib ([#584](https://github.com/supabase/auth-py/issues/584)) ([d47daf1](https://github.com/supabase/auth-py/commit/d47daf1a0208c2df1a8993d37991f283f81f3221)) + +## [2.7.0](https://github.com/supabase/auth-py/compare/v2.6.2...v2.7.0) (2024-08-16) + + +### Features + +* add error codes ([#556](https://github.com/supabase/auth-py/issues/556)) ([05dfb8a](https://github.com/supabase/auth-py/commit/05dfb8aadeb3a3321fbcc01c0e3fe8ce49f3cccd)) + +## v2.6.2 (2024-08-09) + +### Chore + +* chore(deps-dev): bump faker from 26.0.0 to 26.1.0 (#557) ([`86ce4c1`](https://github.com/supabase/auth-py/commit/86ce4c1cbc458aa506b8c2ad8e2f01dfdf6cb68b)) + +* chore: update ci pipeline ([`06de3ac`](https://github.com/supabase/auth-py/commit/06de3acb7815c4b50e766bab2299cb31d8312529)) + +* chore(deps-dev): bump pytest from 8.3.1 to 8.3.2 (#555) ([`8a3362a`](https://github.com/supabase/auth-py/commit/8a3362a1813f01958182b0c2dde1fb3a9134df2a)) + +* chore(deps-dev): bump pytest from 8.3.1 to 8.3.2 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.1...8.3.2) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`02fbbfe`](https://github.com/supabase/auth-py/commit/02fbbfe92bb903444770971b9764dd22b9d773fb)) + +### Ci + +* ci: revert PAT changes (#568) ([`a5f5a4b`](https://github.com/supabase/auth-py/commit/a5f5a4bc4661a357e059907f54d0f473fcebc46c)) + +* ci: remove PAT (#566) ([`69610cf`](https://github.com/supabase/auth-py/commit/69610cfe89cb4d3fd99a8c908f07bdcd06ea097a)) + +* ci: remove PAT ([`4b5a3b5`](https://github.com/supabase/auth-py/commit/4b5a3b56d6cbc4fcf8b1f7eb59b8186bbca81506)) + +* ci: remove PAT (#565) ([`c2b20e0`](https://github.com/supabase/auth-py/commit/c2b20e07f0682d53f424c58a64bb80963efe046f)) + +### Fix + +* fix: update ci pipeline (#559) ([`c914dd3`](https://github.com/supabase/auth-py/commit/c914dd32706257fe37072580760f69fb3ea174d6)) + +* fix: update types (#558) ([`486f3e0`](https://github.com/supabase/auth-py/commit/486f3e04845e1f323ae12617c2d1d18847f658bc)) + +* fix: add role, password_hash & id types to admin user attributes ([`2adcd7f`](https://github.com/supabase/auth-py/commit/2adcd7f349a86e4af55961ff48e374e7c8221148)) + +* fix: add fly, linkedin_oidc and slack_oidc provider type ([`c13e2c7`](https://github.com/supabase/auth-py/commit/c13e2c78522dc77f0e3591cff2c71f9b793526a1)) + +### Unknown + +* update docker compose command call ([`41cefcf`](https://github.com/supabase/auth-py/commit/41cefcf441dedd78d5e0531160588705b8da560b)) + +## v2.6.1 (2024-07-24) + +### Chore + +* chore(release): bump version to v2.6.1 ([`4b5aa15`](https://github.com/supabase/auth-py/commit/4b5aa15996797ee908b5ab652757dbeca37a9aad)) + +* chore(deps-dev): bump pytest from 8.2.2 to 8.3.1 (#551) ([`964826c`](https://github.com/supabase/auth-py/commit/964826c2ae6baf823d491c38b30cba6392b4aa77)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.8.5 to 9.8.6 (#552) ([`fd92320`](https://github.com/supabase/auth-py/commit/fd923204c5da0cae480636ac12d3286e292f88ee)) + +* chore(deps-dev): bump python-semantic-release from 9.8.5 to 9.8.6 (#550) ([`625f4af`](https://github.com/supabase/auth-py/commit/625f4af3b773f242483e4ba10f67e7e0452c28cc)) + +* chore(deps-dev): bump pytest-asyncio from 0.23.7 to 0.23.8 (#549) ([`0a7a766`](https://github.com/supabase/auth-py/commit/0a7a76691e4597a9417afcbc016d24642329bcc9)) + +### Fix + +* fix: add is_anonymous type to the user model (#553) ([`c37913f`](https://github.com/supabase/auth-py/commit/c37913f013be27231bb3347d29334d6af8024eb3)) + +## v2.6.0 (2024-07-17) + +### Chore + +* chore(release): bump version to v2.6.0 ([`0899975`](https://github.com/supabase/auth-py/commit/0899975e9d964ba3ab525bca89fa487ca99b90d5)) + +### Feature + +* feat: add sign_in_with_id_token method (#548) ([`b7e2c2c`](https://github.com/supabase/auth-py/commit/b7e2c2c2b9dd949aa0931c3b90ed66b5de46825e)) + +## v2.5.5 (2024-07-14) + +### Chore + +* chore(release): bump version to v2.5.5 ([`79a24f0`](https://github.com/supabase/auth-py/commit/79a24f0d52b07693102812fa0934ae03b9bed898)) + +* chore(deps-dev): bump zipp from 3.18.1 to 3.19.1 (#546) ([`1c807cb`](https://github.com/supabase/auth-py/commit/1c807cb21ebc88cb95c98c8c12ca9b4e71c6da9d)) + +* chore(deps-dev): bump zipp from 3.18.1 to 3.19.1 + +Bumps [zipp](https://github.com/jaraco/zipp) from 3.18.1 to 3.19.1. +- [Release notes](https://github.com/jaraco/zipp/releases) +- [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst) +- [Commits](https://github.com/jaraco/zipp/compare/v3.18.1...v3.19.1) + +--- +updated-dependencies: +- dependency-name: zipp + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`4052673`](https://github.com/supabase/auth-py/commit/4052673857464876d5703d3c73c9af0b0653d4dc)) + +* chore(deps-dev): bump python-semantic-release from 9.8.3 to 9.8.5 (#544) ([`ff1046e`](https://github.com/supabase/auth-py/commit/ff1046ef2b78c14d6175e511e78cb6e80091425b)) + +* chore(deps-dev): bump python-semantic-release from 9.8.3 to 9.8.5 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.3 to 9.8.5. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.3...v9.8.5) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`dd38cdc`](https://github.com/supabase/auth-py/commit/dd38cdc39dc147850851953017f20ff568621a87)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.8.3 to 9.8.5 (#545) ([`32ac690`](https://github.com/supabase/auth-py/commit/32ac690dbb8a70550cfec8b5b12c8b74b79c69b5)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.3 to 9.8.5. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.3...v9.8.5) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`07042e6`](https://github.com/supabase/auth-py/commit/07042e603adcce30dc76f1fd78b1c8b4209a0a31)) + +* chore(deps): bump certifi from 2024.2.2 to 2024.7.4 (#542) ([`d960b0f`](https://github.com/supabase/auth-py/commit/d960b0f6feb91415c27b805bf1fe09453df9660c)) + +* chore(deps): bump certifi from 2024.2.2 to 2024.7.4 + +Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4. +- [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04) + +--- +updated-dependencies: +- dependency-name: certifi + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`95f06c3`](https://github.com/supabase/auth-py/commit/95f06c326118febd77a8464eadc0726c6a274c75)) + +* chore(deps): bump pydantic from 2.7.4 to 2.8.2 (#539) ([`d0798d6`](https://github.com/supabase/auth-py/commit/d0798d69fc45e6abc20754be253d037265d7733c)) + +* chore(deps): bump pydantic from 2.7.4 to 2.8.2 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.4 to 2.8.2. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/v2.8.2/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.4...v2.8.2) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e22a177`](https://github.com/supabase/auth-py/commit/e22a1777225a64c283e4dfd261e8ad3efbf5be0e)) + +* chore(deps-dev): bump faker from 25.9.1 to 26.0.0 (#535) ([`f2fa2a9`](https://github.com/supabase/auth-py/commit/f2fa2a9eeaac2119e1fa2c6dc646649e926e32b1)) + +* chore(deps-dev): bump faker from 25.9.1 to 26.0.0 + +Bumps [faker](https://github.com/joke2k/faker) from 25.9.1 to 26.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v25.9.1...v26.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1fe332f`](https://github.com/supabase/auth-py/commit/1fe332fa783f71a25c67f7db42484cb029d77bfb)) + +### Fix + +* fix: enable HTTP2 (#534) ([`73ea013`](https://github.com/supabase/auth-py/commit/73ea0131dc326df85adc927c1f908ba11adf4d25)) + +## v2.5.4 (2024-06-23) + +### Chore + +* chore(release): bump version to v2.5.4 ([`ca6e6c9`](https://github.com/supabase/auth-py/commit/ca6e6c97c1071fc13f7efa58269b936065d2395e)) + +### Fix + +* fix: add await to async call for anonymous sign in request (#532) ([`45cfe25`](https://github.com/supabase/auth-py/commit/45cfe25f10cb63134cd297b34bae419687cb64b6)) + +### Unknown + +* add missing await ([`412449a`](https://github.com/supabase/auth-py/commit/412449ac83d72ad05415cbfb63f91821555ed997)) + +* add await ([`5520ad8`](https://github.com/supabase/auth-py/commit/5520ad839ea8071801e00e5c10294958b31dd3e4)) + +* add await to async call for anonymous sign in request ([`4734d0d`](https://github.com/supabase/auth-py/commit/4734d0df974d2a24a8cd9f468584343d5f7ea9c3)) + +## v2.5.3 (2024-06-23) + +### Chore + +* chore(release): bump version to v2.5.3 ([`b194f07`](https://github.com/supabase/auth-py/commit/b194f07c5cbe5e7b813e968a928e99cc5c6070e0)) + +### Fix + +* fix: update property name for should_create_user (#531) ([`388fcba`](https://github.com/supabase/auth-py/commit/388fcbac63bc96119c3c3ece91f28578e384a38b)) + +### Unknown + +* update peoperty name for should_create_user ([`4bc09ef`](https://github.com/supabase/auth-py/commit/4bc09ef2f61ec55e01485dfa606a9d6631e607a5)) + +## v2.5.2 (2024-06-21) + +### Chore + +* chore(release): bump version to v2.5.2 ([`00a04c6`](https://github.com/supabase/auth-py/commit/00a04c6637ccd2fb84700cea13dd4aaf705cc443)) + +* chore(deps-dev): bump faker from 25.8.0 to 25.9.1 (#529) ([`c530238`](https://github.com/supabase/auth-py/commit/c530238b2574921545980fc0bf170b21b6bef20c)) + +* chore(deps-dev): bump faker from 25.8.0 to 25.9.1 + +Bumps [faker](https://github.com/joke2k/faker) from 25.8.0 to 25.9.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v25.8.0...v25.9.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1651b74`](https://github.com/supabase/auth-py/commit/1651b74b63b61511ddd0a473002cf438c07285b5)) + +### Fix + +* fix: link identity methods now working ([`2e95db8`](https://github.com/supabase/auth-py/commit/2e95db8bcf0ba323b330ef56f1928f72523e0b0e)) + +### Unknown + +* fix all link identity methods ([`3570ace`](https://github.com/supabase/auth-py/commit/3570aced5ebcb22f448e987d4a157e7b6d2f0b29)) + +## v2.5.1 (2024-06-20) + +### Chore + +* chore(release): bump version to v2.5.1 ([`6563daf`](https://github.com/supabase/auth-py/commit/6563daf5bd249718a73872a33421b773076e05e7)) + +* chore(deps-dev): bump python-semantic-release from 9.8.1 to 9.8.3 (#526) ([`b7d5755`](https://github.com/supabase/auth-py/commit/b7d5755318c3a7929c057ef8a4ce0f9ac765389c)) + +* chore(deps-dev): bump python-semantic-release from 9.8.1 to 9.8.3 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.1 to 9.8.3. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.1...v9.8.3) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8e5ec68`](https://github.com/supabase/auth-py/commit/8e5ec68f61613bfe60c704b0f393a1bdf6c1b276)) + +* chore(deps-dev): bump faker from 25.4.0 to 25.8.0 (#519) ([`e4fc889`](https://github.com/supabase/auth-py/commit/e4fc8891bcafc94688e75a305dfc85d9fbbb60e9)) + +* chore(deps-dev): bump faker from 25.4.0 to 25.8.0 + +Bumps [faker](https://github.com/joke2k/faker) from 25.4.0 to 25.8.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v25.4.0...v25.8.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c730281`](https://github.com/supabase/auth-py/commit/c7302817c2fca068811fa507d582ce9ce7465494)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.8.1 to 9.8.3 (#527) ([`eda2715`](https://github.com/supabase/auth-py/commit/eda2715283b589942ce85c0a83708190fa595d0b)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.1 to 9.8.3. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.1...v9.8.3) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`941a87b`](https://github.com/supabase/auth-py/commit/941a87b3a62bb249fe01683e814a6f3700cc24f5)) + +* chore(deps-dev): bump urllib3 from 2.2.1 to 2.2.2 (#525) ([`8137d24`](https://github.com/supabase/auth-py/commit/8137d241a12f24e0dac3554c1ec7f34e504aaf87)) + +* chore(deps-dev): bump urllib3 from 2.2.1 to 2.2.2 + +Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2. +- [Release notes](https://github.com/urllib3/urllib3/releases) +- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) +- [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2) + +--- +updated-dependencies: +- dependency-name: urllib3 + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`097d3ac`](https://github.com/supabase/auth-py/commit/097d3ac788e4650358753dde48d93c3204940029)) + +* chore(deps): bump pydantic from 2.7.3 to 2.7.4 (#521) ([`0647d43`](https://github.com/supabase/auth-py/commit/0647d43940df2fc387dde01092cb0ea018711082)) + +* chore(deps): bump pydantic from 2.7.3 to 2.7.4 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.3 to 2.7.4. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.3...v2.7.4) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f7c60e7`](https://github.com/supabase/auth-py/commit/f7c60e727830bb1644416789a32ca0d3173658bc)) + +* chore(deps-dev): bump pytest from 8.2.1 to 8.2.2 (#516) ([`2c40cd6`](https://github.com/supabase/auth-py/commit/2c40cd6fdbfae74ac9c104d307a91d6dc5b21195)) + +* chore(deps-dev): bump pytest from 8.2.1 to 8.2.2 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.2.1 to 8.2.2. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e2f6c0a`](https://github.com/supabase/auth-py/commit/e2f6c0ab7bb962e2ed57a992d90bea91f91b2dd6)) + +### Fix + +* fix: add channel to sign_in and sign_up options ([`498640d`](https://github.com/supabase/auth-py/commit/498640dfa8804257bb62675078dc0487aacc879d)) + +### Unknown + +* Update supabase_auth/_sync/gotrue_client.py + +Co-authored-by: Joel Lee <lee.yi.jie.joel@gmail.com> ([`1df49ef`](https://github.com/supabase/auth-py/commit/1df49ef55ed64cce0a9afa3295e7c8c56b9fd450)) + +* Update supabase_auth/_async/gotrue_client.py + +Co-authored-by: Joel Lee <lee.yi.jie.joel@gmail.com> ([`f44450d`](https://github.com/supabase/auth-py/commit/f44450dc7294ab0bf25e16f81b5ef8423120264d)) + +* format with pre-commit formatter ([`97b534b`](https://github.com/supabase/auth-py/commit/97b534b9e5fd1ec5a04c0f7520aa078029de76a0)) + +* change option name from `redirect_to` to `email_redirect_to` ([`019da4f`](https://github.com/supabase/auth-py/commit/019da4fdbcc4ae92f227322ebd7a3485479d9943)) + +* add sync version ([`4814aeb`](https://github.com/supabase/auth-py/commit/4814aeb4b8c446dacc1086300260dcfefac76210)) + +* add channel to sign_in and sign_up options ([`5a1c44a`](https://github.com/supabase/auth-py/commit/5a1c44afebe5d01b31bacd25423f456baab35ef8)) + +## v2.5.0 (2024-06-20) + +### Chore + +* chore(release): bump version to v2.5.0 ([`a92ec80`](https://github.com/supabase/auth-py/commit/a92ec8068d8751d0864a7cde2cdb62bfb39c7071)) + +* chore(deps-dev): bump python-semantic-release from 9.8.0 to 9.8.1 (#515) ([`98006ed`](https://github.com/supabase/auth-py/commit/98006ed08d07bb2414b261ba3c926f29c0de3645)) + +* chore(deps-dev): bump python-semantic-release from 9.8.0 to 9.8.1 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.0 to 9.8.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.0...v9.8.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f28f6a2`](https://github.com/supabase/auth-py/commit/f28f6a29175a4af0dd00963b9718c65eda5d937a)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.8.0 to 9.8.1 (#514) ([`4458982`](https://github.com/supabase/auth-py/commit/445898229a3c504324a33a67d2bd9709bb8048ea)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.8.0 to 9.8.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.8.0...v9.8.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`960d593`](https://github.com/supabase/auth-py/commit/960d593f28be3cca582812a07d1fcfda6a3bb911)) + +### Feature + +* feat: add anonymous sign in ([`2406d7d`](https://github.com/supabase/auth-py/commit/2406d7d90394cf69bf772916c2232ae8aa8afe13)) + +### Unknown + +* update postgres version for infra ([`ad2179b`](https://github.com/supabase/auth-py/commit/ad2179b1c042c8faf1b1cdc322d8c9f9d5aae9e7)) + +* update async client and type naming ([`85f94f2`](https://github.com/supabase/auth-py/commit/85f94f27320af3438748fe261a752a29b03a2c24)) + +* Add Anonymous login ([`f3d35c7`](https://github.com/supabase/auth-py/commit/f3d35c72d3dc334d5963d4f9a57137eeacdff873)) + +## v2.4.4 (2024-06-04) + +### Chore + +* chore(release): bump version to v2.4.4 ([`1ed3dd7`](https://github.com/supabase/auth-py/commit/1ed3dd7389499ff2f8793f70adf51250cfe55964)) + +* chore(deps): bump pydantic from 2.7.2 to 2.7.3 (#513) ([`4a5e21e`](https://github.com/supabase/auth-py/commit/4a5e21e93f14a76d9a46b218163a2e9538537440)) + +* chore(deps): bump pydantic from 2.7.2 to 2.7.3 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.2 to 2.7.3. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.2...v2.7.3) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7b5269c`](https://github.com/supabase/auth-py/commit/7b5269c4254d598a41507f45ec4893cfa20a14ac)) + +* chore(deps-dev): bump faker from 25.3.0 to 25.4.0 (#512) ([`daafb74`](https://github.com/supabase/auth-py/commit/daafb74a4970a6fcbd9dc36ef757079616d1f150)) + +* chore(deps-dev): bump faker from 25.3.0 to 25.4.0 + +Bumps [faker](https://github.com/joke2k/faker) from 25.3.0 to 25.4.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v25.3.0...v25.4.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`35a8b14`](https://github.com/supabase/auth-py/commit/35a8b14dc5f6657e63d21de29dd2b60a29f07eed)) + +* chore(deps-dev): bump faker from 25.2.0 to 25.3.0 (#510) ([`e7fdc2d`](https://github.com/supabase/auth-py/commit/e7fdc2d978c95638dfc7a27676aabcba44d2a80d)) + +* chore(deps-dev): bump faker from 25.2.0 to 25.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 25.2.0 to 25.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v25.2.0...v25.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`0e55988`](https://github.com/supabase/auth-py/commit/0e55988f74c908c0fee5b629939a8fab3305052c)) + +### Fix + +* fix: follow redirects ([`1fa7fd2`](https://github.com/supabase/auth-py/commit/1fa7fd24c84594a6d63322a685aeccdc4e301af1)) + +### Unknown + +* Fix code style ([`4a18e21`](https://github.com/supabase/auth-py/commit/4a18e212fd5f9b9f25e516735ab92232bf49e5d7)) + +* Fix code style ([`55a415b`](https://github.com/supabase/auth-py/commit/55a415b993e503bed5a564b8db1490a26e6ad51f)) + +* Fix code style ([`645560d`](https://github.com/supabase/auth-py/commit/645560d940243bee5f2b15e59a3b50cd0ca11c5c)) + +* ReSync, fix merges ([`96129b6`](https://github.com/supabase/auth-py/commit/96129b65d3c5438aac5a67852978a7f7cf0b20dd)) + +## v2.4.3 (2024-06-01) + +### Chore + +* chore(release): bump version to v2.4.3 ([`7316cf5`](https://github.com/supabase/auth-py/commit/7316cf56dce5db188690c58f8a30e0cd4568105b)) + +* chore(deps): bump pydantic from 2.7.1 to 2.7.2 (#509) ([`5d433fe`](https://github.com/supabase/auth-py/commit/5d433fe665fcc08dad5dc49c4225a0d53b5fa7e6)) + +* chore(deps): bump pydantic from 2.7.1 to 2.7.2 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.1 to 2.7.2. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.1...v2.7.2) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`9f80a51`](https://github.com/supabase/auth-py/commit/9f80a51c2550e305c79eac312d582c1b8a516bee)) + +* chore(ci): bump python-semantic-release/python-semantic-release from 9.7.3 to 9.8.0 (#508) ([`a3037d9`](https://github.com/supabase/auth-py/commit/a3037d94cdfe0b419b2d8b3785783f36c843196c)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.7.3 to 9.8.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.7.3...v9.8.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7b21e47`](https://github.com/supabase/auth-py/commit/7b21e47fdc240d30add37ca1612105fac8777e4e)) + +* chore(deps-dev): bump python-semantic-release from 9.7.3 to 9.8.0 (#507) ([`fe033c5`](https://github.com/supabase/auth-py/commit/fe033c5b803a64637acc819d84a83be939cbfffd)) + +* chore(deps-dev): bump python-semantic-release from 9.7.3 to 9.8.0 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.7.3 to 9.8.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.7.3...v9.8.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`859379b`](https://github.com/supabase/auth-py/commit/859379bd86a763ebe8f82a2eb4492078b71fcd8a)) + +* chore(deps-dev): bump requests from 2.31.0 to 2.32.0 (#502) ([`16e571f`](https://github.com/supabase/auth-py/commit/16e571fc19168314d82074523de6f246bdd74960)) + +* chore(deps-dev): bump pytest from 8.1.1 to 8.2.1 (#501) ([`56a3842`](https://github.com/supabase/auth-py/commit/56a3842928e942b8d35946de27d99050c277de9b)) + +* chore(deps-dev): bump pytest from 8.1.1 to 8.2.1 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.1. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.1) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1e32b5c`](https://github.com/supabase/auth-py/commit/1e32b5ccedd7d1c77bcadaf01ab1a871f3d326eb)) + +* chore(deps-dev): bump pytest-asyncio from 0.23.6 to 0.23.7 (#500) ([`c0c17f8`](https://github.com/supabase/auth-py/commit/c0c17f86e8f1048818a23ef92bfcd08a8bd16879)) + +* chore(deps-dev): bump pytest-asyncio from 0.23.6 to 0.23.7 + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.6 to 0.23.7. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.6...v0.23.7) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8a72775`](https://github.com/supabase/auth-py/commit/8a72775dc968a9c81a18342422fa710b7987a651)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.5.0 to 9.7.3 (#499) ([`1b1fc19`](https://github.com/supabase/auth-py/commit/1b1fc19f28934c1d716e37153953a237b56a8bd0)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.5.0 to 9.7.3. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.5.0...v9.7.3) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`29ee762`](https://github.com/supabase/auth-py/commit/29ee76258bba9a0b4a1dd2249f3493c8d30e211e)) + +* chore(deps-dev): bump python-semantic-release from 9.5.0 to 9.7.3 (#498) ([`1b6fadf`](https://github.com/supabase/auth-py/commit/1b6fadf02f8d6d77bee9bfedaca5a85731173c67)) + +* chore(deps-dev): bump python-semantic-release from 9.5.0 to 9.7.3 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.5.0 to 9.7.3. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.5.0...v9.7.3) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`039d6a5`](https://github.com/supabase/auth-py/commit/039d6a5c5ec28cb4d9025020e33bf3f9bef801d5)) + +* chore(deps-dev): bump faker from 24.14.1 to 25.2.0 (#497) ([`67b6a58`](https://github.com/supabase/auth-py/commit/67b6a581d45e027a815b8b3f7bc6b3c9068f47b4)) + +* chore(deps-dev): bump faker from 24.14.1 to 25.2.0 + +Bumps [faker](https://github.com/joke2k/faker) from 24.14.1 to 25.2.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v24.14.1...v25.2.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`ce825c8`](https://github.com/supabase/auth-py/commit/ce825c80646943078d54743cdebe3e2f4f38fde9)) + +* chore(deps-dev): bump jinja2 from 3.1.3 to 3.1.4 (#491) ([`a030a33`](https://github.com/supabase/auth-py/commit/a030a33675ad08d37a729a9835c619e8550acbd2)) + +* chore(deps-dev): bump jinja2 from 3.1.3 to 3.1.4 + +Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. +- [Release notes](https://github.com/pallets/jinja/releases) +- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) +- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) + +--- +updated-dependencies: +- dependency-name: jinja2 + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`71f1a6e`](https://github.com/supabase/auth-py/commit/71f1a6ef3cd8ed0595f0e7f4b588227f225cad31)) + +* chore(deps-dev): bump faker from 24.9.0 to 24.14.1 (#480) ([`9776d6a`](https://github.com/supabase/auth-py/commit/9776d6adcce8d8ca28e826d14b05743a1a63e132)) + +* chore(deps-dev): bump faker from 24.9.0 to 24.14.1 + +Bumps [faker](https://github.com/joke2k/faker) from 24.9.0 to 24.14.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v24.9.0...v24.14.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e7e3b58`](https://github.com/supabase/auth-py/commit/e7e3b58bb855145bc8b7aa1cf36065e7da3ea849)) + +* chore(deps-dev): bump black from 24.3.0 to 24.4.2 (#478) ([`ac8339b`](https://github.com/supabase/auth-py/commit/ac8339bee847677fdf1bc8cbe5c67098e8651afd)) + +* chore(deps-dev): bump black from 24.3.0 to 24.4.2 + +Bumps [black](https://github.com/psf/black) from 24.3.0 to 24.4.2. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/24.3.0...24.4.2) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`db0d0e9`](https://github.com/supabase/auth-py/commit/db0d0e999fddfeca3684d46fcbba3ebe5d015ab3)) + +* chore(deps): bump pydantic from 2.7.0 to 2.7.1 (#476) ([`339155a`](https://github.com/supabase/auth-py/commit/339155a60d3047c85fef907961a409ec5113e2b2)) + +* chore(deps): bump pydantic from 2.7.0 to 2.7.1 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.0 to 2.7.1. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.0...v2.7.1) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e0c7dac`](https://github.com/supabase/auth-py/commit/e0c7dac96076ff5d30b85a7f4982f690512cb89e)) + +* chore(deps-dev): bump python-semantic-release from 9.4.1 to 9.5.0 (#475) ([`8900536`](https://github.com/supabase/auth-py/commit/8900536a649aa40badb9331f4ee304d8cfaa83d9)) + +* chore(deps-dev): bump python-semantic-release from 9.4.1 to 9.5.0 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.4.1 to 9.5.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.4.1...v9.5.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`aced040`](https://github.com/supabase/auth-py/commit/aced040ce04afd520c34e6f8502a995f847e9838)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.4.1 to 9.5.0 (#474) ([`1d951b6`](https://github.com/supabase/auth-py/commit/1d951b6ed982a92ca85abed4eaedefd7ac0e1017)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.4.1 to 9.5.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.4.1...v9.5.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`d28719f`](https://github.com/supabase/auth-py/commit/d28719fae2529c5b781954264387b82e96022405)) + +* chore(deps-dev): bump faker from 24.4.0 to 24.9.0 (#469) ([`9ae0b1d`](https://github.com/supabase/auth-py/commit/9ae0b1d51bb08b3396e0c1b66526d63565f108a3)) + +* chore(deps-dev): bump faker from 24.4.0 to 24.9.0 + +Bumps [faker](https://github.com/joke2k/faker) from 24.4.0 to 24.9.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v24.4.0...v24.9.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8db5aab`](https://github.com/supabase/auth-py/commit/8db5aab39588e66f8a2fd51bb1644cb6fd103925)) + +* chore(deps): bump pydantic from 2.6.4 to 2.7.0 (#468) ([`6cee4e2`](https://github.com/supabase/auth-py/commit/6cee4e28d7ef97f736fa0f260dfc65c203676e85)) + +* chore(deps): bump pydantic from 2.6.4 to 2.7.0 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.6.4 to 2.7.0. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.6.4...v2.7.0) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1b888da`](https://github.com/supabase/auth-py/commit/1b888da2bf670cdd9017b26ac57eb3760e9d3c77)) + +* chore(deps): bump idna from 3.6 to 3.7 (#467) ([`75feed7`](https://github.com/supabase/auth-py/commit/75feed766c44dd5581989fb3244ab74cc5f088d3)) + +* chore(deps): bump idna from 3.6 to 3.7 + +Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7. +- [Release notes](https://github.com/kjd/idna/releases) +- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) +- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7) + +--- +updated-dependencies: +- dependency-name: idna + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e9fe838`](https://github.com/supabase/auth-py/commit/e9fe838da1ba1e6c095231e8197b726aa2e7af5f)) + +* chore(deps-dev): bump python-semantic-release from 9.4.0 to 9.4.1 (#464) ([`d659e82`](https://github.com/supabase/auth-py/commit/d659e8248242671648d3d2c1e69af9afea47a77f)) + +* chore(deps-dev): bump python-semantic-release from 9.4.0 to 9.4.1 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.4.0 to 9.4.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.4.0...v9.4.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`13feeae`](https://github.com/supabase/auth-py/commit/13feeae252b6a11c2a67a3724b2cca44f4e57688)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.4.0 to 9.4.1 (#463) ([`066014e`](https://github.com/supabase/auth-py/commit/066014e59ce2e4e8364aeb10352d263846a6902f)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.4.0 to 9.4.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.4.0...v9.4.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`b536cf6`](https://github.com/supabase/auth-py/commit/b536cf6676fbdd4d7bd4eb0a5beb6f98e6f7bcc2)) + +* chore(deps-dev): bump python-semantic-release from 9.3.1 to 9.4.0 (#462) ([`abf77ea`](https://github.com/supabase/auth-py/commit/abf77ea9702425f43b3efef9053b045dd6ca34c9)) + +* chore(deps-dev): bump python-semantic-release from 9.3.1 to 9.4.0 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.3.1 to 9.4.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.3.1...v9.4.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`05b6f2d`](https://github.com/supabase/auth-py/commit/05b6f2da2059cf79e579e8274e2ba260388b3409)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.3.1 to 9.4.0 (#461) ([`d11910c`](https://github.com/supabase/auth-py/commit/d11910cc387fc907c10ace25fc09db935b039386)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.3.1 to 9.4.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.3.1...v9.4.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`4031e43`](https://github.com/supabase/auth-py/commit/4031e43a322a759c5ffb3b626cb43c575b2378ec)) + +* chore(deps-dev): bump pytest-cov from 4.1.0 to 5.0.0 (#458) ([`17e6587`](https://github.com/supabase/auth-py/commit/17e658735512d3dfe1ea021ea24a48bd4e716b5f)) + +* chore(deps-dev): bump pytest-cov from 4.1.0 to 5.0.0 + +Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0. +- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0) + +--- +updated-dependencies: +- dependency-name: pytest-cov + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`10b44fe`](https://github.com/supabase/auth-py/commit/10b44fe5795ddfe015807880b68c875ac57fea58)) + +* chore(deps): bump abatilo/actions-poetry from 2 to 3 (#460) ([`4376a7a`](https://github.com/supabase/auth-py/commit/4376a7ae7d259991c97da019cdbe39dcb0a35a2b)) + +* chore(deps): bump abatilo/actions-poetry from 2 to 3 + +Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 2 to 3. +- [Release notes](https://github.com/abatilo/actions-poetry/releases) +- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc) +- [Commits](https://github.com/abatilo/actions-poetry/compare/v2...v3) + +--- +updated-dependencies: +- dependency-name: abatilo/actions-poetry + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`cac82ad`](https://github.com/supabase/auth-py/commit/cac82adc513e9217fcc5e24c45f1cb7f804abad0)) + +* chore(deps): bump actions/cache from 3 to 4 (#459) ([`67bc67f`](https://github.com/supabase/auth-py/commit/67bc67fb005e1d318fdbe2979afd22e689492044)) + +* chore(deps): bump actions/cache from 3 to 4 + +Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. +- [Release notes](https://github.com/actions/cache/releases) +- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) +- [Commits](https://github.com/actions/cache/compare/v3...v4) + +--- +updated-dependencies: +- dependency-name: actions/cache + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7e1f6f7`](https://github.com/supabase/auth-py/commit/7e1f6f707e545be93bf8479520450fb1e2c59aef)) + +* chore(deps-dev): bump faker from 23.3.0 to 24.4.0 (#456) ([`1562b15`](https://github.com/supabase/auth-py/commit/1562b15aaddc5aa7bfb0043b4fcc4db0dda91779)) + +* chore(deps-dev): bump faker from 23.3.0 to 24.4.0 + +Bumps [faker](https://github.com/joke2k/faker) from 23.3.0 to 24.4.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v23.3.0...v24.4.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`d44b0ea`](https://github.com/supabase/auth-py/commit/d44b0ea2143154dca08b4554a337a024c80ddf2f)) + +### Fix + +* fix: add "verify" flag to the creation of client ([`12f2396`](https://github.com/supabase/auth-py/commit/12f239668a5e49723e6fc92f4f1b07cf7d8f0c87)) + +### Unknown + +* Fix #320 ([`e3862b9`](https://github.com/supabase/auth-py/commit/e3862b951cb16dd7c80617925f93febf7be57cd8)) + +* Fix #320 ([`e6203f6`](https://github.com/supabase/auth-py/commit/e6203f68ef91b74830c438575bd1ec89bae159cb)) + +* Fix #320 ([`d3ee089`](https://github.com/supabase/auth-py/commit/d3ee08910db712c729ddf05c27ef3e41a8fc7951)) + +* Fix #320 ([`339fb59`](https://github.com/supabase/auth-py/commit/339fb591d855612da451266d4f9daeab973865f6)) + +* Fix #320 ([`05d9fe9`](https://github.com/supabase/auth-py/commit/05d9fe9683699038df9007fa7b0f9ba763c81fed)) + +* Fix #320 ([`eba8528`](https://github.com/supabase/auth-py/commit/eba8528d749f7887f853072e586494dceace64a7)) + +* Fix #320 ([`2940262`](https://github.com/supabase/auth-py/commit/29402627fea628329018718e6434c761ae3375f6)) + +* Fix #320 ([`cf2f9be`](https://github.com/supabase/auth-py/commit/cf2f9be0c39b841ea6971e223b12cb580e32dd09)) + +* + +--- +updated-dependencies: +- dependency-name: requests + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`a2423da`](https://github.com/supabase/auth-py/commit/a2423da474b3abf5bb5512b98c9aef508f6d5722)) + +* Fixes and style (#488) ([`7856d01`](https://github.com/supabase/auth-py/commit/7856d014fbec6a49264d57e579701f50bf434239)) + +* Fixes and style ([`4f723eb`](https://github.com/supabase/auth-py/commit/4f723eb7be48c1dda251918b4c3241f8ff6c78e7)) + +* Fixes and style ([`0165846`](https://github.com/supabase/auth-py/commit/01658461ab7c1fe20c21e6093a5871b2da4f51c7)) + +* Update .pre-commit-config.yaml (#487) ([`e00452c`](https://github.com/supabase/auth-py/commit/e00452c242b1b313bd80262238bb5166c3a16874)) + +* Update .pre-commit-config.yaml ([`9824403`](https://github.com/supabase/auth-py/commit/9824403d785269340cc29dc49e840b743a2d2974)) + +* Update .pre-commit-config.yaml ([`d0ceba0`](https://github.com/supabase/auth-py/commit/d0ceba0fac7bab61c76f49e6958ef31ae76ed361)) + +* Update .pre-commit-config.yaml ([`e2470f0`](https://github.com/supabase/auth-py/commit/e2470f0e60c0c6ee6b1e091616b4a2c794656ef4)) + +* Update .pre-commit-config.yaml ([`d0757be`](https://github.com/supabase/auth-py/commit/d0757be5ab3a5762d59f33ccd129ea862d4f3180)) + +* Update .pre-commit-config.yaml ([`da1c0cd`](https://github.com/supabase/auth-py/commit/da1c0cdedf35dcb3aa4005bab606f36b28b812c8)) + +* Update .pre-commit-config.yaml ([`f0890cb`](https://github.com/supabase/auth-py/commit/f0890cbbb3d0c949c6bb529d7e127c9c16aa2e6a)) + +* Update .pre-commit-config.yaml ([`17a4db5`](https://github.com/supabase/auth-py/commit/17a4db52f253bac7c7b839c3c7e3bcba9782d511)) + +* Update .pre-commit-config.yaml ([`83e6021`](https://github.com/supabase/auth-py/commit/83e6021dbbc4818db65840bea261d0798885d9d5)) + +* Add follow_redirects=True ([`fab26ac`](https://github.com/supabase/auth-py/commit/fab26ac5bd8eca3d2aa5a3413943bfbb4aca6417)) + +* Add follow_redirects=True ([`5eea191`](https://github.com/supabase/auth-py/commit/5eea191bcb26a7faf2b5c3496eabd00ccb3ae1c8)) + +* Add follow_redirects=True ([`7b38f75`](https://github.com/supabase/auth-py/commit/7b38f7544b090029b6105a51f5099eee7de59d61)) + +* Add stale bot (#484) ([`7059504`](https://github.com/supabase/auth-py/commit/7059504d95ffccc3d9cef0dad130de28543772bf)) + +* Add stale bot ([`87b27b6`](https://github.com/supabase/auth-py/commit/87b27b63db1f99a64601de4ed16213c0e7ec7d63)) + +## v2.4.2 (2024-03-26) + +### Chore + +* chore(release): bump version to v2.4.2 ([`bdabba3`](https://github.com/supabase/auth-py/commit/bdabba35d7703ac1370964cea2502499ed9f0c28)) + +* chore(deps-dev): bump black from 23.10.1 to 24.3.0 (#446) ([`4850a9f`](https://github.com/supabase/auth-py/commit/4850a9fad7384fae24d9fcec8b89bdaf94eab47b)) + +* chore(deps-dev): bump black from 23.10.1 to 24.3.0 + +Bumps [black](https://github.com/psf/black) from 23.10.1 to 24.3.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/23.10.1...24.3.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`5a2ab10`](https://github.com/supabase/auth-py/commit/5a2ab106d9167ddc7d5c51afc37b5f94d22e3f33)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 9.1.1 to 9.3.1 (#452) ([`31a67fd`](https://github.com/supabase/auth-py/commit/31a67fdc2e852c8fa38f56e1a88f0b37bfa68ae6)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 9.1.1 to 9.3.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.1.1...v9.3.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1b1ce42`](https://github.com/supabase/auth-py/commit/1b1ce429ce9836c7937fae474b1001f0951acf8b)) + +* chore: rename package to supabase_auth (#455) ([`3b63cd6`](https://github.com/supabase/auth-py/commit/3b63cd6dc7fd7e136c88e849c032da696fa977cf)) + +* chore(deps): bump httpx from 0.25.0 to 0.27.0 (#431) ([`3914bb6`](https://github.com/supabase/auth-py/commit/3914bb675e09189418caabb6ca2af6aacaea6582)) + +* chore(deps): bump httpx from 0.25.0 to 0.27.0 + +Bumps [httpx](https://github.com/encode/httpx) from 0.25.0 to 0.27.0. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.25.0...0.27.0) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`60da4b5`](https://github.com/supabase/auth-py/commit/60da4b5b124755ced7ed570c4b7551904a94306c)) + +* chore(deps-dev): bump pytest from 8.0.2 to 8.1.0 (#443) ([`de90b46`](https://github.com/supabase/auth-py/commit/de90b462a9e49b36e6a5298ae3119bc94ec9fba1)) + +* chore(deps-dev): bump pytest from 8.0.2 to 8.1.0 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.0.2 to 8.1.0. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.0) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`91f26fa`](https://github.com/supabase/auth-py/commit/91f26fa55cc8cad38ecab55ccebf61691d01980a)) + +* chore: bump fixed version of poetry (#438) ([`9344e79`](https://github.com/supabase/auth-py/commit/9344e796cb93b803fb395710e678a0bf6801f7ca)) + +* chore(deps-dev): bump faker from 19.12.1 to 23.3.0 (#436) ([`6a9e1ce`](https://github.com/supabase/auth-py/commit/6a9e1ce5ecdf060a086f91a5f7a0f0733be4c6f8)) + +* chore(deps-dev): bump faker from 19.12.1 to 23.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 19.12.1 to 23.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v19.12.1...v23.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7bd2a71`](https://github.com/supabase/auth-py/commit/7bd2a712c562f638322ddc38de6c5178625bb660)) + +* chore(deps-dev): bump pytest from 7.4.3 to 8.0.2 (#439) ([`cb5f6a6`](https://github.com/supabase/auth-py/commit/cb5f6a6104944e0e0fa0b8afa48f989f1fe30e80)) + +* chore(deps-dev): bump pytest from 7.4.3 to 8.0.2 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 8.0.2. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.3...8.0.2) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`0c04828`](https://github.com/supabase/auth-py/commit/0c04828812369a772d9bd1be1ed2da5ac5e78ece)) + +* chore(deps-dev): bump cryptography from 42.0.0 to 42.0.4 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.0 to 42.0.4. +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/42.0.0...42.0.4) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`9066970`](https://github.com/supabase/auth-py/commit/90669704b42d9dd4e8a242d7b57b301ca197534a)) + +* chore(deps-dev): bump python-semantic-release from 8.3.0 to 9.1.1 (#440) ([`0d41e57`](https://github.com/supabase/auth-py/commit/0d41e5735e6e573c77783fd29f71fae6c08b5d63)) + +* chore(deps-dev): bump python-semantic-release from 8.3.0 to 9.1.1 + +Bumps [python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 8.3.0 to 9.1.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v8.3.0...v9.1.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7f2e12d`](https://github.com/supabase/auth-py/commit/7f2e12d7691aa41c77f4791c974511ebda69d430)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 8.3.0 to 9.1.1 (#435) ([`a9789f7`](https://github.com/supabase/auth-py/commit/a9789f726ed82f06f74649c997f0ed40731d1c3c)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 8.3.0 to 9.1.1. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v8.3.0...v9.1.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7468b29`](https://github.com/supabase/auth-py/commit/7468b29b4240104c61578f158a98907c9777c94c)) + +* chore(deps-dev): bump pytest-asyncio from 0.21.1 to 0.23.5 (#424) ([`262547b`](https://github.com/supabase/auth-py/commit/262547b4acb652963033e2daab7ee283983d2f20)) + +* chore(deps-dev): bump pytest-asyncio from 0.21.1 to 0.23.5 + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.21.1 to 0.23.5. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.21.1...v0.23.5) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f88b538`](https://github.com/supabase/auth-py/commit/f88b5384c7b7ba1dbf440f67680ed307a7040845)) + +* chore(deps): bump pydantic from 2.4.2 to 2.6.3 (#437) ([`0de3da7`](https://github.com/supabase/auth-py/commit/0de3da7a6d78fa700324abf673b89568ab2fddec)) + +* chore(deps): bump pydantic from 2.4.2 to 2.6.3 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.4.2 to 2.6.3. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v2.4.2...v2.6.3) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`4d1984d`](https://github.com/supabase/auth-py/commit/4d1984db5ecabc4d7ace6fc7fa183e1aeddc7b5b)) + +* chore(deps): bump codecov/codecov-action from 3 to 4 (#415) ([`f374e91`](https://github.com/supabase/auth-py/commit/f374e916809777c44e86984fa3d4b57d477a5996)) + +* chore(deps): bump codecov/codecov-action from 3 to 4 + +Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. +- [Release notes](https://github.com/codecov/codecov-action/releases) +- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) +- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) + +--- +updated-dependencies: +- dependency-name: codecov/codecov-action + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`db2307a`](https://github.com/supabase/auth-py/commit/db2307a155a4692c90503da78ef959268f0e3db1)) + +* chore(deps): bump abatilo/actions-poetry from 2.3.0 to 3.0.0 (#405) ([`63e028d`](https://github.com/supabase/auth-py/commit/63e028dbf62922de2ec8cf886942c3e80922b9d7)) + +* chore(deps): bump abatilo/actions-poetry from 2.3.0 to 3.0.0 + +Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 2.3.0 to 3.0.0. +- [Release notes](https://github.com/abatilo/actions-poetry/releases) +- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc) +- [Commits](https://github.com/abatilo/actions-poetry/compare/v2.3.0...v3.0.0) + +--- +updated-dependencies: +- dependency-name: abatilo/actions-poetry + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f52f7e2`](https://github.com/supabase/auth-py/commit/f52f7e2793ca37ee7943bfe6e38510ec8bc1ca31)) + +### Fix + +* fix: new minor version release (#457) ([`b831bb1`](https://github.com/supabase/auth-py/commit/b831bb1c1dcd316983ce2b8d15d4dac7c6280a6b)) + +### Unknown + +* package release ([`fd971bd`](https://github.com/supabase/auth-py/commit/fd971bd84d7431d67b15940659e81382e622ba4b)) + +* update formatting ([`54a5bc1`](https://github.com/supabase/auth-py/commit/54a5bc146789b6421a9fc30be1f26d1e2749f2d1)) + +* rename package to supabase_auth ([`55a0f06`](https://github.com/supabase/auth-py/commit/55a0f06b59c932925f4689bebafc1dc26e9c2797)) + +* update ci to include python 3.12 in tests (#451) ([`cacb4b6`](https://github.com/supabase/auth-py/commit/cacb4b623ba85b8a65509298fd83a11f8f4ab3cc)) + +* Update project description and repository links ([`9bd18d1`](https://github.com/supabase/auth-py/commit/9bd18d1e86a2969be11b1fd321d55ab4d6bd87e7)) + +* update ci to include python 3.12 in tests ([`2350d43`](https://github.com/supabase/auth-py/commit/2350d4379460adf677dd3b6ecdd1031243087d07)) + +* Update manual_pypi_publish.yml ([`af2d2e5`](https://github.com/supabase/auth-py/commit/af2d2e51d59f3e0f3d8ead126e06beabd99030dd)) + +* feat (deps-dev): bump cryptography from 42.0.0 to 42.0.4 (#430) ([`26de4cb`](https://github.com/supabase/auth-py/commit/26de4cbbade04cece7af71d43752833496359ee6)) + +## v2.4.1 (2024-02-09) + +### Chore + +* chore(release): bump version to v2.4.1 ([`1c8cd12`](https://github.com/supabase/auth-py/commit/1c8cd1240c5f7cbcd03f54924c91590fa6f7f2ca)) + +* chore(deps-dev): bump cryptography from 41.0.6 to 42.0.0 (#416) ([`f03e518`](https://github.com/supabase/auth-py/commit/f03e5182a850607000717944bfb390b181294080)) + +* chore(deps-dev): bump cryptography from 41.0.6 to 42.0.0 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.6 to 42.0.0. +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/41.0.6...42.0.0) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7d91ca2`](https://github.com/supabase/auth-py/commit/7d91ca2c612a29ccbcfc16d4ff8b030aa5e72d05)) + +### Fix + +* fix: add AuthOtpResponse (#419) ([`130bdb1`](https://github.com/supabase/auth-py/commit/130bdb1ba47f8e18800ee052cb1d1e1538482ed4)) + +* fix: move changes to _async ([`7810bbd`](https://github.com/supabase/auth-py/commit/7810bbd868f202adf2bef1e0a8573973f8b415be)) + +* fix: add AuthOtpResponse ([`8d64ca4`](https://github.com/supabase/auth-py/commit/8d64ca450b37f7063b7fc701ee3156d1677d4fc3)) + +### Unknown + +* formatting ([`97c4713`](https://github.com/supabase/auth-py/commit/97c47138215ee04af14478d913300032964f7a8e)) + +* revert changes to parse_auth_response ([`face521`](https://github.com/supabase/auth-py/commit/face52175ce75bcb074680c9a464fbb0ee320ae5)) + +## v2.4.0 (2024-01-31) + +### Chore + +* chore(release): bump version to v2.4.0 ([`640d0c1`](https://github.com/supabase/auth-py/commit/640d0c1cf37edca0b69a189c2b4b637c09b047d0)) + +* chore(deps-dev): bump jinja2 from 3.1.2 to 3.1.3 (#403) ([`0d58002`](https://github.com/supabase/auth-py/commit/0d580021af269a2e18e4a7864a88462c37334ecd)) + +* chore(deps-dev): bump jinja2 from 3.1.2 to 3.1.3 + +Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3. +- [Release notes](https://github.com/pallets/jinja/releases) +- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) +- [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3) + +--- +updated-dependencies: +- dependency-name: jinja2 + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f439d3f`](https://github.com/supabase/auth-py/commit/f439d3f4baf4edb301fa0e66c40b350dc2112597)) + +* chore(deps-dev): bump gitpython from 3.1.40 to 3.1.41 (#401) ([`8c8cd65`](https://github.com/supabase/auth-py/commit/8c8cd65ae7506c32d0fd040ea146c8cd2a15c73e)) + +* chore(deps-dev): bump gitpython from 3.1.40 to 3.1.41 + +Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.40 to 3.1.41. +- [Release notes](https://github.com/gitpython-developers/GitPython/releases) +- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) +- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.40...3.1.41) + +--- +updated-dependencies: +- dependency-name: gitpython + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`a60428c`](https://github.com/supabase/auth-py/commit/a60428ca97985a0b1210a08eee0866e8eca24fe2)) + +### Feature + +* feat: add new identity linking methods release in LWX (#395) ([`96d18be`](https://github.com/supabase/auth-py/commit/96d18becda00478bacd08f32b61e75adc07d48fe)) + +* feat: add sso (#358) ([`c250f93`](https://github.com/supabase/auth-py/commit/c250f93cf5e50049629e5bb8f7d6b5c3fd408a42)) + +* feat: add docstring ([`dba16fb`](https://github.com/supabase/auth-py/commit/dba16fbc2a3ec688587abb75bad541da8055c016)) + +* feat: move skip_http_redirect to options ([`0547a2a`](https://github.com/supabase/auth-py/commit/0547a2a740bbf7620331cf371b12f42a3dacc801)) + +* feat: skip http redirects ([`8dca4a5`](https://github.com/supabase/auth-py/commit/8dca4a555718f463c54dba22aa77f77b748dc472)) + +* feat: add sso ([`99c9409`](https://github.com/supabase/auth-py/commit/99c9409ef2398b10bb7563ca3cbde0b0204ec88d)) + +* feat: add pydantic type ([`1308119`](https://github.com/supabase/auth-py/commit/1308119b80b3685852a20ec322fd8bea4315fd75)) + +* feat: add get_user_identities ([`2de717a`](https://github.com/supabase/auth-py/commit/2de717aa1785585ff872e4f94df891673b16858a)) + +* feat: link and unlink ([`6103511`](https://github.com/supabase/auth-py/commit/6103511a459d13713b55713161f634df30a40ca4)) + +### Fix + +* fix: run black ([`89bcd15`](https://github.com/supabase/auth-py/commit/89bcd155c560ac404bc9f5e649027d3873661b0f)) + +* fix: apply sourcery suggestions ([`e225504`](https://github.com/supabase/auth-py/commit/e22550464d209e817118259caa06a06e3c494a42)) + +* fix: update types ([`51566e4`](https://github.com/supabase/auth-py/commit/51566e420002f965e29bcc9bf71510a144aa7e3c)) + +### Unknown + +* Merge branch 'main' into j0/add_linking_methods ([`ed9edab`](https://github.com/supabase/auth-py/commit/ed9edab57179fcf0e9a4cb11d615f6c137e9e8da)) + +* Update gotrue/_async/gotrue_base_api.py ([`72eeb1e`](https://github.com/supabase/auth-py/commit/72eeb1ebd2fbe6d7b66a7fccd08d5b538b742508)) + +* Merge branch 'main' of github.com:supabase-community/gotrue-py into j0/add_linking_methods ([`851e523`](https://github.com/supabase/auth-py/commit/851e5233070b96dd022cc2a78c074fb0789f3c11)) + +## v2.3.0 (2024-01-12) + +### Chore + +* chore(release): bump version to v2.3.0 ([`17fa5d2`](https://github.com/supabase/auth-py/commit/17fa5d23daab9344c1f1b47b30c69e62803f7e06)) + +* chore: update to pull auth instead of gotrue ([`38977a6`](https://github.com/supabase/auth-py/commit/38977a6596f426805cf5fb106e7fdbbae311394c)) + +* chore: pull latest postgres image instead ([`e1877b2`](https://github.com/supabase/auth-py/commit/e1877b230f960d5eda7b1ad6b11b8a87a7513487)) + +### Feature + +* feat: add sync methods ([`7c9f642`](https://github.com/supabase/auth-py/commit/7c9f642c9a6448707683fe6b54d3be4102aa8506)) + +* feat: add sync methods ([`280dc40`](https://github.com/supabase/auth-py/commit/280dc406c4e031b6c4681c56a36a9c9ddefd15d9)) + +* feat: pin version ([`5a90fff`](https://github.com/supabase/auth-py/commit/5a90fff802ff2c4860694d472243d69e8bedce1e)) + +### Fix + +* fix: add linking methods ([`987adf2`](https://github.com/supabase/auth-py/commit/987adf2c112398be252d1fd9563cc8763dfc0b6f)) + +* fix: add linking methods ([`a29083a`](https://github.com/supabase/auth-py/commit/a29083a840200d984e9b969569eeb16185e5b598)) + +* fix: fix build by updating error message in test (#404) ([`ac94c60`](https://github.com/supabase/auth-py/commit/ac94c60529984d7cf82b422f0c65bc3b01e43732)) + +* fix: update error message in tests ([`6718dcd`](https://github.com/supabase/auth-py/commit/6718dcd19f3707647077ccd5cfa413bcbe718fd4)) + +### Unknown + +* Merge branch 'j0/add_linking_methods' of github.com:supabase-community/gotrue-py into j0/add_linking_methods ([`fa834cf`](https://github.com/supabase/auth-py/commit/fa834cfaca27cadcc7e536ee41d68702c6ae33b5)) + +* Update MAINTAINERS.md ([`5681011`](https://github.com/supabase/auth-py/commit/568101128a27280600da86780590fce62025410e)) + +## v2.2.0 (2023-12-13) + +### Chore + +* chore(release): bump version to v2.2.0 ([`6db7cc2`](https://github.com/supabase/auth-py/commit/6db7cc25cbca86e9908d2f268a46fd5f066aa5e3)) + +* chore(deps): bump actions/setup-python from 4 to 5 (#382) ([`55c04e2`](https://github.com/supabase/auth-py/commit/55c04e2121948b26ecd6af63c07e643892f05b45)) + +* chore(deps): bump actions/setup-python from 4 to 5 + +Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. +- [Release notes](https://github.com/actions/setup-python/releases) +- [Commits](https://github.com/actions/setup-python/compare/v4...v5) + +--- +updated-dependencies: +- dependency-name: actions/setup-python + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c6f378a`](https://github.com/supabase/auth-py/commit/c6f378a409489f136f5808d1b6aeb859ac95636f)) + +### Feature + +* feat: update docs (#386) ([`1bb53b0`](https://github.com/supabase/auth-py/commit/1bb53b060c7716c6d99e540f3a400f7f83d551e0)) + +* feat: update docs ([`d2c9693`](https://github.com/supabase/auth-py/commit/d2c969338f7e4685782947d696d547ffe4da45ee)) + +### Unknown + +* Update README.md ([`62983ac`](https://github.com/supabase/auth-py/commit/62983ac31011b2fd1a810f1f2583632f88f3526c)) + +## v2.1.0 (2023-12-07) + +### Chore + +* chore(release): bump version to v2.1.0 ([`a1f060c`](https://github.com/supabase/auth-py/commit/a1f060c55d3a18fae8a616d2b4f7cd3cd2afc867)) + +### Feature + +* feat: add sign_out() scope option (#381) ([`4ec8842`](https://github.com/supabase/auth-py/commit/4ec884227c57da32774dc107a2469d36befe4e8d)) + +* feat: add sign_out() scope option ([`3818dba`](https://github.com/supabase/auth-py/commit/3818dba635705d747b7da84fe8ab4dc2c5dcc9ab)) + +### Unknown + +* Update to use query property of the request method ([`34a3ddf`](https://github.com/supabase/auth-py/commit/34a3ddfe8b9343c15b6959338a353aeb5e530b70)) + +## v2.0.0 (2023-11-30) + +### Breaking + +* feat: exchange code for session now fully async + +BREAKING CHANGE: change async method on_auth_state_change to sync only. ([`a249ba0`](https://github.com/supabase/auth-py/commit/a249ba03cc7d99b2d1805480ca89ee457ad865f1)) + +### Chore + +* chore(release): bump version to v2.0.0 ([`1440dd6`](https://github.com/supabase/auth-py/commit/1440dd6099cb5c7f8220223ea3a0e4d966e8ce60)) + +* chore(deps-dev): bump cryptography from 41.0.5 to 41.0.6 (#377) ([`e6b3d46`](https://github.com/supabase/auth-py/commit/e6b3d46fba5f24631fdd5fdb1b8d1688a1087053)) + +* chore(deps-dev): bump cryptography from 41.0.5 to 41.0.6 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.5 to 41.0.6. +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/41.0.5...41.0.6) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`424a5df`](https://github.com/supabase/auth-py/commit/424a5df56f91ac30fab056bd9128d4dac004c565)) + +### Feature + +* feat: exchange code for session now fully async (#378) ([`c294568`](https://github.com/supabase/auth-py/commit/c2945684a8ad4d8710ca72428b4ec16aab87bad1)) + +### Unknown + +* bug fix: use pydantic v2 model.model_rebuild (#373) ([`fd94314`](https://github.com/supabase/auth-py/commit/fd94314c4412d49610acf8d3166abe660f6a4ee4)) + +* bug fix: use pydantic v2 model.model_rebuild not rebuild_model ([`9d723df`](https://github.com/supabase/auth-py/commit/9d723df7637fb501f83aad3562a49e2ed24bb9a2)) + +* add soft delete support to "delete user" (#376) ([`08bada3`](https://github.com/supabase/auth-py/commit/08bada3692f230cf1bd793a7eb60a12d62fec0c6)) + +* add soft delete support to "delete user" ([`5471167`](https://github.com/supabase/auth-py/commit/54711675533ce58fdf3017f250e92a88db719aad)) + +## v1.3.1 (2023-11-29) + +### Chore + +* chore(release): bump version to v1.3.1 ([`ede20fe`](https://github.com/supabase/auth-py/commit/ede20fea097fd900df2efc6231c14ea86f486087)) + +### Fix + +* fix: remove unnecessary async to on_auth_state_change (#374) ([`574c739`](https://github.com/supabase/auth-py/commit/574c739500dd304aa6d09c69122373b6e4a5be01)) + +* fix: remove unnecessary async to on_auth_state_change + +Somehow this got reverted on a refactor (https://github.com/supabase-community/gotrue-py/commit/e7ebc64112d970673265c7b314a1e8820fc0f7e1) + +This causes problems when using the supabase client, since it's not being awaited: +https://github.com/supabase-community/supabase-py/blob/main/supabase/_async/client.py#L90 ([`7548d02`](https://github.com/supabase/auth-py/commit/7548d0290199bdb1053564b953932c53aabdea29)) + +## v1.3.0 (2023-11-01) + +### Chore + +* chore(release): bump version to v1.3.0 ([`abe3e2a`](https://github.com/supabase/auth-py/commit/abe3e2a871d97df4a91f2c4ae3a3c05e56e6e083)) + +* chore: update CI config with PAT (#361) ([`fbddd6d`](https://github.com/supabase/auth-py/commit/fbddd6dbbeb67895f6d804f6a9fd804247ccf0c4)) + +* chore: update CI config with PAT ([`1fffc1f`](https://github.com/supabase/auth-py/commit/1fffc1fdebca39c35a9e69ea3d25240e13eb73b7)) + +* chore(deps-dev): bump pygithub from 1.59.1 to 2.1.1 (#337) ([`160b671`](https://github.com/supabase/auth-py/commit/160b671de0ecab9d40a6caf4c6553d8808974b1f)) + +* chore(deps-dev): bump pygithub from 1.59.1 to 2.1.1 + +Bumps [pygithub](https://github.com/pygithub/pygithub) from 1.59.1 to 2.1.1. +- [Release notes](https://github.com/pygithub/pygithub/releases) +- [Changelog](https://github.com/PyGithub/PyGithub/blob/main/doc/changes.rst) +- [Commits](https://github.com/pygithub/pygithub/compare/v1.59.1...v2.1.1) + +--- +updated-dependencies: +- dependency-name: pygithub + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`264e773`](https://github.com/supabase/auth-py/commit/264e77314e5a50e0b325d906e11998be933339d2)) + +* chore: update dependencies (#357) ([`a5d4a81`](https://github.com/supabase/auth-py/commit/a5d4a812d1cc4bb8624a33853088a1710b1a501d)) + +* chore: update dependencies ([`dc67de1`](https://github.com/supabase/auth-py/commit/dc67de1b9e56fd7d07e98cba73969ec458ce4d3c)) + +* chore(deps-dev): bump urllib3 from 2.0.4 to 2.0.7 (#350) ([`4ea73e4`](https://github.com/supabase/auth-py/commit/4ea73e42892f63a3065e3fc9cc059ae940cefdc1)) + +* chore(deps-dev): bump urllib3 from 2.0.4 to 2.0.7 + +Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.4 to 2.0.7. +- [Release notes](https://github.com/urllib3/urllib3/releases) +- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) +- [Commits](https://github.com/urllib3/urllib3/compare/2.0.4...2.0.7) + +--- +updated-dependencies: +- dependency-name: urllib3 + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`2400229`](https://github.com/supabase/auth-py/commit/240022942e7513b67e5db3e26e4f9a9be77f7f83)) + +* chore(deps): bump python-semantic-release/python-semantic-release from 8.0.8 to 8.3.0 (#354) ([`a385575`](https://github.com/supabase/auth-py/commit/a385575e960426090c5aa6b8395c43c706015f72)) + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 8.0.8 to 8.3.0. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v8.0.8...v8.3.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`3fbe6fc`](https://github.com/supabase/auth-py/commit/3fbe6fcbefde561833a45c07df565ad00c448669)) + +* chore(deps): bump httpx from 0.24.1 to 0.25.0 + +Bumps [httpx](https://github.com/encode/httpx) from 0.24.1 to 0.25.0. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.24.1...0.25.0) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`a5b2f85`](https://github.com/supabase/auth-py/commit/a5b2f85c68a4157c41eec606fcd041f89969b08e)) + +### Feature + +* feat: add OAuth PKCE (#331) ([`8fe633e`](https://github.com/supabase/auth-py/commit/8fe633e5cfff20d748e8eb5dfdb82c2cbd9390fd)) + +* feat: allow dev to pass in code_verifier ([`f33bf3e`](https://github.com/supabase/auth-py/commit/f33bf3ed43b930f8ef43756693c3af514ddfaa62)) + +### Fix + +* fix: add latest semantic-release dependency (#360) ([`81e63ea`](https://github.com/supabase/auth-py/commit/81e63ea56b2b0a611852609c03abee79e2bda6ef)) + +* fix: add latest semantic-release dependency ([`251a4f4`](https://github.com/supabase/auth-py/commit/251a4f49f85a9489362833c02f932dad06f4d8ec)) + +* fix: fix typo ([`72ee18d`](https://github.com/supabase/auth-py/commit/72ee18db05b8d1ea2b916a4bfea074459df9ecf0)) + +* fix: update imports ([`66759da`](https://github.com/supabase/auth-py/commit/66759da715a1d1511cf2518b5a1119bdfa205088)) + +### Unknown + +* patch: read from storage ([`364292c`](https://github.com/supabase/auth-py/commit/364292c0bb1c8362f6da4a198ea6a2fe2d98fd8e)) + +* Merge branch 'j0/pkce' of github.com:supabase-community/gotrue-py into j0/pkce ([`1c40ed7`](https://github.com/supabase/auth-py/commit/1c40ed76ddeb1916278bb7b4827d8631b7749066)) + +* Merge branch 'main' into j0/pkce ([`4c44238`](https://github.com/supabase/auth-py/commit/4c4423859375b2d796f6527689e1ebace36860e3)) + +* Merge pull request #325 from supabase-community/dependabot/pip/main/httpx-0.25.0 + +chore(deps): bump httpx from 0.24.1 to 0.25.0 ([`89019fb`](https://github.com/supabase/auth-py/commit/89019fb8ecf2d7b1609bf4d0114b552ec9062e6d)) + +* Merge pull request #352 from fbeutel/urlsafe_b64decode + +Use urlsafe_b64decode to properly handle URL-encoded JWTs ([`7541ade`](https://github.com/supabase/auth-py/commit/7541ade5e09349b477d64afc5a80774fd4be0f07)) + +* Use urlsafe_b64decode to properly handle URL-encoded JWTs ([`0034af1`](https://github.com/supabase/auth-py/commit/0034af1816b54c043079ef251df6ac2f76d5b41b)) + +## v1.2.0 (2023-10-05) + +### Chore + +* chore(release): bump version to v1.2.0 ([`8da1032`](https://github.com/supabase/auth-py/commit/8da10323c27adf9976c1eaab4ac82aca560ee578)) + +* chore(deps-dev): bump cryptography from 41.0.3 to 41.0.4 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4. +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`4aa7224`](https://github.com/supabase/auth-py/commit/4aa7224e414df9c7ddc822d29d79e0db777dd7d3)) + +### Feature + +* feat: Add exception to handle API errors on signout ([`59b90d5`](https://github.com/supabase/auth-py/commit/59b90d545f59cb00b08b3a5c02edcd9a4a229538)) + +### Fix + +* fix: patch imports ([`3b61aff`](https://github.com/supabase/auth-py/commit/3b61aff1574a6adbbc49cd9ef81e86ff3b35535d)) + +* fix: run pre-commit ([`754c7ab`](https://github.com/supabase/auth-py/commit/754c7abe6f30d429d2606a24b3f63667fc96c530)) + +* fix: add pkce ([`02f7c05`](https://github.com/supabase/auth-py/commit/02f7c05c24c656d079c4be149d98140411445c37)) + +### Unknown + +* Merge pull request #342 from supabase-community/silentworks/sign-out-exception-handling + +feat: Add exception to handle API errors on signout ([`708859c`](https://github.com/supabase/auth-py/commit/708859c1fe12535910ef5d8385f42a6aabda2613)) + +* Update to use suppress instead of try except ([`2d964ad`](https://github.com/supabase/auth-py/commit/2d964ad892e3a40204904319d499f7da9aa9289a)) + +* Merge pull request #341 from supabase-community/silentworks/add-correct-semantic-release-vars + +Add correct variables for semantic release ([`5155312`](https://github.com/supabase/auth-py/commit/5155312a31fd669a6f62616ea75036386a2fc004)) + +* Get pre-commit to ignore changelog ([`738602e`](https://github.com/supabase/auth-py/commit/738602ea6ba013ff9071115ee65842c26f4098cf)) + +* Add correct variables for semantic release ([`80b0e30`](https://github.com/supabase/auth-py/commit/80b0e30cc5ddf510f2ea1891aab247bde7c01ccc)) + +* Merge pull request #330 from supabase-community/dependabot/pip/cryptography-41.0.4 + +chore(deps-dev): bump cryptography from 41.0.3 to 41.0.4 ([`1bf3e5d`](https://github.com/supabase/auth-py/commit/1bf3e5dc03f9d0da1f939001ec120e1ef0c69420)) + +* Merge pull request #332 from supabase-community/sourcery/j0/pkce + +feat: add OAuth PKCE (Sourcery refactored) ([`b331d04`](https://github.com/supabase/auth-py/commit/b331d043ce056a9a09355e14989e036e86db9c59)) + +* 'Refactored by Sourcery' ([`af4f842`](https://github.com/supabase/auth-py/commit/af4f842d8fdf59fcef04b853660627b6b24a18a4)) + +## v1.1.1 (2023-09-22) + +### Chore + +* chore(deps): bump python-semantic-release/python-semantic-release + +Bumps [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) from 8.0.0 to 8.0.8. +- [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v8.0.0...v8.0.8) + +--- +updated-dependencies: +- dependency-name: python-semantic-release/python-semantic-release + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8f094df`](https://github.com/supabase/auth-py/commit/8f094df3e6d8ba21f2c429b8c8c208c42e18f68c)) + +* chore(deps-dev): bump pytest from 7.4.0 to 7.4.2 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.2. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.0...7.4.2) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`cb41e8b`](https://github.com/supabase/auth-py/commit/cb41e8ba11c913411a4fd5ce19ec597ef04ff3d5)) + +* chore(deps-dev): bump pre-commit from 3.3.3 to 3.4.0 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.3.3 to 3.4.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.3.3...v3.4.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`133eca2`](https://github.com/supabase/auth-py/commit/133eca2d76c8e69d5be439a2f0067a767377b83a)) + +* chore(deps-dev): bump black from 23.7.0 to 23.9.1 + +Bumps [black](https://github.com/psf/black) from 23.7.0 to 23.9.1. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/23.7.0...23.9.1) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`5617a6a`](https://github.com/supabase/auth-py/commit/5617a6a8443db03aa829ae8ba4129b7341d11442)) + +* chore(deps-dev): bump faker from 19.3.0 to 19.6.1 + +Bumps [faker](https://github.com/joke2k/faker) from 19.3.0 to 19.6.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v19.3.0...v19.6.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`ab39f66`](https://github.com/supabase/auth-py/commit/ab39f668f3128810baef1d7db49ad6f0cfaa8521)) + +### Fix + +* fix: add verify token hash ([`da51e8e`](https://github.com/supabase/auth-py/commit/da51e8ea262fa113da7bd77c4e67ba14c7262851)) + +### Refactor + +* refactor: remove unused v1 files ([`e9005e4`](https://github.com/supabase/auth-py/commit/e9005e4c3ee4e4a9a344993c6e3120dc30832eac)) + +### Unknown + +* 1.1.1 + +Automatically generated by python-semantic-release ([`152ed06`](https://github.com/supabase/auth-py/commit/152ed06f65369ad1f15e6d105bd9d93d1b79da12)) + +* Merge pull request #328 from supabase-community/feat/add-verify-token-hash + +fix: add verify token hash ([`ccb2173`](https://github.com/supabase/auth-py/commit/ccb2173eb694622be100eec36942ec8a69b151a7)) + +* Fixed formatting ([`82a04aa`](https://github.com/supabase/auth-py/commit/82a04aaee7c6cb1047091789007f6366bbe1a535)) + +* Merge pull request #303 from supabase-community/dependabot/github_actions/main/python-semantic-release/python-semantic-release-8.0.8 + +chore(deps): bump python-semantic-release/python-semantic-release from 8.0.0 to 8.0.8 ([`cb6abab`](https://github.com/supabase/auth-py/commit/cb6abab03286a9305e480c1754c4ddfcde649196)) + +* Merge pull request #310 from supabase-community/dependabot/pip/main/pytest-7.4.2 + +chore(deps-dev): bump pytest from 7.4.0 to 7.4.2 ([`1df12a7`](https://github.com/supabase/auth-py/commit/1df12a7d5078b5ffb8bf611ae14e18da0609e9f6)) + +* Merge pull request #323 from supabase-community/j0/remove_unused_v1 + +refactor: remove unused v1 files ([`817fafb`](https://github.com/supabase/auth-py/commit/817fafb2720f4f201f940cf37f6e4262743241e8)) + +* Merge pull request #305 from supabase-community/dependabot/pip/main/pre-commit-3.4.0 + +chore(deps-dev): bump pre-commit from 3.3.3 to 3.4.0 ([`687c52d`](https://github.com/supabase/auth-py/commit/687c52daa2ef2aa4dd35348eaff83e6891f98750)) + +* Merge pull request #317 from supabase-community/dependabot/pip/main/black-23.9.1 + +chore(deps-dev): bump black from 23.7.0 to 23.9.1 ([`b0144ef`](https://github.com/supabase/auth-py/commit/b0144ef0ab12e07eea5526822a5cd0a7a3548097)) + +* Merge pull request #318 from supabase-community/dependabot/pip/main/faker-19.6.1 + +chore(deps-dev): bump faker from 19.3.0 to 19.6.1 ([`2cc68f9`](https://github.com/supabase/auth-py/commit/2cc68f944e03c61962d4da559da174f592717f18)) + +* Merge pull request #315 from jantznick/main + +fix get_user calls fail when no session or jwt is available ([`2a562a7`](https://github.com/supabase/auth-py/commit/2a562a749c12866c42dc16c9783e8b36823b253b)) + +* changelog changed from precommit hooks ([`5cf279e`](https://github.com/supabase/auth-py/commit/5cf279e6711c4e235e4b83dea874fa1eea2a6695)) + +* update return type for get_user calls ([`ddeb595`](https://github.com/supabase/auth-py/commit/ddeb5951cb5a8f7b2fc8c5157d74feb1f339be3d)) + +* return None when no session or jwt on get_user calls ([`50fa8b0`](https://github.com/supabase/auth-py/commit/50fa8b00000d1a4dd54506fba4dc6c90cd893006)) + +## v1.1.0 (2023-09-08) + +### Chore + +* chore(deps): bump actions/checkout from 3 to 4 + +Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. +- [Release notes](https://github.com/actions/checkout/releases) +- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) +- [Commits](https://github.com/actions/checkout/compare/v3...v4) + +--- +updated-dependencies: +- dependency-name: actions/checkout + dependency-type: direct:production + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`2a51fff`](https://github.com/supabase/auth-py/commit/2a51fff932e6eb9773614cf4cd5a72400c9c2aef)) + +* chore(deps-dev): bump gitpython from 3.1.32 to 3.1.35 + +Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.32 to 3.1.35. +- [Release notes](https://github.com/gitpython-developers/GitPython/releases) +- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) +- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.32...3.1.35) + +--- +updated-dependencies: +- dependency-name: gitpython + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7b0fb17`](https://github.com/supabase/auth-py/commit/7b0fb1711c030697cc724900034e504ca97dc324)) + +* chore: run formatting on CHANGELOG to correct for mix line endings ([`3f47338`](https://github.com/supabase/auth-py/commit/3f47338c81c95cc89d349b73d4e080eebf708cfa)) + +### Feature + +* feat: support pagination for admin list_users ([`e7cbd9f`](https://github.com/supabase/auth-py/commit/e7cbd9f17a3c5690d77954b2b57c267ea5382afd)) + +### Unknown + +* 1.1.0 + +Automatically generated by python-semantic-release ([`ff35a92`](https://github.com/supabase/auth-py/commit/ff35a928ef5c5cb8a5559a122fe55a868be0c023)) + +* Merge pull request #307 from supabase-community/dependabot/github_actions/main/actions/checkout-4 + +chore(deps): bump actions/checkout from 3 to 4 ([`64c0c66`](https://github.com/supabase/auth-py/commit/64c0c66f452f5d149fcbe61dc88c74baf9056584)) + +* Merge pull request #312 from supabase-community/dependabot/pip/gitpython-3.1.35 + +chore(deps-dev): bump gitpython from 3.1.32 to 3.1.35 ([`9b62e68`](https://github.com/supabase/auth-py/commit/9b62e6861d579ea437f450065a456c3a0a8eacab)) + +* Merge pull request #304 from connorlurring/admin-list-users-pagination + +feat: support pagination for auth admin list_users ([`1b88c51`](https://github.com/supabase/auth-py/commit/1b88c51bf29f69dc066377f94fe38dadb66b12cb)) + +* Merge pull request #311 from supabase-community/j0/lint_changelog + +chore: run formatting on CHANGELOG to correct for mix line endings ([`8684125`](https://github.com/supabase/auth-py/commit/86841257aefbbbc986e6ec4779a2c9373cf61c85)) + +## v1.0.4 (2023-08-23) + +### Chore + +* chore: add contents permission ([`a2a4912`](https://github.com/supabase/auth-py/commit/a2a49127a59b759e1a7336d9caf33bf277cf8a7f)) + +* chore: upgrade smenatic release ([`0cf22c2`](https://github.com/supabase/auth-py/commit/0cf22c2709f294d404549e141cae64d6c240fde5)) + +* chore(deps-dev): bump faker from 19.2.0 to 19.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 19.2.0 to 19.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v19.2.0...v19.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`29f1a1e`](https://github.com/supabase/auth-py/commit/29f1a1e1d10182c2c3e3fc837beff0371457cac2)) + +* chore(deps-dev): bump faker from 18.13.0 to 19.2.0 + +Bumps [faker](https://github.com/joke2k/faker) from 18.13.0 to 19.2.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v18.13.0...v19.2.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`0a03065`](https://github.com/supabase/auth-py/commit/0a030658bcc6b3bd3b522dec576174c1dc56a1ad)) + +### Fix + +* fix: update more pyproject.toml config options ([`3c38c2b`](https://github.com/supabase/auth-py/commit/3c38c2bb252e45d1d7a9f0b856134e269ac5db4c)) + +* fix: convert version_toml to array ([`d9c555d`](https://github.com/supabase/auth-py/commit/d9c555d334aa7eaf15ea1a73109e76ed6e86abd0)) + +* fix: add relevant pypi info and permissions ([`875484e`](https://github.com/supabase/auth-py/commit/875484ede1f71e2a87992bc52381bc2c649f7966)) + +* fix: add new providers ([`b4c1681`](https://github.com/supabase/auth-py/commit/b4c16818ba781fe597766d81816c2be5a8a8265e)) + +* fix: add type definitions for new providers ([`e033cee`](https://github.com/supabase/auth-py/commit/e033cee2e42ae318614dd59a9ba02b8f15ce5600)) + +### Unknown + +* 1.0.4 + +Automatically generated by python-semantic-release ([`5cc198b`](https://github.com/supabase/auth-py/commit/5cc198b631189a0622a23b71e8d5a4acba1de005)) + +* Merge pull request #299 from supabase-community/j0/fix_version_toml + +chore: add contents permission ([`7561df9`](https://github.com/supabase/auth-py/commit/7561df9a8b7b0279763e318f2a48b54693278c9b)) + +* Merge pull request #298 from supabase-community/j0/fix_version_toml + +fix: update version options ([`b66f958`](https://github.com/supabase/auth-py/commit/b66f9589be39526340152c8523c5130dde90ea0f)) + +* Merge pull request #297 from supabase-community/j0/fix_semantic_release + +fix: fix semantic release ([`3c9984b`](https://github.com/supabase/auth-py/commit/3c9984b2f89828f304b8f4fa1702b209ef5243bf)) + +* Merge pull request #290 from supabase-community/dependabot/pip/main/faker-19.3.0 + +chore(deps-dev): bump faker from 19.2.0 to 19.3.0 ([`52474bc`](https://github.com/supabase/auth-py/commit/52474bc1237ef0e2562f09b09aadb73e90e706e0)) + +* Merge pull request #293 from supabase-community/or/pydantic-v1-v2-support + +Support for pydantic v1 & v2 ([`6765f07`](https://github.com/supabase/auth-py/commit/6765f0778ec16afe66cddccd7067af0451838b7a)) + +* revert TypeAdapter to parse_obj_as for pydantic v1 support ([`79cd743`](https://github.com/supabase/auth-py/commit/79cd7438b1482742fd832044b90a51953609944d)) + +* run tests with pydantic v1 under CI ([`9c4aa2a`](https://github.com/supabase/auth-py/commit/9c4aa2abe4d8d711a7d85b4a9a6ccc6e93b02eab)) + +* pydantic v1 & v2 compatibility ([`45456c8`](https://github.com/supabase/auth-py/commit/45456c8e4c29d5a2846cd74679091c6f055f8158)) + +* Merge pull request #289 from yuvanist/main + +Specify minor version of pydantic to avoid dependency issue ([`c2ed950`](https://github.com/supabase/auth-py/commit/c2ed950516e4b9bee6eb8f066969049b9a361a28)) + +* Update lock file ([`9258777`](https://github.com/supabase/auth-py/commit/92587777ea9d1d844e25d5c4905ff8f24d50b369)) + +* specify minor version of pydantic ([`2b0bdb4`](https://github.com/supabase/auth-py/commit/2b0bdb4e347aaba33820dcf9b404da448148b004)) + +* Merge pull request #288 from supabase-community/dependabot/pip/main/faker-19.2.0 + +chore(deps-dev): bump faker from 18.13.0 to 19.2.0 ([`69f5994`](https://github.com/supabase/auth-py/commit/69f59947e62e4ec808376595bbffc53170f49aae)) + +## v1.0.3 (2023-08-03) + +### Chore + +* chore: bump ci file ([`cfe3acb`](https://github.com/supabase/auth-py/commit/cfe3acbba295d30b1360269f891449ca2fe7fa13)) + +* chore(deps-dev): bump pygithub from 1.58.0 to 1.59.0 + +Bumps [pygithub](https://github.com/pygithub/pygithub) from 1.58.0 to 1.59.0. +- [Release notes](https://github.com/pygithub/pygithub/releases) +- [Changelog](https://github.com/PyGithub/PyGithub/blob/main/doc/changes.rst) +- [Commits](https://github.com/pygithub/pygithub/compare/v1.58.0...v1.59.0) + +--- +updated-dependencies: +- dependency-name: pygithub + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`57ae5f7`](https://github.com/supabase/auth-py/commit/57ae5f79261545977d97c39f76d2cc9e868b65b1)) + +* chore(deps-dev): bump pytest-asyncio from 0.20.3 to 0.21.1 + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.3 to 0.21.1. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.3...v0.21.1) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`d5f937f`](https://github.com/supabase/auth-py/commit/d5f937f3c2d377336c93537d46cc53969ad1f1f6)) + +* chore(deps-dev): bump python-semantic-release from 7.33.2 to 7.34.6 + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.33.2 to 7.34.6. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.33.2...v7.34.6) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`9ac7669`](https://github.com/supabase/auth-py/commit/9ac7669f42fc709b3e4d59b4aa2d1dbfff0b93a6)) + +* chore(deps): bump cryptography from 39.0.1 to 41.0.0 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.1 to 41.0.0. +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/39.0.1...41.0.0) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`461aa75`](https://github.com/supabase/auth-py/commit/461aa75350e8214b942f869ccd8d719e4313eb36)) + +### Feature + +* feat: release new version ([`0cf8072`](https://github.com/supabase/auth-py/commit/0cf80722fc7fd69c8adf93ce83fa11f68f531b8e)) + +### Fix + +* fix: revert semantic release ([`aea52c2`](https://github.com/supabase/auth-py/commit/aea52c251664ee1496f9af875dc2ab09f823d467)) + +* fix: bump version ([`da8be6f`](https://github.com/supabase/auth-py/commit/da8be6fc3b9ee9ada78acfc110bdb7c8ec354b91)) + +### Unknown + +* Merge pull request #287 from supabase-community/j0/release_new_ver + +feat: release v1.0.3 ([`a6f8fd6`](https://github.com/supabase/auth-py/commit/a6f8fd6d9af1572abacb73a10471c7a6bfa160aa)) + +* Revert "chore: bump ci file" + +This reverts commit cfe3acbba295d30b1360269f891449ca2fe7fa13. ([`baa692c`](https://github.com/supabase/auth-py/commit/baa692c46b2270dfec2697dbe26f58fb016bf593)) + +* Merge pull request #279 from supabase-community/dependabot/pip/main/pygithub-1.59.0 + +chore(deps-dev): bump pygithub from 1.58.0 to 1.59.0 ([`86b9648`](https://github.com/supabase/auth-py/commit/86b9648683af94e314d2c0c8f6d3c4a49be81b8a)) + +* Merge pull request #286 from yuvanist/main + +Upgrade gotrue-py to pydantic > 2.1.x ([`86ac440`](https://github.com/supabase/auth-py/commit/86ac4405b266e6c33f10888352316fd7c5c917a5)) + +* change lock file ([`6ca5a7d`](https://github.com/supabase/auth-py/commit/6ca5a7dbfc7cb8c56b7b9c430745b370954ce109)) + +* Change pydantic version to 2.1 ([`1daebe0`](https://github.com/supabase/auth-py/commit/1daebe01db2e309ae76c4d7f50d39e05536afca6)) + +* reformat ([`3aa81c7`](https://github.com/supabase/auth-py/commit/3aa81c75c1edccb73b8ad8c845afd2ae58fa1522)) + +* change to model_rebuild ([`cd053cc`](https://github.com/supabase/auth-py/commit/cd053ccc1e377cad73b2b6858ba3eb802a8ce2db)) + +* update poetry lock ([`e99b5b5`](https://github.com/supabase/auth-py/commit/e99b5b5236ad8040387014dcc9a83ea98382d90d)) + +* root_validator to model_validator ([`0dee34e`](https://github.com/supabase/auth-py/commit/0dee34e493c199c38691c7c3a88ee3315f6b3f3d)) + +* Upgrade to Pydantic V2 - Initial Changes ([`e59bb96`](https://github.com/supabase/auth-py/commit/e59bb96ccb3ca4db9b25ebb6c20c40f488e53a47)) + +* Merge pull request #284 from supabase-community/dependabot/pip/main/pytest-asyncio-0.21.1 + +chore(deps-dev): bump pytest-asyncio from 0.20.3 to 0.21.1 ([`743d6c1`](https://github.com/supabase/auth-py/commit/743d6c1e38966100d8d05c1abfc650cdc2b9e530)) + +* Merge pull request #278 from supabase-community/dependabot/pip/main/python-semantic-release-7.34.6 + +chore(deps-dev): bump python-semantic-release from 7.33.2 to 7.34.6 ([`aa12b1b`](https://github.com/supabase/auth-py/commit/aa12b1b98c56e8e371c00b9f2534187cbd196d48)) + +* Merge pull request #280 from supabase-community/j0/update_version + +fix: bump __init__ version to 1.02 ([`99b9907`](https://github.com/supabase/auth-py/commit/99b9907018f3ecdd8bda91fd794f2e4d6fd4fb7b)) + +* Merge pull request #275 from supabase-community/dependabot/pip/cryptography-41.0.0 + +chore(deps): bump cryptography from 39.0.1 to 41.0.0 ([`6b0b687`](https://github.com/supabase/auth-py/commit/6b0b687d2a896bba179e5cd445a7a255ee2f37a8)) + +## v1.0.2 (2023-06-01) + +### Chore + +* chore(deps-dev): bump pytest from 7.2.2 to 7.3.1 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.1. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.2...7.3.1) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`9c334f2`](https://github.com/supabase/auth-py/commit/9c334f23d1d529f29de26a1a99d16b00a457a263)) + +* chore(deps-dev): bump black from 23.1.0 to 23.3.0 + +Bumps [black](https://github.com/psf/black) from 23.1.0 to 23.3.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/23.1.0...23.3.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`0c26730`](https://github.com/supabase/auth-py/commit/0c2673043d453c680a034f452ed3a5cb957aaac7)) + +* chore(deps): bump pydantic from 1.10.5 to 1.10.8 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.5 to 1.10.8. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.8/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.5...v1.10.8) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8a8aba2`](https://github.com/supabase/auth-py/commit/8a8aba2ba0c2370b9697fa854a8e1be3fd28b7b4)) + +* chore(deps): bump requests from 2.28.2 to 2.31.0 + +Bumps [requests](https://github.com/psf/requests) from 2.28.2 to 2.31.0. +- [Release notes](https://github.com/psf/requests/releases) +- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) +- [Commits](https://github.com/psf/requests/compare/v2.28.2...v2.31.0) + +--- +updated-dependencies: +- dependency-name: requests + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`883c218`](https://github.com/supabase/auth-py/commit/883c218172eaee7264625a004c17c033471218a7)) + +* chore(deps): bump httpx from 0.23.3 to 0.24.1 + +Bumps [httpx](https://github.com/encode/httpx) from 0.23.3 to 0.24.1. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.23.3...0.24.1) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`f184d7a`](https://github.com/supabase/auth-py/commit/f184d7a6f6185b0861677b9835b4f4fe1b467a33)) + +* chore(deps-dev): bump pre-commit from 3.1.1 to 3.2.2 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.1.1 to 3.2.2. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.1.1...v3.2.2) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`5c5e384`](https://github.com/supabase/auth-py/commit/5c5e384fe14eae908796e5f9df4dc56efd1682b6)) + +### Feature + +* feat: update README ([`a0a0d79`](https://github.com/supabase/auth-py/commit/a0a0d79d4e1608dce31093b69eed48fd8bd664f8)) + +### Fix + +* fix: parse_auth_response to handle cases when data is an empty dictionary ([`71f1b07`](https://github.com/supabase/auth-py/commit/71f1b072bc9959618870996991b621ef7463f20e)) + +### Refactor + +* refactor: parse_auth_response handles cases when data does not have key user ([`f533b92`](https://github.com/supabase/auth-py/commit/f533b922d0c99007d1fe56ee99c6852c48a51403)) + +### Unknown + +* Merge pull request #273 from supabase-community/j0/release_1_0_2 + +feat: update README, release v1.0.2 ([`8477576`](https://github.com/supabase/auth-py/commit/84775765f46accc81eed8da8cddf408a51afba58)) + +* Merge pull request #267 from supabase-community/dependabot/pip/main/pytest-7.3.1 + +chore(deps-dev): bump pytest from 7.2.2 to 7.3.1 ([`29fb30b`](https://github.com/supabase/auth-py/commit/29fb30b0105dfcb2176be73b94e0d37ed0db3577)) + +* Merge pull request #258 from supabase-community/dependabot/pip/main/black-23.3.0 + +chore(deps-dev): bump black from 23.1.0 to 23.3.0 ([`a5beade`](https://github.com/supabase/auth-py/commit/a5beadeed6ed9605a5c64def1025c1fc49d7776c)) + +* Merge pull request #269 from supabase-community/dependabot/pip/main/pydantic-1.10.8 + +chore(deps): bump pydantic from 1.10.5 to 1.10.8 ([`4845101`](https://github.com/supabase/auth-py/commit/4845101ef546e30fe6f01eb1b90d9ada2bed645e)) + +* Merge pull request #268 from supabase-community/dependabot/pip/requests-2.31.0 + +chore(deps): bump requests from 2.28.2 to 2.31.0 ([`15f2dbd`](https://github.com/supabase/auth-py/commit/15f2dbd7db45b1c7631a818538ce5cf4c1b3a5d6)) + +* Merge pull request #264 from lmoj/double-urlencode + +fix issue #246 ([`dca7d9a`](https://github.com/supabase/auth-py/commit/dca7d9a4219230d3f99b69d00b84bcd84f0dd12f)) + +* fix import ([`034da90`](https://github.com/supabase/auth-py/commit/034da901685a6022e6f16a8588386d54c43b2594)) + +* fix url double encode ([`69a4f4a`](https://github.com/supabase/auth-py/commit/69a4f4a6a03ab30ea0c0d9862fa7c15191588f21)) + +* Merge pull request #266 from supabase-community/dependabot/pip/main/httpx-0.24.1 + +chore(deps): bump httpx from 0.23.3 to 0.24.1 ([`0efdd9d`](https://github.com/supabase/auth-py/commit/0efdd9d1e206fcac8aa9b62b45b86e2120f835d1)) + +* Merge pull request #265 from lmoj/optional-last_sign_in_at + +optional last_sign_in_at in UserIdentity type ([`99d4567`](https://github.com/supabase/auth-py/commit/99d45678572b4ea7ad91c2340a5753a263a40703)) + +* optional last_sign_in_at in UserIdentity type ([`6fef36a`](https://github.com/supabase/auth-py/commit/6fef36a583018c113f72e927a6598adf4948669b)) + +* Merge pull request #261 from hgseo16/main + +fix: parse_auth_response to handle cases when data is an empty dictio… ([`633c4ab`](https://github.com/supabase/auth-py/commit/633c4ab5207d12991d0e97c35786ed699ac175b9)) + +* Merge pull request #260 from supabase-community/dependabot/pip/main/pre-commit-3.2.2 + +chore(deps-dev): bump pre-commit from 3.1.1 to 3.2.2 ([`736f334`](https://github.com/supabase/auth-py/commit/736f3342f2f379728ea578b4804f3f919f7a476b)) + +## v1.0.1 (2023-04-03) + +### Chore + +* chore(deps-dev): bump faker from 18.3.0 to 18.3.1 + +Bumps [faker](https://github.com/joke2k/faker) from 18.3.0 to 18.3.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v18.3.0...v18.3.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`11c55d5`](https://github.com/supabase/auth-py/commit/11c55d56c67ccec3d2a69fd0918cd8a5a65d9e52)) + +* chore(deps-dev): bump faker from 17.6.0 to 18.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 17.6.0 to 18.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v17.6.0...v18.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`cff3a01`](https://github.com/supabase/auth-py/commit/cff3a0113c8fca149a21a3b8feb0f512ceb992c3)) + +* chore(deps-dev): bump python-semantic-release from 7.33.1 to 7.33.2 + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.33.1 to 7.33.2. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.33.1...v7.33.2) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`70c4ed6`](https://github.com/supabase/auth-py/commit/70c4ed670927eaba03a323752b2539f7be35f5e1)) + +* chore(deps-dev): bump faker from 17.3.0 to 17.6.0 + +Bumps [faker](https://github.com/joke2k/faker) from 17.3.0 to 17.6.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v17.3.0...v17.6.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8ba18fd`](https://github.com/supabase/auth-py/commit/8ba18fd6d16bf98fbd8fb5cff186ec4322ce6189)) + +* chore(deps): bump abatilo/actions-poetry from 2.2.0 to 2.3.0 + +Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 2.2.0 to 2.3.0. +- [Release notes](https://github.com/abatilo/actions-poetry/releases) +- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc) +- [Commits](https://github.com/abatilo/actions-poetry/compare/v2.2.0...v2.3.0) + +--- +updated-dependencies: +- dependency-name: abatilo/actions-poetry + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e28fd25`](https://github.com/supabase/auth-py/commit/e28fd25a03f09468237a08accc7546210461d4ea)) + +* chore(deps-dev): bump pytest from 7.2.1 to 7.2.2 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`fc53e5b`](https://github.com/supabase/auth-py/commit/fc53e5b659e0bfbf27f6276e4450565722097e12)) + +* chore(deps-dev): bump faker from 17.0.0 to 17.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 17.0.0 to 17.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v17.0.0...v17.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`ee32bd9`](https://github.com/supabase/auth-py/commit/ee32bd90bdf4e2e598dbaf777d747dedb98e02ff)) + +* chore(deps-dev): bump pre-commit from 3.1.0 to 3.1.1 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.1.0 to 3.1.1. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.1.0...v3.1.1) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`2bf3dce`](https://github.com/supabase/auth-py/commit/2bf3dcee6dab4287e03d16905891a51b1a79fab9)) + +* chore(deps-dev): bump pre-commit from 3.0.4 to 3.1.0 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.0.4 to 3.1.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v3.0.4...v3.1.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c9b9f95`](https://github.com/supabase/auth-py/commit/c9b9f959f158c808ac1cbe0576dd53f5e798f654)) + +* chore(deps-dev): bump pygithub from 1.57 to 1.58.0 + +Bumps [pygithub](https://github.com/pygithub/pygithub) from 1.57 to 1.58.0. +- [Release notes](https://github.com/pygithub/pygithub/releases) +- [Changelog](https://github.com/PyGithub/PyGithub/blob/master/doc/changes.rst) +- [Commits](https://github.com/pygithub/pygithub/compare/v1.57...v1.58.0) + +--- +updated-dependencies: +- dependency-name: pygithub + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c6f0b49`](https://github.com/supabase/auth-py/commit/c6f0b49d971ac5aaa0a77eccf13c623f6d23e5f2)) + +* chore(deps-dev): bump faker from 16.8.1 to 17.0.0 + +Bumps [faker](https://github.com/joke2k/faker) from 16.8.1 to 17.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v16.8.1...v17.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`911f5cb`](https://github.com/supabase/auth-py/commit/911f5cba28f0ccd0b1194ec414d08d0db416dbdf)) + +* chore(deps): bump pydantic from 1.10.4 to 1.10.5 (#233) + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.4 to 1.10.5. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.5/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.4...v1.10.5) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`a785e16`](https://github.com/supabase/auth-py/commit/a785e16c79a76d9a897d58dbc17f370a014038b0)) + +* chore(deps-dev): bump faker from 16.7.0 to 16.8.1 + +Bumps [faker](https://github.com/joke2k/faker) from 16.7.0 to 16.8.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v16.7.0...v16.8.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`e7088a7`](https://github.com/supabase/auth-py/commit/e7088a72d647483841857d00d6c33153ed39de51)) + +* chore(deps): bump cryptography from 39.0.0 to 39.0.1 + +Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.0 to 39.0.1. +- [Release notes](https://github.com/pyca/cryptography/releases) +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/39.0.0...39.0.1) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c84f04d`](https://github.com/supabase/auth-py/commit/c84f04d42f8e25e0fd492861b82d03c6d0ec98a5)) + +* chore(deps-dev): bump faker from 16.6.1 to 16.7.0 + +Bumps [faker](https://github.com/joke2k/faker) from 16.6.1 to 16.7.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v16.6.1...v16.7.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7d5cbfc`](https://github.com/supabase/auth-py/commit/7d5cbfcf2a94533dc106db4fbda20aa0e0d14070)) + +### Feature + +* feat: bump version to 1.0.1 ([`0056efc`](https://github.com/supabase/auth-py/commit/0056efccd35b824f952b53d6a42a73fffb42553f)) + +### Fix + +* fix: run black ([`a37afe3`](https://github.com/supabase/auth-py/commit/a37afe39c3930c022010d018647d325d8d583d33)) + +* fix: patch padding for base64 ([`4ae0a21`](https://github.com/supabase/auth-py/commit/4ae0a2114e202f525ad9d992cd65c5297b7a934d)) + +### Unknown + +* Merge pull request #259 from supabase-community/j0/bump_version + +feat: bump version to 1.0.1 ([`359a003`](https://github.com/supabase/auth-py/commit/359a003b3b98e2e14902bb2658a97f27417469df)) + +* Merge pull request #256 from supabase-community/dependabot/pip/main/faker-18.3.1 + +chore(deps-dev): bump faker from 18.3.0 to 18.3.1 ([`578734e`](https://github.com/supabase/auth-py/commit/578734e896b46af9cf8c4991bdcce4e6caa40103)) + +* Merge pull request #254 from supabase-community/j0/patch_padding_for_base64 + +fix: patch padding for base64 ([`f77663a`](https://github.com/supabase/auth-py/commit/f77663ac11f6303f95677f4913c12f7765bd07f5)) + +* Update helpers.py ([`e1ba5e2`](https://github.com/supabase/auth-py/commit/e1ba5e2ab79484beda860c27961336771f73b15b)) + +* Merge pull request #252 from supabase-community/dependabot/pip/main/faker-18.3.0 + +chore(deps-dev): bump faker from 17.6.0 to 18.3.0 ([`5d075d2`](https://github.com/supabase/auth-py/commit/5d075d2649ac4c2942dfdc3cfad3459d43868a1e)) + +* Merge pull request #235 from supabase-community/dependabot/pip/main/python-semantic-release-7.33.2 + +chore(deps-dev): bump python-semantic-release from 7.33.1 to 7.33.2 ([`3476384`](https://github.com/supabase/auth-py/commit/3476384e75fd29b8efbc32b886c0c845d6b62e89)) + +* Merge pull request #244 from supabase-community/dependabot/pip/main/faker-17.6.0 + +chore(deps-dev): bump faker from 17.3.0 to 17.6.0 ([`03ba918`](https://github.com/supabase/auth-py/commit/03ba918d82c891d0ddcdda35f8c19c155403f9d4)) + +* Merge pull request #240 from supabase-community/dependabot/github_actions/main/abatilo/actions-poetry-2.3.0 + +chore(deps): bump abatilo/actions-poetry from 2.2.0 to 2.3.0 ([`3ec751b`](https://github.com/supabase/auth-py/commit/3ec751bbc97f6917fd7c3aace24dfb67e23efbda)) + +* Merge pull request #245 from supabase-community/dependabot/pip/main/pytest-7.2.2 + +chore(deps-dev): bump pytest from 7.2.1 to 7.2.2 ([`ad3b366`](https://github.com/supabase/auth-py/commit/ad3b3669bf2078725dbe660e0bb4a471b17ca6aa)) + +* Merge pull request #239 from supabase-community/dependabot/pip/main/faker-17.3.0 + +chore(deps-dev): bump faker from 17.0.0 to 17.3.0 ([`20fcfbd`](https://github.com/supabase/auth-py/commit/20fcfbd123d7ba495de492b1bd5384c6d9117ac3)) + +* Merge pull request #241 from supabase-community/dependabot/pip/main/pre-commit-3.1.1 + +chore(deps-dev): bump pre-commit from 3.1.0 to 3.1.1 ([`9a511ea`](https://github.com/supabase/auth-py/commit/9a511ea744cbb5c2e34cd2c14379b536520bc7bc)) + +* Merge pull request #238 from supabase-community/dependabot/pip/main/pre-commit-3.1.0 + +chore(deps-dev): bump pre-commit from 3.0.4 to 3.1.0 ([`5561e94`](https://github.com/supabase/auth-py/commit/5561e94cdbb697e02c0261ebae3f717d46dddbd9)) + +* Merge pull request #236 from supabase-community/dependabot/pip/main/pygithub-1.58.0 + +chore(deps-dev): bump pygithub from 1.57 to 1.58.0 ([`6c1247c`](https://github.com/supabase/auth-py/commit/6c1247c23302a81cfb33177f297f619d91a5fbb0)) + +* Merge pull request #232 from supabase-community/dependabot/pip/main/faker-17.0.0 + +chore(deps-dev): bump faker from 16.8.1 to 17.0.0 ([`d47b7c1`](https://github.com/supabase/auth-py/commit/d47b7c102c8e9ac7c46eda9b7fe977ff7c3beb0f)) + +* Merge pull request #230 from supabase-community/dependabot/pip/main/faker-16.8.1 + +chore(deps-dev): bump faker from 16.7.0 to 16.8.1 ([`9699606`](https://github.com/supabase/auth-py/commit/9699606e307d12f4e3b50461d62b0ba5e3e456e4)) + +* Merge pull request #228 from supabase-community/dependabot/pip/cryptography-39.0.1 + +chore(deps): bump cryptography from 39.0.0 to 39.0.1 ([`af1a7f0`](https://github.com/supabase/auth-py/commit/af1a7f064a3be09e1ca74e6f31328160c7c4db77)) + +* Merge pull request #229 from supabase-community/dependabot/pip/main/faker-16.7.0 + +chore(deps-dev): bump faker from 16.6.1 to 16.7.0 ([`db484db`](https://github.com/supabase/auth-py/commit/db484dbe0d1dbf959a219dc7f727fe43de6cdef3)) + +## v1.0.0 (2023-02-05) + +### Chore + +* chore(deps-dev): bump pre-commit from 2.21.0 to 3.0.4 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.21.0 to 3.0.4. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.21.0...v3.0.4) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c67fbda`](https://github.com/supabase/auth-py/commit/c67fbdac96ea2939a36d7f5cb2d7605b670bee07)) + +* chore(deps-dev): bump python-semantic-release from 7.33.0 to 7.33.1 + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.33.0 to 7.33.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.33.0...v7.33.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`6423f19`](https://github.com/supabase/auth-py/commit/6423f19594cc324a029a9dc53e0dd7e9be3cdce2)) + +* chore(deps-dev): bump black from 22.12.0 to 23.1.0 + +Bumps [black](https://github.com/psf/black) from 22.12.0 to 23.1.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/22.12.0...23.1.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`3b2fd7f`](https://github.com/supabase/auth-py/commit/3b2fd7f2cea8ffd80a401c9f4f3d5031c3f3fddd)) + +* chore: update ci ([`dc3573c`](https://github.com/supabase/auth-py/commit/dc3573c466b139fc7fdb89670f0a2de08da38830)) + +* chore(deps-dev): bump faker from 16.6.0 to 16.6.1 + +Bumps [faker](https://github.com/joke2k/faker) from 16.6.0 to 16.6.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v16.6.0...v16.6.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`55bccec`](https://github.com/supabase/auth-py/commit/55bccec3816feda0d63809a4b625ffaecc3e623b)) + +* chore(deps-dev): bump faker from 15.3.4 to 16.6.0 + +Bumps [faker](https://github.com/joke2k/faker) from 15.3.4 to 16.6.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.3.4...v16.6.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`db13e78`](https://github.com/supabase/auth-py/commit/db13e785b995a3e8f8a18033e955388e7e277a14)) + +* chore: include 3.11 ([`f74c31f`](https://github.com/supabase/auth-py/commit/f74c31f5b413ebbabb37463d2c8ae2184aa90b81)) + +* chore: adjust ci poetry version ([`4d4685b`](https://github.com/supabase/auth-py/commit/4d4685bbcfeb81ec61fea86ffa404b5849a2fa1d)) + +* chore(deps): bump pydantic from 1.10.2 to 1.10.4 + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.2 to 1.10.4. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.4/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.2...v1.10.4) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`be56f07`](https://github.com/supabase/auth-py/commit/be56f07810d726329884ddc1a3664fb4c3c57d7b)) + +* chore(deps-dev): bump pre-commit from 2.20.0 to 2.21.0 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.20.0 to 2.21.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.20.0...v2.21.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7116357`](https://github.com/supabase/auth-py/commit/711635725ff9143fb57e2652da448c5bd49e84fc)) + +* chore(deps): bump gitpython from 3.1.27 to 3.1.30 + +Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.27 to 3.1.30. +- [Release notes](https://github.com/gitpython-developers/GitPython/releases) +- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) +- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.27...3.1.30) + +--- +updated-dependencies: +- dependency-name: gitpython + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`8a1b4dd`](https://github.com/supabase/auth-py/commit/8a1b4dd2510feb731b8862a4971b32642ae9f8de)) + +* chore(deps): bump wheel from 0.37.1 to 0.38.1 + +Bumps [wheel](https://github.com/pypa/wheel) from 0.37.1 to 0.38.1. +- [Release notes](https://github.com/pypa/wheel/releases) +- [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst) +- [Commits](https://github.com/pypa/wheel/compare/0.37.1...0.38.1) + +--- +updated-dependencies: +- dependency-name: wheel + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`ff9818c`](https://github.com/supabase/auth-py/commit/ff9818c960499b69a3863f5e19bfb7079d810566)) + +* chore(deps-dev): bump isort from 5.11.1 to 5.11.4 + +Bumps [isort](https://github.com/pycqa/isort) from 5.11.1 to 5.11.4. +- [Release notes](https://github.com/pycqa/isort/releases) +- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pycqa/isort/compare/5.11.1...5.11.4) + +--- +updated-dependencies: +- dependency-name: isort + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`ba7cdc6`](https://github.com/supabase/auth-py/commit/ba7cdc6f4490e01ebcafad4768050998ea9a7307)) + +* chore(deps): bump httpx from 0.23.1 to 0.23.3 + +Bumps [httpx](https://github.com/encode/httpx) from 0.23.1 to 0.23.3. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.23.1...0.23.3) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`32ce139`](https://github.com/supabase/auth-py/commit/32ce139d602503287fe5a6846f2983cbcc42f8bb)) + +* chore(deps-dev): bump isort from 5.10.1 to 5.11.1 (#195) + +Bumps [isort](https://github.com/pycqa/isort) from 5.10.1 to 5.11.1. +- [Release notes](https://github.com/pycqa/isort/releases) +- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pycqa/isort/compare/5.10.1...5.11.1) + +--- +updated-dependencies: +- dependency-name: isort + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`d12d0c5`](https://github.com/supabase/auth-py/commit/d12d0c57e2111e3e4547a36761bd91749f856205)) + +* chore(deps-dev): bump black from 22.10.0 to 22.12.0 + +Bumps [black](https://github.com/psf/black) from 22.10.0 to 22.12.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/22.10.0...22.12.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`2207e09`](https://github.com/supabase/auth-py/commit/2207e095cb43648082bbad2e513e29a23920e0c1)) + +* chore(deps): bump certifi from 2022.6.15 to 2022.12.7 (#192) + +Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7. +- [Release notes](https://github.com/certifi/python-certifi/releases) +- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07) + +--- +updated-dependencies: +- dependency-name: certifi + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`841083e`](https://github.com/supabase/auth-py/commit/841083e36c7098def95303d3e9eff3a94949b8c7)) + +* chore(deps-dev): bump pytest-asyncio from 0.20.2 to 0.20.3 (#193) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.2 to 0.20.3. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.2...v0.20.3) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`2b8b03a`](https://github.com/supabase/auth-py/commit/2b8b03ab9c971a08c7e56ff2a41cbf976ce54b2a)) + +* chore(deps-dev): bump faker from 15.3.3 to 15.3.4 (#191) + +Bumps [faker](https://github.com/joke2k/faker) from 15.3.3 to 15.3.4. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.3.3...v15.3.4) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`5773bf0`](https://github.com/supabase/auth-py/commit/5773bf0b97bf6fbc2a2adb61175d73941915bfa9)) + +* chore(deps): bump abatilo/actions-poetry from 2.1.6 to 2.2.0 (#190) + +Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 2.1.6 to 2.2.0. +- [Release notes](https://github.com/abatilo/actions-poetry/releases) +- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc) +- [Commits](https://github.com/abatilo/actions-poetry/compare/v2.1.6...v2.2.0) + +--- +updated-dependencies: +- dependency-name: abatilo/actions-poetry + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`ad040d1`](https://github.com/supabase/auth-py/commit/ad040d1428d5419e4a62b993e9946e872ecf5904)) + +* chore(deps-dev): bump faker from 15.3.2 to 15.3.3 (#189) + +Bumps [faker](https://github.com/joke2k/faker) from 15.3.2 to 15.3.3. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.3.2...v15.3.3) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`ab1f0da`](https://github.com/supabase/auth-py/commit/ab1f0dab02d57c2387b85016ddeb4103f7a0db55)) + +* chore(deps): bump httpx from 0.23.0 to 0.23.1 (#188) + +Bumps [httpx](https://github.com/encode/httpx) from 0.23.0 to 0.23.1. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.23.0...0.23.1) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`dd04ad2`](https://github.com/supabase/auth-py/commit/dd04ad2eff2da019f65b1a37119d6f612fc4e5e4)) + +* chore(deps-dev): bump faker from 15.3.1 to 15.3.2 (#186) + +Bumps [faker](https://github.com/joke2k/faker) from 15.3.1 to 15.3.2. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.3.1...v15.3.2) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`8d75f1c`](https://github.com/supabase/auth-py/commit/8d75f1cee71f74349a90922b945a82343f68963b)) + +* chore(deps): bump cryptography from 37.0.4 to 38.0.3 (#185) + +Bumps [cryptography](https://github.com/pyca/cryptography) from 37.0.4 to 38.0.3. +- [Release notes](https://github.com/pyca/cryptography/releases) +- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pyca/cryptography/compare/37.0.4...38.0.3) + +--- +updated-dependencies: +- dependency-name: cryptography + dependency-type: indirect +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`6e0df2f`](https://github.com/supabase/auth-py/commit/6e0df2f2c357aef1d1384604d7012704a447217a)) + +* chore(deps-dev): bump pytest-asyncio from 0.20.1 to 0.20.2 (#184) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.1 to 0.20.2. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.1...v0.20.2) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`bebb22f`](https://github.com/supabase/auth-py/commit/bebb22fe7b5ef2862388916e873b6db17dcb41f3)) + +* chore(deps-dev): bump faker from 15.1.3 to 15.3.1 (#183) + +Bumps [faker](https://github.com/joke2k/faker) from 15.1.3 to 15.3.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.1.3...v15.3.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`ae2043a`](https://github.com/supabase/auth-py/commit/ae2043ad8a0e1f1f0b4e2e7fd173f8277e15db6a)) + +* chore: update lock ([`9dd42b4`](https://github.com/supabase/auth-py/commit/9dd42b4c516879e208ca2d15d5934d682f9e4481)) + +* chore(deps-dev): bump faker from 15.1.1 to 15.1.3 (#182) + +Bumps [faker](https://github.com/joke2k/faker) from 15.1.1 to 15.1.3. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.1.1...v15.1.3) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`2c15513`](https://github.com/supabase/auth-py/commit/2c15513f5986ff27448eba04e4c8e75140e96441)) + +* chore: fix EOL ([`44a4f25`](https://github.com/supabase/auth-py/commit/44a4f2573deb61ecdae704ec67f9e65c1dc25bd2)) + +* chore(deps-dev): bump pytest from 7.1.3 to 7.2.0 (#179) + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.3 to 7.2.0. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.3...7.2.0) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`73df32a`](https://github.com/supabase/auth-py/commit/73df32a75e4621218db4a7a95cf5d5c123157a0b)) + +* chore(deps-dev): bump python-semantic-release from 7.32.1 to 7.32.2 (#177) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.32.1 to 7.32.2. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.32.1...v7.32.2) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`5aae844`](https://github.com/supabase/auth-py/commit/5aae8441185febd2db0439d486735e3724e3b437)) + +* chore(deps-dev): bump pytest-asyncio from 0.19.0 to 0.20.1 + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.19.0 to 0.20.1. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.19.0...v0.20.1) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`89f1336`](https://github.com/supabase/auth-py/commit/89f133619118f4fbf8eaa9ef4b05283397fbfefd)) + +* chore: remove PyGithub as dep ([`08231ba`](https://github.com/supabase/auth-py/commit/08231baf14e72772e19f9d70ddd29e1b7387587e)) + +* chore: gen sync files ([`01385ea`](https://github.com/supabase/auth-py/commit/01385eac0c0ae79ae583b4ade84ab985f5933d20)) + +* chore: implement clients and utils for testing ([`6fbd3de`](https://github.com/supabase/auth-py/commit/6fbd3de4247f38ddd4af76422729658a8f0fb2da)) + +* chore(deps-dev): bump faker from 15.0.0 to 15.1.1 + +Bumps [faker](https://github.com/joke2k/faker) from 15.0.0 to 15.1.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v15.0.0...v15.1.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`69c7c4e`](https://github.com/supabase/auth-py/commit/69c7c4e0e9434a16195c7cc9fb2c29bd119362ca)) + +* chore(deps): fix poetry lock file ([`5148277`](https://github.com/supabase/auth-py/commit/514827788828799817ea47f3f89f442c163ba5be)) + +* chore(deps-dev): bump black from 22.8.0 to 22.10.0 (#170) + +Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`be6a2b3`](https://github.com/supabase/auth-py/commit/be6a2b330c8234b48b5af5e5a3e6586ab76a51d5)) + +* chore(deps-dev): bump python-semantic-release from 7.32.0 to 7.32.1 (#171) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.32.0 to 7.32.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.32.0...v7.32.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`6a02950`](https://github.com/supabase/auth-py/commit/6a0295001b5cd476e27b72fb77b34c6c2a1dee98)) + +* chore(deps-dev): bump pytest-cov from 3.0.0 to 4.0.0 (#168) + +Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 3.0.0 to 4.0.0. +- [Release notes](https://github.com/pytest-dev/pytest-cov/releases) +- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0) + +--- +updated-dependencies: +- dependency-name: pytest-cov + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`57ae3b3`](https://github.com/supabase/auth-py/commit/57ae3b33c925252fd2ec2faa2efbc911434393fa)) + +* chore(deps-dev): bump faker from 14.2.1 to 15.0.0 (#167) + +Bumps [faker](https://github.com/joke2k/faker) from 14.2.1 to 15.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v14.2.1...v15.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`cc918f9`](https://github.com/supabase/auth-py/commit/cc918f90e57288f32ed081555c4b104947837340)) + +* chore(deps-dev): bump python-semantic-release from 7.31.4 to 7.32.0 (#165) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.31.4 to 7.32.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.31.4...v7.32.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`b8a9944`](https://github.com/supabase/auth-py/commit/b8a99441c82b450b667741b1c05830e92eb63b4a)) + +* chore(deps-dev): bump faker from 14.2.0 to 14.2.1 + +Bumps [faker](https://github.com/joke2k/faker) from 14.2.0 to 14.2.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v14.2.0...v14.2.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`a16f952`](https://github.com/supabase/auth-py/commit/a16f95240d5215e07f330befa138f7aa6d329fc2)) + +* chore(deps): bump pydantic from 1.10.1 to 1.10.2 (#162) + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.1 to 1.10.2. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.1...v1.10.2) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`4666845`](https://github.com/supabase/auth-py/commit/46668453b0fbb0411af78f171b0dae80c5e2c23f)) + +* chore(deps-dev): bump pytest from 7.1.2 to 7.1.3 (#161) + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.2...7.1.3) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`b93c9d7`](https://github.com/supabase/auth-py/commit/b93c9d7d3f3f52f60effe5ffd17bfa400d46cd3c)) + +* chore(deps-dev): bump black from 22.6.0 to 22.8.0 (#159) + +Bumps [black](https://github.com/psf/black) from 22.6.0 to 22.8.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/22.6.0...22.8.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`b552dea`](https://github.com/supabase/auth-py/commit/b552dea77b8d8cc058d6ba856f37f0f1d26d3480)) + +* chore(deps-dev): bump faker from 14.1.1 to 14.2.0 (#158) + +Bumps [faker](https://github.com/joke2k/faker) from 14.1.1 to 14.2.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v14.1.1...v14.2.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`214353e`](https://github.com/supabase/auth-py/commit/214353e69e8da96ee628e87deac7c7f856e85c03)) + +* chore(deps): bump pydantic from 1.10.0 to 1.10.1 (#160) + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.0 to 1.10.1. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.10.0...v1.10.1) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`a322dce`](https://github.com/supabase/auth-py/commit/a322dce65f54de8a98d54c02dce0a12fdd5201c2)) + +* chore(deps): bump dev dependencies ([`93aabbe`](https://github.com/supabase/auth-py/commit/93aabbe1f5f130677b9a3765c998936940b75dc2)) + +* chore: format docstring to avoid linter warning ([`6a200fe`](https://github.com/supabase/auth-py/commit/6a200fead8b18cb06fe4fcf93c37b65345b48372)) + +* chore: update dependencies ([`6b15a50`](https://github.com/supabase/auth-py/commit/6b15a50466f82b87aae67823e08770c25cb352f6)) + +### Feature + +* feat: bump to 1.0.0 ([`ac012cd`](https://github.com/supabase/auth-py/commit/ac012cda87d33f44fcc648fca0e166693f890ad3)) + +* feat: switch from flake8->ruff ([`19cdde0`](https://github.com/supabase/auth-py/commit/19cdde0867f9f1e5b7ef996fd162bbd3dc4e1a39)) + +* feat: move sphinx into doc group ([`173399b`](https://github.com/supabase/auth-py/commit/173399bb859bffd68b59bb7ff87d33501c92df62)) + +* feat: add docs ([`afc563a`](https://github.com/supabase/auth-py/commit/afc563add7782efeeab2ca967ca85e7476c864f3)) + +* feat: mfa challenge and verify and refresh session ([`b714206`](https://github.com/supabase/auth-py/commit/b7142060c99d06270bdcde9ecc2f823c33cf11b4)) + +* feat: add mfa ([`4c2b443`](https://github.com/supabase/auth-py/commit/4c2b4437d0b99c6f19ec251e245909d86ca69f8f)) + +* feat: implement decode_jwt_payload in helpers ([`c3ff22a`](https://github.com/supabase/auth-py/commit/c3ff22a0a8758585ee77a68381041b77ac25a234)) + +### Fix + +* fix: adjust timer units and drop * 1000 since timer takes in seconds and not miliseconds ([`78ed6d2`](https://github.com/supabase/auth-py/commit/78ed6d224bc73cd89defb26b84a8ccf9c95dc68c)) + +* fix: reinstate 3.8 ([`58db1be`](https://github.com/supabase/auth-py/commit/58db1be7939c7aae1d50e7e29dee3a8f110775e4)) + +* fix: bump isort and python version ([`7f50501`](https://github.com/supabase/auth-py/commit/7f505016f04c1befc9db75a7f2dd71089b224eb4)) + +* fix: move dependency ([`1e51fce`](https://github.com/supabase/auth-py/commit/1e51fce51b32cada87d94efc8cf11f6a1aa6b282)) + +* fix: regenerate lock ([`34b8fb1`](https://github.com/supabase/auth-py/commit/34b8fb1aaf383a149efaa8f5832cbc82782b91d4)) + +* fix: add jwt package ([`36e8d12`](https://github.com/supabase/auth-py/commit/36e8d12e10f5f8494a7e5b00bf443678f6db0e47)) + +* fix: revert poetry.lock to original main files ([`df4ab8f`](https://github.com/supabase/auth-py/commit/df4ab8fa9ddbcf966026e98a9617f652f1f91179)) + +* fix: checkout main poetry.lock and poetry.toml ([`c8aeabc`](https://github.com/supabase/auth-py/commit/c8aeabc06dd6208f13e7934e0d70bf7bac8ea487)) + +* fix: remove ruff ([`ef21730`](https://github.com/supabase/auth-py/commit/ef21730997d23f51c9f52123742436c53dc96d29)) + +* fix: comment out sphinx ([`d46cf0d`](https://github.com/supabase/auth-py/commit/d46cf0d195b293f2f285190a726208c033a4722c)) + +* fix: remove group from pyproject.toml ([`41349d2`](https://github.com/supabase/auth-py/commit/41349d2cebf239a3529d92c2d848ca1cf78cc4f3)) + +* fix: revert dependency group ([`a923e52`](https://github.com/supabase/auth-py/commit/a923e52ecebc5706c210135b0bfeaaccf78fffd8)) + +* fix: stray EOLS ([`33cefba`](https://github.com/supabase/auth-py/commit/33cefba743b326647aee7c8639f000894dd609e9)) + +* fix: include modules page ([`8dab491`](https://github.com/supabase/auth-py/commit/8dab491fc1ce754f9d5aa27eab0e4c938b1342cd)) + +* fix: patch merge conflicts on next ([`fb033e4`](https://github.com/supabase/auth-py/commit/fb033e44208be7f3ae611eefd83d48d379909c44)) + +* fix: bugs in admin api and finish tests implementation ([`9db7ce6`](https://github.com/supabase/auth-py/commit/9db7ce680326c149730ef84cbe9540ad8a735676)) + +* fix: list users method of admin api ([`a973a1c`](https://github.com/supabase/auth-py/commit/a973a1cce3ea90e3c3ece0def70626c68727ae0d)) + +* fix: respect EXPIRY_MARGIN on getSession ([`45afb35`](https://github.com/supabase/auth-py/commit/45afb351ebe960a292edb77d4ea77bc235ee1563)) + +* fix: use literal from typing extensions ([`ec6618b`](https://github.com/supabase/auth-py/commit/ec6618b0c4e1ac0137f98debbc02b98e61e8ef60)) + +* fix: insert default content type header ([`332f782`](https://github.com/supabase/auth-py/commit/332f782db88921ef30a5362e054fd39d26166d2d)) + +* fix: implement the reset_password_email method ([`6ffa532`](https://github.com/supabase/auth-py/commit/6ffa53227d991c2135b5a1fd5202fea97e8d6097)) + +### Refactor + +* refactor: migrate to implementation as similar as possible to the implementation of gotrue-js + +The tests still need to be implemented. ([`4cb7713`](https://github.com/supabase/auth-py/commit/4cb771352e7f4065b2aac5de1c2b1260a0771268)) + +### Test + +* test: add test to user fetch methods of admin api ([`8c076f0`](https://github.com/supabase/auth-py/commit/8c076f0d23411b0ee1861b91739f45069275c630)) + +### Unknown + +* Merge pull request #227 from supabase-community/j0/new_release + +feat: bump to 1.0.0 ([`b5cdd36`](https://github.com/supabase/auth-py/commit/b5cdd3687eab5923f117c3d2bcfd824f5d8ee842)) + +* Merge pull request #226 from supabase-community/dependabot/pip/main/pre-commit-3.0.4 + +chore(deps-dev): bump pre-commit from 2.21.0 to 3.0.4 ([`53c1fef`](https://github.com/supabase/auth-py/commit/53c1fefc057292fc8aec564e2849b6fcc2f011cd)) + +* Merge pull request #225 from supabase-community/dependabot/pip/main/python-semantic-release-7.33.1 + +chore(deps-dev): bump python-semantic-release from 7.33.0 to 7.33.1 ([`527d8a0`](https://github.com/supabase/auth-py/commit/527d8a0a631d05f84d44a89b179e9fd4255a9abd)) + +* Merge pull request #223 from supabase-community/dependabot/pip/main/black-23.1.0 + +chore(deps-dev): bump black from 22.12.0 to 23.1.0 ([`2941512`](https://github.com/supabase/auth-py/commit/2941512c479ea6fb4c2e0344f4d46f1de366d0d6)) + +* Merge pull request #220 from supabase-community/j0/fix_timer_units + +fix: adjust timer units and drop * 1000 ([`758fb2f`](https://github.com/supabase/auth-py/commit/758fb2fd32c858181442f4b117a54d16b70ea519)) + +* Merge pull request #148 from supabase-community/next + +next release - feature parity - high testing coverage ([`b79e2a8`](https://github.com/supabase/auth-py/commit/b79e2a842cc6a49d1831a2690f13283db3b5ed4c)) + +* Merge pull request #219 from supabase-community/j0/patch_conflicts_on_next + +chore: patch merge conflicts on next ([`66f3f3a`](https://github.com/supabase/auth-py/commit/66f3f3aad672cb8c8f33e02f25d164311a4e688b)) + +* Merge branch 'main' of github.com:supabase-community/gotrue-py into j0/patch_conflicts_on_next ([`1682747`](https://github.com/supabase/auth-py/commit/1682747cad333b1731841d18eee748ee16361420)) + +* Merge pull request #218 from supabase-community/dependabot/pip/main/faker-16.6.1 + +chore(deps-dev): bump faker from 16.6.0 to 16.6.1 ([`52d4c8d`](https://github.com/supabase/auth-py/commit/52d4c8db450de015f04a18d80d80ec41991a88f4)) + +* Merge pull request #213 from supabase-community/dependabot/pip/main/faker-16.6.0 + +chore(deps-dev): bump faker from 15.3.4 to 16.6.0 ([`2217833`](https://github.com/supabase/auth-py/commit/2217833bc80629288d980dbff22ade936b696b6d)) + +* Merge pull request #214 from supabase-community/j0/bump-poetry + +chore: adjust ci poetry version ([`a43f38b`](https://github.com/supabase/auth-py/commit/a43f38b81a9f87ebc6b00f130f07041f5faaf77b)) + +* Merge pull request #203 from supabase-community/dependabot/pip/main/pydantic-1.10.4 + +chore(deps): bump pydantic from 1.10.2 to 1.10.4 ([`cf1faed`](https://github.com/supabase/auth-py/commit/cf1faedc712bf379ebf32a2f88c79480fc2a28bb)) + +* Merge pull request #200 from supabase-community/dependabot/pip/main/pre-commit-2.21.0 + +chore(deps-dev): bump pre-commit from 2.20.0 to 2.21.0 ([`9711f54`](https://github.com/supabase/auth-py/commit/9711f5440b9452d487366d99b340e1956f0c1f47)) + +* Merge pull request #206 from supabase-community/dependabot/pip/gitpython-3.1.30 + +chore(deps): bump gitpython from 3.1.27 to 3.1.30 ([`5861c0c`](https://github.com/supabase/auth-py/commit/5861c0cfe0fd38bc664657a0bb9fd13fb547fcd9)) + +* Merge pull request #201 from supabase-community/dependabot/pip/wheel-0.38.1 + +chore(deps): bump wheel from 0.37.1 to 0.38.1 ([`26c1eea`](https://github.com/supabase/auth-py/commit/26c1eeac67dfee01352f019d9f140c10ff6eb426)) + +* Merge pull request #199 from supabase-community/dependabot/pip/main/isort-5.11.4 + +chore(deps-dev): bump isort from 5.11.1 to 5.11.4 ([`80f2cf2`](https://github.com/supabase/auth-py/commit/80f2cf27e7e4efd7a57ed7fcb9529f5025dd822b)) + +* Merge pull request #205 from supabase-community/dependabot/pip/main/httpx-0.23.3 + +chore(deps): bump httpx from 0.23.1 to 0.23.3 ([`b8ae350`](https://github.com/supabase/auth-py/commit/b8ae350047aef889e11dd042425af9691b0533cb)) + +* Merge pull request #180 from supabase-community/j0/add_docs + +feat: add docs for GoTrue ([`f7eade8`](https://github.com/supabase/auth-py/commit/f7eade84b66e0cbe4143d47181cc053d1b46e159)) + +* revert: remove ruff from ci ([`34535dd`](https://github.com/supabase/auth-py/commit/34535ddeb4e3577a0231a098f9abdd3475ed6467)) + +* Merge pull request #194 from supabase-community/dependabot/pip/main/black-22.12.0 + +chore(deps-dev): bump black from 22.10.0 to 22.12.0 ([`dff74c6`](https://github.com/supabase/auth-py/commit/dff74c6abc9da6939dc7c384e5ec4e2ea270e13a)) + +* Merge branch 'j0/add_docs' of github.com:supabase-community/gotrue-py into j0/add_docs ([`7c5ea9c`](https://github.com/supabase/auth-py/commit/7c5ea9cbf957e0b37be0fcc09c71d922bdca8480)) + +* Merge branch 'main' into j0/add_docs ([`24fbb46`](https://github.com/supabase/auth-py/commit/24fbb46852bb1f311cb7f8358b2806834ca80be3)) + +* Merge branch 'j0/add_docs' of github.com:supabase-community/gotrue-py into j0/add_docs ([`798ef1c`](https://github.com/supabase/auth-py/commit/798ef1cd86751de8fd55fd1aeef30ad6ef1548be)) + +* Merge pull request #181 from supabase-community/sourcery/j0/add_docs + +feat: add docs for GoTrue (Sourcery refactored) ([`7482f1a`](https://github.com/supabase/auth-py/commit/7482f1a6c42103e5b7a410f8326d596e61846a3e)) + +* 'Refactored by Sourcery' ([`d26ff18`](https://github.com/supabase/auth-py/commit/d26ff1880cc55fe467beae0a3b59f629d79a212e)) + +* Merge pull request #178 from supabase-community/dependabot/pip/main/pytest-asyncio-0.20.1 + +chore(deps-dev): bump pytest-asyncio from 0.19.0 to 0.20.1 ([`e20893a`](https://github.com/supabase/auth-py/commit/e20893a9a9dc0be47883962396d91378ab2de81d)) + +* tests: add python 3.11 and update poetry version ([`33756af`](https://github.com/supabase/auth-py/commit/33756af49f995017cf3d9fdfd581b4974f13003c)) + +* tests: add tests for create user in admin api ([`30fd751`](https://github.com/supabase/auth-py/commit/30fd7512221145363f1ac5fb44fed86cdb08eb6d)) + +* Merge remote-tracking branch 'remotes/origin/next' into next ([`20638fe`](https://github.com/supabase/auth-py/commit/20638fe9b8388367ce72669817c2590070ad60d6)) + +* 'Refactored by Sourcery' (#176) + +Co-authored-by: Sourcery AI <> ([`d5a0920`](https://github.com/supabase/auth-py/commit/d5a092095145459c4c67321915cabfb723170909)) + +* 'Refactored by Sourcery' (#175) + +Co-authored-by: Sourcery AI <> ([`9e4a1f2`](https://github.com/supabase/auth-py/commit/9e4a1f2c7caba255cb9e31eb546f28a9a469b205)) + +* Merge branch 'main' into next ([`4e3be99`](https://github.com/supabase/auth-py/commit/4e3be993dd11dcaad542bae2177783c37a8e165c)) + +* Merge pull request #174 from supabase-community/dependabot/pip/main/faker-15.1.1 + +chore(deps-dev): bump faker from 15.0.0 to 15.1.1 ([`dbe13b7`](https://github.com/supabase/auth-py/commit/dbe13b7355b6767f4412e74be283f2abe8277091)) + +* 'Refactored by Sourcery' (#172) + +Co-authored-by: Sourcery AI <> ([`20b882c`](https://github.com/supabase/auth-py/commit/20b882cc2501ac39dce5b6fe622d0fd27d946eea)) + +* Merge branch 'main' into next ([`9f80bbc`](https://github.com/supabase/auth-py/commit/9f80bbcdefdec2d9af3a895a3752956b158887c9)) + +* Merge pull request #164 from supabase-community/dependabot/pip/main/faker-14.2.1 + +chore(deps-dev): bump faker from 14.2.0 to 14.2.1 ([`7c5a665`](https://github.com/supabase/auth-py/commit/7c5a665aec69acc905588fb6a61019281a3de70a)) + +* Merge branch 'main' into next ([`971b816`](https://github.com/supabase/auth-py/commit/971b816646268d7740b40a4c018af27a38aaa4b9)) + +## v0.5.4 (2022-08-31) + +### Chore + +* chore: setup version 0.5.4 for new release (#157) + +* chore: setup version 0.5.4 for new release + +* chore: update version of __init__.py + +* chore: update changelog ([`d559e2d`](https://github.com/supabase/auth-py/commit/d559e2dedfbadde1e604087d8677cdb8aa63d7d6)) + +* chore(deps-dev): bump faker from 14.1.0 to 14.1.1 (#154) + +Bumps [faker](https://github.com/joke2k/faker) from 14.1.0 to 14.1.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v14.1.0...v14.1.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`272ad56`](https://github.com/supabase/auth-py/commit/272ad56d212ed37b6a6a60c8ce2c6613d2cb9ec1)) + +* chore(deps): bump pydantic from 1.9.2 to 1.10.0 (#153) + +Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.9.2 to 1.10.0. +- [Release notes](https://github.com/pydantic/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) +- [Commits](https://github.com/pydantic/pydantic/compare/v1.9.2...v1.10.0) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`913e014`](https://github.com/supabase/auth-py/commit/913e0142d92cf4c77e42624314577d5e29deed69)) + +* chore(deps): bump abatilo/actions-poetry from 2.1.5 to 2.1.6 (#152) + +Bumps [abatilo/actions-poetry](https://github.com/abatilo/actions-poetry) from 2.1.5 to 2.1.6. +- [Release notes](https://github.com/abatilo/actions-poetry/releases) +- [Changelog](https://github.com/abatilo/actions-poetry/blob/master/.releaserc) +- [Commits](https://github.com/abatilo/actions-poetry/compare/v2.1.5...v2.1.6) + +--- +updated-dependencies: +- dependency-name: abatilo/actions-poetry + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`62311f3`](https://github.com/supabase/auth-py/commit/62311f3598fb57f2559483f175fb2ff8b4c0f154)) + +* chore: add github action ecosystem to dependabot ([`76d9114`](https://github.com/supabase/auth-py/commit/76d91148e9e9bac13e3f771a9f09a98fbd9986b9)) + +* chore(deps-dev): bump python-semantic-release from 7.31.3 to 7.31.4 (#151) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.31.3 to 7.31.4. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.31.3...v7.31.4) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`62adb59`](https://github.com/supabase/auth-py/commit/62adb5918229335af6998ebe4084f6ed0b56cd3d)) + +* chore(deps-dev): bump python-semantic-release from 7.31.2 to 7.31.3 (#150) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.31.2 to 7.31.3. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.31.2...v7.31.3) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`a9c110d`](https://github.com/supabase/auth-py/commit/a9c110d37f7c6f732681e0a15676dfc0ac3c9af8)) + +* chore: change made by pyupgrade pre-commit ([`68ed829`](https://github.com/supabase/auth-py/commit/68ed829b33db91a95ef5fef28b344f84f1ecaccd)) + +* chore: add sourcery config file ([`5be3c33`](https://github.com/supabase/auth-py/commit/5be3c335822c7963b95dbce98db6ae4d5ae24d1c)) + +* chore: implement script to sync config of infra ([`896a40c`](https://github.com/supabase/auth-py/commit/896a40c06f6800b0f0b3eb81a386cf00219f793c)) + +* chore: upgrade dependencies ([`41ee2dd`](https://github.com/supabase/auth-py/commit/41ee2dd79eb02c65c20554f327b595ffb4bb936c)) + +* chore(deps-dev): bump faker from 14.0.0 to 14.1.0 (#147) + +Bumps [faker](https://github.com/joke2k/faker) from 14.0.0 to 14.1.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v14.0.0...v14.1.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`0c85e7e`](https://github.com/supabase/auth-py/commit/0c85e7ed8e7c79187200b19d151bba63251d1c71)) + +* chore(deps-dev): bump faker from 13.15.1 to 14.0.0 (#146) + +Bumps [faker](https://github.com/joke2k/faker) from 13.15.1 to 14.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.15.1...v14.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`2374b04`](https://github.com/supabase/auth-py/commit/2374b04c17b1ab30f3d4996b5c01bcb2ebda5a9b)) + +* chore(deps): bump pydantic from 1.9.1 to 1.9.2 (#145) + +Bumps [pydantic](https://github.com/samuelcolvin/pydantic) from 1.9.1 to 1.9.2. +- [Release notes](https://github.com/samuelcolvin/pydantic/releases) +- [Changelog](https://github.com/pydantic/pydantic/blob/v1.9.2/HISTORY.md) +- [Commits](https://github.com/samuelcolvin/pydantic/compare/v1.9.1...v1.9.2) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Signed-off-by: dependabot[bot] <support@github.com> +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`db929b4`](https://github.com/supabase/auth-py/commit/db929b4f91846f83e7a72c8d32d455c533e79d14)) + +* chore(deps-dev): bump flake8 from 5.0.3 to 5.0.4 (#144) + +Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.3 to 5.0.4. +- [Release notes](https://github.com/pycqa/flake8/releases) +- [Commits](https://github.com/pycqa/flake8/compare/5.0.3...5.0.4) + +--- +updated-dependencies: +- dependency-name: flake8 + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`456c818`](https://github.com/supabase/auth-py/commit/456c818ac6b9c0ddc822d586fa774f32b32891f9)) + +* chore(deps-dev): bump flake8 from 5.0.1 to 5.0.3 (#143) + +Bumps [flake8](https://github.com/pycqa/flake8) from 5.0.1 to 5.0.3. +- [Release notes](https://github.com/pycqa/flake8/releases) +- [Commits](https://github.com/pycqa/flake8/compare/5.0.1...5.0.3) + +--- +updated-dependencies: +- dependency-name: flake8 + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`d84e005`](https://github.com/supabase/auth-py/commit/d84e005173014ceaa09be9106e83145f5770da64)) + +* chore(deps-dev): bump python-semantic-release from 7.31.1 to 7.31.2 (#141) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.31.1 to 7.31.2. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.31.1...v7.31.2) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`b08ccf7`](https://github.com/supabase/auth-py/commit/b08ccf7fd2394735964b0aca9f90d474c7b0e50d)) + +* chore(deps-dev): bump flake8 from 4.0.1 to 5.0.1 (#142) + +Bumps [flake8](https://github.com/pycqa/flake8) from 4.0.1 to 5.0.1. +- [Release notes](https://github.com/pycqa/flake8/releases) +- [Commits](https://github.com/pycqa/flake8/compare/4.0.1...5.0.1) + +--- +updated-dependencies: +- dependency-name: flake8 + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`4c62d77`](https://github.com/supabase/auth-py/commit/4c62d77da0f1c281c24aabb0a90acc87861c1825)) + +* chore(deps-dev): bump python-semantic-release from 7.30.2 to 7.31.1 (#140) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.30.2 to 7.31.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.30.2...v7.31.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`8aba5b5`](https://github.com/supabase/auth-py/commit/8aba5b56d0e7177d3edbb2ee1535592eb05ffbb6)) + +* chore(deps-dev): bump python-semantic-release from 7.30.1 to 7.30.2 (#139) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.30.1 to 7.30.2. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.30.1...v7.30.2) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`93aa22b`](https://github.com/supabase/auth-py/commit/93aa22b9d4de65ad10983d7254076048b926b265)) + +* chore(deps-dev): bump python-semantic-release from 7.29.7 to 7.30.1 (#138) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.29.7 to 7.30.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.29.7...v7.30.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`4e464eb`](https://github.com/supabase/auth-py/commit/4e464eb16737cf77d24de95185d6ac772b14f178)) + +* chore(deps-dev): bump faker from 13.15.0 to 13.15.1 (#136) + +Bumps [faker](https://github.com/joke2k/faker) from 13.15.0 to 13.15.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.15.0...v13.15.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`606f158`](https://github.com/supabase/auth-py/commit/606f158cbe900165757bae2d8624dc5c90d8b533)) + +* chore(deps-dev): bump python-semantic-release from 7.29.5 to 7.29.7 (#137) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.29.5 to 7.29.7. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.29.5...v7.29.7) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`74a9410`](https://github.com/supabase/auth-py/commit/74a9410067e302040eff3aba064b230ccf2a9216)) + +* chore: upgrade pytest-asyncio ([`1fc2fa9`](https://github.com/supabase/auth-py/commit/1fc2fa968851c42947f9733f1ad7a169d475be2c)) + +### Fix + +* fix: use str type instead of uuid type in id property of identity model (#156) ([`460208d`](https://github.com/supabase/auth-py/commit/460208d16db5a49f682563c29dcfdf138d384c23)) + +### Unknown + +* 'Refactored by Sourcery' (#149) + +Co-authored-by: Sourcery AI <> ([`07f897e`](https://github.com/supabase/auth-py/commit/07f897ed3eddd93c9411f8f23f83948ed71bf674)) + +* Merge remote-tracking branch 'origin/main' ([`dde85fb`](https://github.com/supabase/auth-py/commit/dde85fbf5e404d3787c1da55cf216cf75f0578d8)) + +## v0.5.3 (2022-07-17) + +### Chore + +* chore: bump version to 0.5.3 ([`7d7d43e`](https://github.com/supabase/auth-py/commit/7d7d43e7f284e8916515b4d4ef79bcc0174df58b)) + +* chore(deps-dev): bump python-semantic-release from 7.29.4 to 7.29.5 (#131) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.29.4 to 7.29.5. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.29.4...v7.29.5) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`c506308`](https://github.com/supabase/auth-py/commit/c506308603ebaca0a54d6ab4517f0116cb7f07b9)) + +### Fix + +* fix: set total false to user attributes typed dict ([`b21d6e3`](https://github.com/supabase/auth-py/commit/b21d6e385d72d0d94729d2427ab3bdcf178a1efb)) + +## v0.5.2 (2022-07-13) + +### Chore + +* chore: upgrade changelog ([`b72f7e9`](https://github.com/supabase/auth-py/commit/b72f7e9cebeb42206b02e29f49f64051312a18e2)) + +* chore: bump version to 0.5.2 in __init__.py ([`345e978`](https://github.com/supabase/auth-py/commit/345e9782733fb0a6fd916026fe47a859a4742e0f)) + +* chore: bump version to 0.5.2 ([`95e8fdd`](https://github.com/supabase/auth-py/commit/95e8fddf4892d8bb8e7584d5f2455de7a6c708c4)) + +* chore(deps-dev): bump pre-commit from 2.19.0 to 2.20.0 (#128) + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.19.0 to 2.20.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.19.0...v2.20.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`76fad56`](https://github.com/supabase/auth-py/commit/76fad56d7eb0ad7966569389fa6639d4b6c9995d)) + +* chore(deps-dev): bump faker from 13.14.0 to 13.15.0 (#127) + +Bumps [faker](https://github.com/joke2k/faker) from 13.14.0 to 13.15.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.14.0...v13.15.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`0f2e0e5`](https://github.com/supabase/auth-py/commit/0f2e0e51cf3356c84a1a9cf87f435688b69a315c)) + +### Feature + +* feat: change `.update` method to also allow dictionaries (#130) + +* Allow users to send a dict instead of UserAttributes model + +* Add tests + +* Check Python version before trying to import TypedDict + +* Remove `TypedDict` from the main `typing` import + +* Change format + +* Change format of types + +* 'Refactored by Sourcery' + +Co-authored-by: odiseo0 <pedro.esserweb@gmail.com> +Co-authored-by: Sourcery AI <> ([`df3f69e`](https://github.com/supabase/auth-py/commit/df3f69ea18599a651715271d546b684555933286)) + +## v0.5.1 (2022-07-05) + +### Chore + +* chore: implement github action for manual release ([`cd8a590`](https://github.com/supabase/auth-py/commit/cd8a590effe65fa5ce1eb1fe3eef265e6f5fd655)) + +* chore: setup release ([`0a9ca44`](https://github.com/supabase/auth-py/commit/0a9ca445c7042057be0025562da85aceed22e8e3)) + +* chore(deps-dev): bump python-semantic-release from 7.28.0 to 7.28.1 (#110) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.28.0 to 7.28.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.28.0...v7.28.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`8a19b39`](https://github.com/supabase/auth-py/commit/8a19b3941d4a0eb06e5f2d3479a3c99b3cced0fd)) + +* chore(deps-dev): bump pre-commit from 2.17.0 to 2.18.1 (#107) + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.17.0 to 2.18.1. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v2.18.1) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`d0abb73`](https://github.com/supabase/auth-py/commit/d0abb73ccccc0eefbd0dfcea95766bb29ae96be7)) + +* chore(deps-dev): bump black from 22.1.0 to 22.3.0 (#105) + +Bumps [black](https://github.com/psf/black) from 22.1.0 to 22.3.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/compare/22.1.0...22.3.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`d2e6761`](https://github.com/supabase/auth-py/commit/d2e676150b2a5d72bf9464bfb44442401f452605)) + +* chore(deps-dev): bump faker from 13.3.3 to 13.3.4 (#106) + +Bumps [faker](https://github.com/joke2k/faker) from 13.3.3 to 13.3.4. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.3.3...v13.3.4) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`a309526`](https://github.com/supabase/auth-py/commit/a309526dc6302a791b780476b12428d8ad1d7781)) + +* chore(deps-dev): bump python-semantic-release from 7.27.0 to 7.28.0 (#109) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.27.0 to 7.28.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.27.0...v7.28.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`8042862`](https://github.com/supabase/auth-py/commit/804286224c86a45163f722aae42ac18deb05293e)) + +* chore(deps-dev): bump pytest-asyncio from 0.18.2 to 0.18.3 (#104) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.2 to 0.18.3. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.2...v0.18.3) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`270bd1e`](https://github.com/supabase/auth-py/commit/270bd1e6b267f36a3b944ba3588bbc7d48feb09c)) + +* chore(deps-dev): bump faker from 13.3.2 to 13.3.3 (#103) + +Bumps [faker](https://github.com/joke2k/faker) from 13.3.2 to 13.3.3. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.3.2...v13.3.3) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`bea6573`](https://github.com/supabase/auth-py/commit/bea65739c558e25a842c2b399d413724a00cd113)) + +* chore(deps-dev): bump pytest from 7.1.0 to 7.1.1 (#102) + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.0 to 7.1.1. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.0...7.1.1) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`c0d06af`](https://github.com/supabase/auth-py/commit/c0d06af7b870e2c1085a4034738a32856a384306)) + +* chore(deps-dev): bump python-semantic-release from 7.26.0 to 7.27.0 (#101) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.26.0 to 7.27.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.26.0...v7.27.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`3bfe980`](https://github.com/supabase/auth-py/commit/3bfe980ffb6b743fab101a981695ecb10196d9a8)) + +* chore(deps-dev): bump faker from 13.3.1 to 13.3.2 (#100) + +Bumps [faker](https://github.com/joke2k/faker) from 13.3.1 to 13.3.2. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.3.1...v13.3.2) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`feb9bc6`](https://github.com/supabase/auth-py/commit/feb9bc6938f1995299fb88c0a085f795f0440e1f)) + +* chore(deps-dev): bump pytest from 7.0.1 to 7.1.0 + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.0. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...7.1.0) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`c8181d2`](https://github.com/supabase/auth-py/commit/c8181d2bc6076599611b9e17203c81d8650584c3)) + +* chore(deps-dev): bump python-semantic-release from 7.25.2 to 7.26.0 (#98) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.25.2 to 7.26.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.25.2...v7.26.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`aaddf78`](https://github.com/supabase/auth-py/commit/aaddf78696aa599264a0dd665458ec491cc57c32)) + +* chore(deps-dev): bump faker from 13.3.0 to 13.3.1 (#97) + +Bumps [faker](https://github.com/joke2k/faker) from 13.3.0 to 13.3.1. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.3.0...v13.3.1) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`26428de`](https://github.com/supabase/auth-py/commit/26428de8aa1c1202adc65ca4e1c4687fd62badf5)) + +* chore(deps-dev): bump pytest-asyncio from 0.18.1 to 0.18.2 (#96) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.1 to 0.18.2. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.1...v0.18.2) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`6e7b257`](https://github.com/supabase/auth-py/commit/6e7b257023497e6fecd79d396cfc106762f4785c)) + +* chore(deps-dev): bump faker from 13.2.0 to 13.3.0 (#95) + +Bumps [faker](https://github.com/joke2k/faker) from 13.2.0 to 13.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.2.0...v13.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`627404c`](https://github.com/supabase/auth-py/commit/627404cd5dc93fb0ecb4f594922360d9fd048a68)) + +* chore(deps-dev): bump python-semantic-release from 7.25.1 to 7.25.2 (#94) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.25.1 to 7.25.2. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.25.1...v7.25.2) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`3273350`](https://github.com/supabase/auth-py/commit/327335056f1c82684019436aafde129573bc91a8)) + +* chore(deps-dev): bump python-semantic-release from 7.25.0 to 7.25.1 (#93) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.25.0 to 7.25.1. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.25.0...v7.25.1) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`c2e54a6`](https://github.com/supabase/auth-py/commit/c2e54a698de49581c6e72dea1f918b4745cd67dc)) + +* chore(deps-dev): bump faker from 13.0.0 to 13.2.0 (#92) + +Bumps [faker](https://github.com/joke2k/faker) from 13.0.0 to 13.2.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v13.0.0...v13.2.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`e0156bc`](https://github.com/supabase/auth-py/commit/e0156bc214d1bcd5229f83190e084c1b8b1cf551)) + +* chore(deps-dev): bump python-semantic-release from 7.24.0 to 7.25.0 (#91) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.24.0 to 7.25.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.24.0...v7.25.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`9b30df6`](https://github.com/supabase/auth-py/commit/9b30df6872f3b36e8cdd277ae84161233e6bd3c3)) + +* chore(deps-dev): bump faker from 12.3.3 to 13.0.0 (#90) + +Bumps [faker](https://github.com/joke2k/faker) from 12.3.3 to 13.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v12.3.3...v13.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`ff1653e`](https://github.com/supabase/auth-py/commit/ff1653eb9ab98b99e6200d3bfbea7a36dbadf079)) + +* chore(deps-dev): bump faker from 12.3.0 to 12.3.3 (#89) + +Bumps [faker](https://github.com/joke2k/faker) from 12.3.0 to 12.3.3. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v12.3.0...v12.3.3) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`da7c173`](https://github.com/supabase/auth-py/commit/da7c173353b683bd682b8f84325d47b9a2818b6c)) + +* chore(deps-dev): bump pytest from 7.0.0 to 7.0.1 (#88) + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.0 to 7.0.1. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/7.0.0...7.0.1) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`ec198b0`](https://github.com/supabase/auth-py/commit/ec198b07e200cfd98c8e7f8e3e946a1bcd3cbf97)) + +* chore(deps-dev): bump faker from 12.2.0 to 12.3.0 (#87) + +Bumps [faker](https://github.com/joke2k/faker) from 12.2.0 to 12.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v12.2.0...v12.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`3addee0`](https://github.com/supabase/auth-py/commit/3addee0a7b9f4af834f11bab86b7dc4480f6635c)) + +* chore(deps-dev): bump pytest-asyncio from 0.17.2 to 0.18.1 (#86) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.17.2 to 0.18.1. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.17.2...v0.18.1) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`f14e19d`](https://github.com/supabase/auth-py/commit/f14e19d1a31a1f61fd192f48d68684f591f08a02)) + +* chore(deps-dev): bump faker from 12.1.0 to 12.2.0 (#85) + +Bumps [faker](https://github.com/joke2k/faker) from 12.1.0 to 12.2.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v12.1.0...v12.2.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`df71a96`](https://github.com/supabase/auth-py/commit/df71a964f56172a4fd6292defc76f52a812f8a88)) + +* chore(deps-dev): bump faker from 12.0.0 to 12.1.0 (#83) + +Bumps [faker](https://github.com/joke2k/faker) from 12.0.0 to 12.1.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v12.0.0...v12.1.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`809b75d`](https://github.com/supabase/auth-py/commit/809b75d2c02b489e7b4a2fe4c7944343b97a6912)) + +* chore(deps-dev): bump pytest from 6.2.5 to 7.0.0 (#82) + +Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0. +- [Release notes](https://github.com/pytest-dev/pytest/releases) +- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) +- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.0) + +--- +updated-dependencies: +- dependency-name: pytest + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`7a749ac`](https://github.com/supabase/auth-py/commit/7a749ac3227ba0825a211dd85503e81a89f9708b)) + +* chore(deps-dev): bump faker from 11.3.0 to 12.0.0 (#81) + +Bumps [faker](https://github.com/joke2k/faker) from 11.3.0 to 12.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v11.3.0...v12.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`f54751a`](https://github.com/supabase/auth-py/commit/f54751a7baba7dd250d16193efd4eb7522454cf1)) + +* chore(deps-dev): bump black from 21.12b0 to 22.1.0 (#80) + +Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.1.0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/commits/22.1.0) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`a284212`](https://github.com/supabase/auth-py/commit/a28421291f145c11cbc36c6a2c46c86803a7e848)) + +* chore(deps): bump httpx from 0.21.3 to 0.22.0 (#79) + +Bumps [httpx](https://github.com/encode/httpx) from 0.21.3 to 0.22.0. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.21.3...0.22.0) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`9e2101d`](https://github.com/supabase/auth-py/commit/9e2101dfd657f30a0b6665943dfbd51b51299deb)) + +* chore(deps-dev): bump python-semantic-release from 7.23.0 to 7.24.0 (#78) + +Bumps [python-semantic-release](https://github.com/relekang/python-semantic-release) from 7.23.0 to 7.24.0. +- [Release notes](https://github.com/relekang/python-semantic-release/releases) +- [Changelog](https://github.com/relekang/python-semantic-release/blob/master/CHANGELOG.md) +- [Commits](https://github.com/relekang/python-semantic-release/compare/v7.23.0...v7.24.0) + +--- +updated-dependencies: +- dependency-name: python-semantic-release + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`84c6071`](https://github.com/supabase/auth-py/commit/84c60712cae9fe2277530ef525960d795d52f7be)) + +### Fix + +* fix: upgrade dependencies and fix tests (#126) + +* fix: upgrade dependencies + +* fix: update docker-compose of infra + +* chore: upgrade pre-commit config + +* chore: upgrade github action for ci ([`c9f2bde`](https://github.com/supabase/auth-py/commit/c9f2bde349f3f4e415366966ce0a39a1ac2084f2)) + +### Unknown + +* Merge pull request #99 from supabase-community/dependabot/pip/main/pytest-7.1.0 + +chore(deps-dev): bump pytest from 7.0.1 to 7.1.0 ([`657f07b`](https://github.com/supabase/auth-py/commit/657f07b8dc6a7a49ef0badf11e6e97d143a3250a)) + +## v0.5.0 (2022-01-20) + +### Chore + +* chore(release): bump version to v0.5.0 + +Automatically generated by python-semantic-release ([`888ba1b`](https://github.com/supabase/auth-py/commit/888ba1b9bcb8c4c89c3910408213053afac8e553)) + +* chore: set upload_to_repository to true ([`6316827`](https://github.com/supabase/auth-py/commit/63168276afd5e7786c8c55baadceef6ad60ab14f)) + +* chore(deps-dev): bump pytest-asyncio from 0.17.1 to 0.17.2 (#73) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.17.1 to 0.17.2. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.17.1...v0.17.2) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`c6d4d09`](https://github.com/supabase/auth-py/commit/c6d4d09deac00bf0c58b3a4f7da6a7303c423790)) + +* chore(deps-dev): bump pre-commit from 2.16.0 to 2.17.0 (#74) + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.16.0 to 2.17.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.16.0...v2.17.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`150ae9d`](https://github.com/supabase/auth-py/commit/150ae9d71c9d034d0f86d43683203e1702266f57)) + +* chore(deps-dev): bump pytest-asyncio from 0.17.0 to 0.17.1 (#72) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.17.0 to 0.17.1. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.17.0...v0.17.1) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`bc6f391`](https://github.com/supabase/auth-py/commit/bc6f391f07ba22467503cf25be3db48bebd58c14)) + +### Documentation + +* docs: add maintainers file ([`2f5f005`](https://github.com/supabase/auth-py/commit/2f5f005235e90127cb7effde7396bb55088b815f)) + +### Feature + +* feat: add create user param to sign in (#75) + +* feat: add create user param to sign in + +ref: https://github.com/supabase/gotrue/pull/318 + +* 'Refactored by Sourcery' (#76) + +Co-authored-by: Sourcery AI <> + +* chore: format code + +* chore: format code + +Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> ([`57ec6d8`](https://github.com/supabase/auth-py/commit/57ec6d8efe1233c1b90a8585045e6f85a4a3c17b)) + +## v0.4.0 (2022-01-17) + +### Chore + +* chore(release): bump version to v0.4.0 + +Automatically generated by python-semantic-release ([`d2e138c`](https://github.com/supabase/auth-py/commit/d2e138c8143cceab47dc6cd67089a53c0f259be9)) + +### Feature + +* feat: add notion to enum of providers (#70) + +* feat: add notion to enum of providers + +* 'Refactored by Sourcery' (#71) + +Co-authored-by: Sourcery AI <> + +Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> ([`a8f2c45`](https://github.com/supabase/auth-py/commit/a8f2c45b25c9d008de7a5e1e6f18cc47a259c73c)) + +## v0.3.5 (2022-01-15) + +### Chore + +* chore(release): bump version to v0.3.5 + +Automatically generated by python-semantic-release ([`5034285`](https://github.com/supabase/auth-py/commit/50342856e4823136890b98d38d306cd8a83708c2)) + +* chore(deps-dev): bump pytest-asyncio from 0.16.0 to 0.17.0 (#67) + +Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.16.0 to 0.17.0. +- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) +- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.16.0...v0.17.0) + +--- +updated-dependencies: +- dependency-name: pytest-asyncio + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`e1acdf3`](https://github.com/supabase/auth-py/commit/e1acdf3036883bf7cabcb6cac9e7b9f5ac99a651)) + +### Fix + +* fix: delete_user returns Exception event if response is Ok (#68) + +* fix: delete_user returns Exception event if response is Ok + +* 'Refactored by Sourcery' (#69) + +Co-authored-by: Sourcery AI <> + +* chore: format code + +Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> ([`23c167e`](https://github.com/supabase/auth-py/commit/23c167e7082c5ddb4dd64b958aa55065c2b3e468)) + +## v0.3.4 (2022-01-13) + +### Chore + +* chore(release): bump version to v0.3.4 + +Automatically generated by python-semantic-release ([`4b5ce2f`](https://github.com/supabase/auth-py/commit/4b5ce2f583ca84a972445a7cd08ffd44bbbb03c9)) + +* chore: fix http warning use ([`19005e2`](https://github.com/supabase/auth-py/commit/19005e2b9715075a9fb9bb0a75b7b786b9710aac)) + +* chore: fix http warning use (#61) + +because the intention is good but instead receives an annoying print. ([`ae20a8e`](https://github.com/supabase/auth-py/commit/ae20a8ea7bd0979cd48e06b426a2c8534efea93c)) + +* chore: fix config ([`2f94bfd`](https://github.com/supabase/auth-py/commit/2f94bfdbc2cf20ef50ec777bbda03face1da3e85)) + +### Fix + +* fix: string formatting in `delete_user` (#64) + +* fix: string formatting (vs. javascript style) ([`d783015`](https://github.com/supabase/auth-py/commit/d783015b5d2472fe95a83f5d42efe97f79331516)) + +## v0.3.3 (2022-01-08) + +### Chore + +* chore(release): bump version to v0.3.3 + +Automatically generated by python-semantic-release ([`d355393`](https://github.com/supabase/auth-py/commit/d355393bacc339678543b2637dcc9295c41bc8b7)) + +* chore(deps): upgrade dependencies ([`029bcd4`](https://github.com/supabase/auth-py/commit/029bcd49c7d986ee2454e8517474f212f996d9c5)) + +* chore(deps): bump httpx from 0.21.2 to 0.21.3 (#60) + +Bumps [httpx](https://github.com/encode/httpx) from 0.21.2 to 0.21.3. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.21.2...0.21.3) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> + +Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ([`328f2bb`](https://github.com/supabase/auth-py/commit/328f2bb52c4cff4d5f0ced91861e574eec3242ad)) + +* chore: remove skip of tests (#59) + +* chore: remove skip of tests in async version + +* chore: remove skip of tests in sync version ([`70b1286`](https://github.com/supabase/auth-py/commit/70b128680588c8bb96ec0d0e1a2a66820a590222)) + +* chore(deps-dev): bump faker from 11.1.0 to 11.3.0 + +Bumps [faker](https://github.com/joke2k/faker) from 11.1.0 to 11.3.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v11.1.0...v11.3.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`67a52e8`](https://github.com/supabase/auth-py/commit/67a52e8ad178695367bda26d01fc7c469803487e)) + +* chore(deps): bump httpx from 0.21.1 to 0.21.2 + +Bumps [httpx](https://github.com/encode/httpx) from 0.21.1 to 0.21.2. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.21.1...0.21.2) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`52c9a4a`](https://github.com/supabase/auth-py/commit/52c9a4ae593fe80471a37e0e3a328345816fac53)) + +* chore: filter deploy section by repo owner ([`c28e6c7`](https://github.com/supabase/auth-py/commit/c28e6c76152fc81882b514b108e629c1aea3504f)) + +* chore: add ignore md rules to dev container and fix changelog ([`2638c1b`](https://github.com/supabase/auth-py/commit/2638c1b5bc1da97826812546b4e7902111b46fa9)) + +* chore: update poetry version in ci github action ([`a1e765d`](https://github.com/supabase/auth-py/commit/a1e765d87331638ac038c76f6a4f3f8d538571f4)) + +* chore: add python versions badge to readme ([`cb407bd`](https://github.com/supabase/auth-py/commit/cb407bd4e00c8e1c3fdfab7773a61ee797bae60f)) + +* chore: fix rule in makefile ([`455d0ce`](https://github.com/supabase/auth-py/commit/455d0cefe2163f24f3246a8673a03ab10730bc19)) + +### Unknown + +* Merge pull request #57 from supabase-community/dependabot/pip/main/faker-11.3.0 + +chore(deps-dev): bump faker from 11.1.0 to 11.3.0 ([`2ffcd22`](https://github.com/supabase/auth-py/commit/2ffcd22050a977c8b852445cd12f493a03695fd5)) + +* Merge pull request #58 from supabase-community/dependabot/pip/main/httpx-0.21.2 + +chore(deps): bump httpx from 0.21.1 to 0.21.2 ([`79cc984`](https://github.com/supabase/auth-py/commit/79cc98453e2cebdffab7ebcdf86e6b59a98c32fd)) + +* Merge pull request #56 from leynier/main + +chore: filter deploy section by repo owner ([`b07bd7b`](https://github.com/supabase/auth-py/commit/b07bd7ba73be931569e176daef073b53edfc356f)) + +* Merge pull request #55 from leynier/chore/add-ignore-md-rules-to-dev-container-and-fix-changelog + +chore: add ignore md rules to dev container and fix changelog ([`b74576d`](https://github.com/supabase/auth-py/commit/b74576d7e20d9a5dffaac66572243aa02120f4da)) + +* Merge pull request #53 from supabase-community/chore/add-python-versions-badge-to-readme + +chore: add python versions badge to readme ([`ad2e2ea`](https://github.com/supabase/auth-py/commit/ad2e2ea0a7b3b6b27ef99dcf6c157e90fd750244)) + +* Merge pull request #54 from supabase-community/chore/fix-rule-in-makefile + +chore: fix rule in makefile ([`56fd621`](https://github.com/supabase/auth-py/commit/56fd621474f2c25f96924b9f4e666864e32fd28f)) + +## v0.3.2 (2022-01-04) + +### Chore + +* chore(release): bump version to v0.3.2 + +Automatically generated by python-semantic-release ([`4183894`](https://github.com/supabase/auth-py/commit/41838949e87bd52c9c6a6522f477c93bfa0c21ca)) + +* chore(deps): bump pydantic from 1.8.2 to 1.9.0 + +Bumps [pydantic](https://github.com/samuelcolvin/pydantic) from 1.8.2 to 1.9.0. +- [Release notes](https://github.com/samuelcolvin/pydantic/releases) +- [Changelog](https://github.com/samuelcolvin/pydantic/blob/master/HISTORY.md) +- [Commits](https://github.com/samuelcolvin/pydantic/compare/v1.8.2...v1.9.0) + +--- +updated-dependencies: +- dependency-name: pydantic + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`92b3b94`](https://github.com/supabase/auth-py/commit/92b3b944fde4507877c1e2dada5eb77a1b8209cc)) + +### Fix + +* fix: deploy action ([`467fa3f`](https://github.com/supabase/auth-py/commit/467fa3f6b9e09295806cbac3e8c4fcfe05c3147d)) + +### Unknown + +* Merge pull request #52 from leynier/fix/deploy-action + +fix: deploy action ([`4c05816`](https://github.com/supabase/auth-py/commit/4c058169fa6c43721dd03afa0dca65dcdac2f94c)) + +* Merge pull request #51 from supabase-community/dependabot/pip/main/pydantic-1.9.0 + +chore(deps): bump pydantic from 1.8.2 to 1.9.0 ([`293065c`](https://github.com/supabase/auth-py/commit/293065cf895bc5b458431769faba5ed870c251a8)) + +## v0.3.1 (2022-01-02) + +### Chore + +* chore: bumping version to v0.3.1 ([`9498105`](https://github.com/supabase/auth-py/commit/9498105ed63903a5a04c427cf345d15235fef1e3)) + +### Unknown + +* Merge pull request #49 from supabase-community/inherit-from-exception + +Inherit from Exception ([`f415aa0`](https://github.com/supabase/auth-py/commit/f415aa0a7a2cdb4a54cd2c61e5aa2db6221aa05e)) + +* Inherit from Exception + +Inherit from Exception instead of from BaseException. +More info: [https://docs.python.org/3/tutorial/errors.html#tut-userexceptions](https://docs.python.org/3/tutorial/errors.html#tut-userexceptions) ([`3347136`](https://github.com/supabase/auth-py/commit/33471366b95b32303314c6b4d3938cd768a28fc4)) + +## v0.3.0 (2021-12-29) + +### Chore + +* chore(deps-dev): bump faker from 11.0.0 to 11.1.0 + +Bumps [faker](https://github.com/joke2k/faker) from 11.0.0 to 11.1.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v11.0.0...v11.1.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`2802b80`](https://github.com/supabase/auth-py/commit/2802b80d0e6169117f169cb26df88dd532954205)) + +* chore(deps-dev): bump faker from 10.0.0 to 11.0.0 + +Bumps [faker](https://github.com/joke2k/faker) from 10.0.0 to 11.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v10.0.0...v11.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`fe56fa2`](https://github.com/supabase/auth-py/commit/fe56fa2d4427746797cfba01982193adea67f164)) + +* chore(deps-dev): bump commitizen from 2.20.2 to 2.20.3 + +Bumps [commitizen](https://github.com/commitizen-tools/commitizen) from 2.20.2 to 2.20.3. +- [Release notes](https://github.com/commitizen-tools/commitizen/releases) +- [Changelog](https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md) +- [Commits](https://github.com/commitizen-tools/commitizen/compare/v2.20.2...v2.20.3) + +--- +updated-dependencies: +- dependency-name: commitizen + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`1f7ecb8`](https://github.com/supabase/auth-py/commit/1f7ecb8a44f77b14552e678fc244c78f38b41b73)) + +* chore(deps-dev): bump sphinx from 4.3.1 to 4.3.2 + +Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.1 to 4.3.2. +- [Release notes](https://github.com/sphinx-doc/sphinx/releases) +- [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) +- [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.1...v4.3.2) + +--- +updated-dependencies: +- dependency-name: sphinx + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`57c73f3`](https://github.com/supabase/auth-py/commit/57c73f338e5135f38f09d88d6e60442848a59bbb)) + +* chore(deps-dev): bump faker from 9.8.2 to 10.0.0 + +Bumps [faker](https://github.com/joke2k/faker) from 9.8.2 to 10.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v9.8.2...v10.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`d012ae8`](https://github.com/supabase/auth-py/commit/d012ae88341accfa9b79666d3b9e1a68cdf594f4)) + +* chore(deps): update poetry.lock ([`882388f`](https://github.com/supabase/auth-py/commit/882388f0b162cd4dae714623c3c5dde5a4ab8b72)) + +* chore(deps-dev): bump commitizen from 2.20.0 to 2.20.2 + +Bumps [commitizen](https://github.com/commitizen-tools/commitizen) from 2.20.0 to 2.20.2. +- [Release notes](https://github.com/commitizen-tools/commitizen/releases) +- [Changelog](https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md) +- [Commits](https://github.com/commitizen-tools/commitizen/compare/v2.20.0...v2.20.2) + +--- +updated-dependencies: +- dependency-name: commitizen + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`4210dba`](https://github.com/supabase/auth-py/commit/4210dba0050667cd2d7d2dac52c8a1430d08d3a5)) + +* chore: add noqa comment ([`360cc0d`](https://github.com/supabase/auth-py/commit/360cc0da2ad3a552964a61578f05ec77f87021ad)) + +* chore: add --remove-orphans to clean_infra and increase sleep time ([`ad504ae`](https://github.com/supabase/auth-py/commit/ad504ae1a2a5037d5f0d33a9fab2a67ace56c320)) + +* chore(deps): update poetry.lock ([`3130b5a`](https://github.com/supabase/auth-py/commit/3130b5afd13ad2655b90655aa3be5f5e5c381db7)) + +* chore(deps-dev): bump black from 21.11b1 to 21.12b0 + +Bumps [black](https://github.com/psf/black) from 21.11b1 to 21.12b0. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/commits) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`bbc76f5`](https://github.com/supabase/auth-py/commit/bbc76f50ceb54a105f32c14c25d57e2d9caf0727)) + +* chore(deps-dev): bump faker from 9.9.0 to 10.0.0 + +Bumps [faker](https://github.com/joke2k/faker) from 9.9.0 to 10.0.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v9.9.0...v10.0.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`475e9f6`](https://github.com/supabase/auth-py/commit/475e9f6eba17b3a67d1ca1c4cf566832658ce480)) + +* chore: update dependencies ([`59635a9`](https://github.com/supabase/auth-py/commit/59635a9c1e66109fa1ac1d54812d6e88f2b845b2)) + +* chore(deps-dev): bump pre-commit from 2.15.0 to 2.16.0 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.15.0 to 2.16.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.15.0...v2.16.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`7b8f8f7`](https://github.com/supabase/auth-py/commit/7b8f8f77ca5b9e39e7fe2cc9f72f7c49596f2954)) + +* chore(deps-dev): bump sphinx from 4.3.0 to 4.3.1 + +Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.3.0 to 4.3.1. +- [Release notes](https://github.com/sphinx-doc/sphinx/releases) +- [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) +- [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.3.0...v4.3.1) + +--- +updated-dependencies: +- dependency-name: sphinx + dependency-type: direct:development + update-type: version-update:semver-patch +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`5d64552`](https://github.com/supabase/auth-py/commit/5d6455266eced8dbb31ae868450ed98e71c072a1)) + +* chore: fix ci action with ignore chore commits ([`baa0acf`](https://github.com/supabase/auth-py/commit/baa0acfa1f93896e5d36694ba8abb595b20f88c2)) + +* chore(deps-dev): bump faker from 9.8.2 to 9.9.0 + +Bumps [faker](https://github.com/joke2k/faker) from 9.8.2 to 9.9.0. +- [Release notes](https://github.com/joke2k/faker/releases) +- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) +- [Commits](https://github.com/joke2k/faker/compare/v9.8.2...v9.9.0) + +--- +updated-dependencies: +- dependency-name: faker + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`a905349`](https://github.com/supabase/auth-py/commit/a9053499ff1616b05cd3cf40b843a3b774ae9719)) + +* chore(deps-dev): bump pre-commit from 2.15.0 to 2.16.0 + +Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.15.0 to 2.16.0. +- [Release notes](https://github.com/pre-commit/pre-commit/releases) +- [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) +- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.15.0...v2.16.0) + +--- +updated-dependencies: +- dependency-name: pre-commit + dependency-type: direct:development + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`fc369d6`](https://github.com/supabase/auth-py/commit/fc369d6001ed570fafbef4e8275a3e2c43f552ef)) + +* chore: add make rules for generate html report and clean infra ([`e4a1405`](https://github.com/supabase/auth-py/commit/e4a1405bbc101ecdb5c9d3f5cbe1b38f82b8fadd)) + +* chore: remove html report for try fix the gh action ([`f717a9f`](https://github.com/supabase/auth-py/commit/f717a9f45bcf5f7a1310d028f35cb528e7ce3404)) + +* chore: add html report to make rul ([`26783b5`](https://github.com/supabase/auth-py/commit/26783b583b28be8c35eb0d865242a60bd0567973)) + +* chore: add test ci and codecov badges ([`38597b3`](https://github.com/supabase/auth-py/commit/38597b37be5a16027186e719583d6477fd505367)) + +* chore: add badges to README.md ([`a74a0bf`](https://github.com/supabase/auth-py/commit/a74a0bfdf93b8efe3fd3a71e179e3a37181c54c6)) + +* chore: add pragma no cover comments ([`1256518`](https://github.com/supabase/auth-py/commit/1256518d534a0d84785a7fd60174fe25795dffd0)) + +* chore(deps): bump httpx from 0.20.0 to 0.21.1 + +Bumps [httpx](https://github.com/encode/httpx) from 0.20.0 to 0.21.1. +- [Release notes](https://github.com/encode/httpx/releases) +- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) +- [Commits](https://github.com/encode/httpx/compare/0.20.0...0.21.1) + +--- +updated-dependencies: +- dependency-name: httpx + dependency-type: direct:production + update-type: version-update:semver-minor +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`3145377`](https://github.com/supabase/auth-py/commit/3145377da08dc4d595d4e01995fec16717f04661)) + +* chore(deps-dev): bump flake8 from 3.9.2 to 4.0.1 + +Bumps [flake8](https://github.com/pycqa/flake8) from 3.9.2 to 4.0.1. +- [Release notes](https://github.com/pycqa/flake8/releases) +- [Commits](https://github.com/pycqa/flake8/compare/3.9.2...4.0.1) + +--- +updated-dependencies: +- dependency-name: flake8 + dependency-type: direct:development + update-type: version-update:semver-major +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`acde964`](https://github.com/supabase/auth-py/commit/acde9649bdb2fd5f198c30f039c38851c5f66abe)) + +* chore(deps-dev): bump black from 21.10b0 to 21.11b1 + +Bumps [black](https://github.com/psf/black) from 21.10b0 to 21.11b1. +- [Release notes](https://github.com/psf/black/releases) +- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) +- [Commits](https://github.com/psf/black/commits) + +--- +updated-dependencies: +- dependency-name: black + dependency-type: direct:development +... + +Signed-off-by: dependabot[bot] <support@github.com> ([`415f7d9`](https://github.com/supabase/auth-py/commit/415f7d93559111097ee6e8ddccd137423239f0b1)) + +* chore: remove unused noqa comment ([`fdc6318`](https://github.com/supabase/auth-py/commit/fdc6318c90bbd1b9696e8e521e8a60f2741862f2)) + +* chore: update dependencies ([`83e8f5e`](https://github.com/supabase/auth-py/commit/83e8f5e79d8465015b0796955e37b32dcd62552d)) + +* chore: remove Python 3.10 for unknow error ([`a261996`](https://github.com/supabase/auth-py/commit/a261996b9bfedb66f4bfc64f7ab5ad69ab13e6ce)) + +* chore: add Python 3.10 to GitHub Action ([`d782994`](https://github.com/supabase/auth-py/commit/d7829948628eff686d0836ffba7047abab1cf3a0)) + +* chore: remove noqa comments ([`8d2c74e`](https://github.com/supabase/auth-py/commit/8d2c74e5592f6f64dccd40116661ccfc97c88f0e)) + +* chore: revert to use single _ in "private" methods ([`45dbc4f`](https://github.com/supabase/auth-py/commit/45dbc4fde3c44993080400eb9217da1fa186b230)) + +* chore: changes by run pre-commit ([`f5b0b01`](https://github.com/supabase/auth-py/commit/f5b0b010cc8c338167956ece326745d54c057aa5)) + +* chore: split some make rules ([`843008b`](https://github.com/supabase/auth-py/commit/843008b9382adf2b595c8c6e24c1afc060417ef3)) + +* chore: add logs ([`068fe0d`](https://github.com/supabase/auth-py/commit/068fe0d4c65309b2be9fa1ed63864b79df6a3e84)) + +* chore: run ci in all branches ([`b3cba71`](https://github.com/supabase/auth-py/commit/b3cba7129d2c4bcb0120b79a7dab0f5f1caf26b2)) + +* chore: regenerate sync client ([`72b85b5`](https://github.com/supabase/auth-py/commit/72b85b59ccfe79f9cd56a730866931cee4379bbe)) + +* chore: separate tests rule in multiples rules ([`5815566`](https://github.com/supabase/auth-py/commit/5815566afe4cf56350e586ffde2dc7525b20098d)) + +* chore: add dependencies for code coverage and fix tests command ([`3ab70a5`](https://github.com/supabase/auth-py/commit/3ab70a5aded79520dd0ec3931f39764d7c24b5b4)) + +* chore: format code by pre-commit ([`2fbcb70`](https://github.com/supabase/auth-py/commit/2fbcb705f7be07e2d03471c004a2b3ed043f9b5a)) + +* chore: regenerate sync client ([`8ddd940`](https://github.com/supabase/auth-py/commit/8ddd940343e72ce3c9fd9eafa57ee160249ed50f)) + +* chore: format code of sync client ([`bfc5fab`](https://github.com/supabase/auth-py/commit/bfc5fabdd94e1826774f90a2fd4322848a9f719d)) + +* chore: add doc to on_auth_state_change ([`267c62c`](https://github.com/supabase/auth-py/commit/267c62ca3376c73be4202db368539b55c5096b51)) + +* chore: fix name of ci test job ([`2ac3930`](https://github.com/supabase/auth-py/commit/2ac3930946f8ff28fa2a1f68e6a36c95c52faf27)) + +* chore: change order of strategy parameter ([`ae0ba3f`](https://github.com/supabase/auth-py/commit/ae0ba3f2dc2363197404ba169ea0f74552eec240)) + +* chore: fix name of ci test job ([`112ce71`](https://github.com/supabase/auth-py/commit/112ce71e7180fec0f055e68b940290497b0d3e62)) + +* chore: format conftest.py ([`3400431`](https://github.com/supabase/auth-py/commit/34004311d70913b2d53e8a22f05dcedba1c48518)) + +* chore: formatting code ([`f9ff600`](https://github.com/supabase/auth-py/commit/f9ff600f82f14ee43be0a15d7eec91ce5079db2a)) + +* chore: fix warnings of markdown files ([`8992382`](https://github.com/supabase/auth-py/commit/8992382916932a064c208b4affbe8ed7b5d53892)) + +* chore: add poetry files (pyproject.toml and poetry.lock) + +Remove requirements.txt and setup.py ([`01aea7f`](https://github.com/supabase/auth-py/commit/01aea7f611690c007983d6f418d1ecf215f9b3e7)) + +* chore: merge .gitignore files ([`7ce2d11`](https://github.com/supabase/auth-py/commit/7ce2d1176d955949adc51d0337acabf3e12c4e1d)) + +* chore: fix title of license ([`d82dc5c`](https://github.com/supabase/auth-py/commit/d82dc5cd491e633108b816637718ba2f3ea8f51b)) + +* chore: add code of conduct and license ([`a3dcc00`](https://github.com/supabase/auth-py/commit/a3dcc0027b3c66328ac8c302638bb6a6c0e28e3c)) + +* chore: update docker image & remove lib from imports ([`71ce191`](https://github.com/supabase/auth-py/commit/71ce1918f5ad0372557036a99d0b5ea40a85ea46)) + +* chore: update gitignore to allow lib ([`2b46d3a`](https://github.com/supabase/auth-py/commit/2b46d3af24330af131ac0c4fa3c035bf408512ba)) + +* chore: unpeg requests ([`80486d1`](https://github.com/supabase/auth-py/commit/80486d14c6c1966c212a3656fa8f5173ab4a8c31)) + +* chore: update deps ([`c8c138b`](https://github.com/supabase/auth-py/commit/c8c138bb65748e61534a267e41f3041fe31212c2)) + +### Documentation + +* docs: update some info of readme ([`20235fe`](https://github.com/supabase/auth-py/commit/20235fe1fcb04f5496bfeafc22fd43749d143173)) + +* docs: add description to aud field of User model ([`283bd9f`](https://github.com/supabase/auth-py/commit/283bd9f687f3f50af315cb7b7a33551df944a918)) + +* docs: add comment for email preferences over phone ([`e900c49`](https://github.com/supabase/auth-py/commit/e900c49587c2d96bcda43c05ca80de0b7d99eaf1)) + +### Feature + +* feat: add job to github action for auto deploy ([`2f6bcf2`](https://github.com/supabase/auth-py/commit/2f6bcf2f85f7831319c0c86faf16027a02e50058)) + +* feat: add pre-commit hook for conventional commit ([`3eebc95`](https://github.com/supabase/auth-py/commit/3eebc95e325efa4162750661e25cb750bffb17f3)) + +* feat: add devcontainer.json for use github codespaces ([`0e78b62`](https://github.com/supabase/auth-py/commit/0e78b627c6175a177f6676082287b6cf739b9261)) + +* feat: add X-Client-Info to default headers ([`ec2c893`](https://github.com/supabase/auth-py/commit/ec2c893e275877a9fce8c327b454821b21cef4d1)) + +* feat: allow providing custom api and http client implementation + +For follow dependency injection pattern + +https://github.com/supabase/gotrue-js/pull/168 ([`c60718c`](https://github.com/supabase/auth-py/commit/c60718c0d413ea35a6c9c5076b2c0e843f068029)) + +* feat: add create_user and list_users + +https://github.com/supabase/gotrue-js/pull/166 ([`72f05e2`](https://github.com/supabase/auth-py/commit/72f05e27248597590d96d5a423f3213e7dbc6341)) + +* feat: add datetime and uuid types ([`455794f`](https://github.com/supabase/auth-py/commit/455794fdca1a2c5cbad58dcc60e62043e96f6f2a)) + +* feat: add pyupgrade pre-commit hook ([`eaeae46`](https://github.com/supabase/auth-py/commit/eaeae46a23842833118516028de26687af1a8324)) + +* feat: migrate types to pydantic models ([`a0e4008`](https://github.com/supabase/auth-py/commit/a0e40088e329388d4f45ad0e53e13f2c09963198)) + +* feat: unify logic of __recover_session and __recover_and_refresh ([`b2eb4f2`](https://github.com/supabase/auth-py/commit/b2eb4f292ac62b9b37b9b037bdb33ead7b56bd69)) + +* feat: uuid4().hex instead of str(uuid4()) in _sync client ([`b91e556`](https://github.com/supabase/auth-py/commit/b91e5569dcf6edeeb8ea59f3ca69e31cf225dcf6)) + +* feat: uuid4().hex instead of str(uuid4()) + +Co-authored-by: dreinon <67071425+dreinon@users.noreply.github.com> ([`08565ac`](https://github.com/supabase/auth-py/commit/08565ac1cf89b4c4395927fba9a8a5320f972899)) + +* feat: add --ignore-init-module-imports to .pre-commit-config.yaml + +Co-authored-by: dreinon <67071425+dreinon@users.noreply.github.com> ([`9c7a18c`](https://github.com/supabase/auth-py/commit/9c7a18c7bf74093d70e56b4e988f427c17d4133d)) + +* feat: add end-of-file-fixer to .pre-commit-config.yaml + +Co-authored-by: dreinon <67071425+dreinon@users.noreply.github.com> ([`3ef3707`](https://github.com/supabase/auth-py/commit/3ef3707023d710d1f02cc4d2c35e17fed475b919)) + +* feat: use asdict of dataclasses for to_dicts ([`3333813`](https://github.com/supabase/auth-py/commit/3333813b6ed32d9d1129b4ab57ecc699be227386)) + +* feat: use annotations from __future__ ([`b16356d`](https://github.com/supabase/auth-py/commit/b16356ddd974fc8cfef7b4ec49b2d85e1b278922)) + +* feat: add identities to User + +Reason: https://github.com/supabase/gotrue/commit/e3a52e64e3ae89e93984cdcbe822413f53a37484 ([`d042152`](https://github.com/supabase/auth-py/commit/d042152ce9731e6cd304a6c512880c3ea998d263)) + +* feat: use reflection for parsing dataclasses ([`3d226f5`](https://github.com/supabase/auth-py/commit/3d226f50d5aefde62566921ef30e2a23a030cc74)) + +* feat: implement and use cli for unasync ([`f70fdd2`](https://github.com/supabase/auth-py/commit/f70fdd200c2b676069e262d18a4d3f1cb8b4d99a)) + +* feat: force the use of keyword parameters ([`4cb8330`](https://github.com/supabase/auth-py/commit/4cb83308cd3185df4708904ef04f7858227c3009)) + +* feat: generate tests for sync client ([`46373e4`](https://github.com/supabase/auth-py/commit/46373e4233853bf2744c0c9e61d8dcc06cfea17d)) + +* feat: add Make rule for infra ([`6ac5334`](https://github.com/supabase/auth-py/commit/6ac533462173bde3776dd2fe481fae7b9268aa55)) + +* feat: add pre-commit system for check Pythonic style ([`09f9b47`](https://github.com/supabase/auth-py/commit/09f9b47c07c0b77cc5b9a031715c708d119b2d73)) + +* feat: add context manager support ([`42619ea`](https://github.com/supabase/auth-py/commit/42619eadfb6705c863377082a3ea77064f42d773)) + +* feat: implement sync/async support with httppx and unasync + +The "_sync" folder is generated automatically with the +"make build_sync" command. + +All scripts in the "_async" folder will be translated into their +synchronous form using "unasync" behind the scenes. ([`da10e87`](https://github.com/supabase/auth-py/commit/da10e875aaf34ecccc6066827e06e2b1c0e86370)) + +* feat: add poetry to CI action and add dependabot config file ([`ee916c2`](https://github.com/supabase/auth-py/commit/ee916c2760c9be7e60e52b6a126deb372423bf1a)) + +* feat: synchronize implementation of GoTrueClient ([`0a669de`](https://github.com/supabase/auth-py/commit/0a669de64170fe7074bd7e65742308be2c3ff869)) + +* feat: add CookieOptions to constructor of GoTrueApi ([`4ef1b55`](https://github.com/supabase/auth-py/commit/4ef1b55608135f88ee0ebf784fb234b28c405aa7)) + +* feat: implement SupportedStorage abstract storage and MemoryStorage storage ([`044efe8`](https://github.com/supabase/auth-py/commit/044efe828737eea26c1f1f62152e7676c9374063)) + +* feat: implement Subscription type ([`8c64876`](https://github.com/supabase/auth-py/commit/8c64876d61e3fc3b5806085fa9ec1e0885210ace)) + +* feat: implement AuthChangeEvent enum ([`a6dacaf`](https://github.com/supabase/auth-py/commit/a6dacaf4aca4c9f7b5ea801a0840ab7a3f6c118d)) + +* feat: implement CookieOptions type ([`930273f`](https://github.com/supabase/auth-py/commit/930273f28ec9e3b89954859c3ff299c406d17b67)) + +* feat: add types to API class and align implementation with gotrue-js ([`1ed9754`](https://github.com/supabase/auth-py/commit/1ed9754d932c34e3fcffafd4b63474802f22ae43)) + +* feat: add new types, implement to_dicts and align files with gotrue-js ([`25e223b`](https://github.com/supabase/auth-py/commit/25e223b7f92984239e343b02995aa86527320cc6)) + +* feat: implement dataclasses with methods for parsing ([`82d1d6c`](https://github.com/supabase/auth-py/commit/82d1d6c48626b7a4eabd9d513634eec102c9e004)) + +### Fix + +* fix: error in from_dict of APIError ([`ef250f5`](https://github.com/supabase/auth-py/commit/ef250f5985337809c8c068ceca3c5bbca72ce1ed)) + +* fix: merged default headers instead of replace + +Also add argument for replace or not default headers ([`96d390c`](https://github.com/supabase/auth-py/commit/96d390cd304baf9ceb73361bdaf8d2905af264b4)) + +* fix: error in recovery_mode of get_session_from_url ([`041406a`](https://github.com/supabase/auth-py/commit/041406aee3753e023a7ffad42a54cf9238384bce)) + +* fix: error in get_session_from_url ([`5f65e47`](https://github.com/supabase/auth-py/commit/5f65e472174eedbd0fb211ffa769cc21d6886b2a)) + +* fix: add a new TOKEN_REFRESHED event + +https://github.com/supabase/gotrue-js/commit/0add6956a22c51c785d9d735edf38cac8a2c2368 ([`e702835`](https://github.com/supabase/auth-py/commit/e7028356cda625fff435c69d9772c85c0b072b4a)) + +* fix: get recovery mode before notify sign in event + +https://github.com/supabase/gotrue-js/commit/9c0f42b50e60fac00bf52c30ad3548906cf49b0a ([`4ef96ce`](https://github.com/supabase/auth-py/commit/4ef96ce77861b62aec345f443af1e7b8710df83a)) + +* fix: remove duplicate var env in docker-compose.yml ([`6e501c9`](https://github.com/supabase/auth-py/commit/6e501c99ea115dbd537157c5da13945a17d10f15)) + +* fix: use str for declare Python 3.10 ([`2350250`](https://github.com/supabase/auth-py/commit/235025008437e3e2e07fd868632a276b90e42de1)) + +* fix: use expires_in var for avoid typing warning ([`4c7003e`](https://github.com/supabase/auth-py/commit/4c7003e6a2740f3a5c24db1a13d269ef72cfa82b)) + +* fix: use typing annotations ([`6905d45`](https://github.com/supabase/auth-py/commit/6905d451fcd6fa8448cfd9619f39e0ddc06dc45f)) + +* fix: use directly parse_obj instead of lambdas ([`177b619`](https://github.com/supabase/auth-py/commit/177b6194b8547590b1714931144ea2a1f8ff35a2)) + +* fix: use parse_obj ([`383298a`](https://github.com/supabase/auth-py/commit/383298a141093bbb00feeb3799e46526db008957)) + +* fix: set prefix __ instead of _ for private methods ([`c2d37fb`](https://github.com/supabase/auth-py/commit/c2d37fb40f7bdf24d8a072bc070ce88f462ffb65)) + +* fix: .pre-commit-config.yaml format ([`747f411`](https://github.com/supabase/auth-py/commit/747f411e26aed397e2a7a28858179991e75130b9)) + +* fix: change Api by API ([`13cc086`](https://github.com/supabase/auth-py/commit/13cc0865482fe72f129a4bc47e325519475ec831)) + +* fix: change ApiError by APIError in docstrings ([`55c03a6`](https://github.com/supabase/auth-py/commit/55c03a682aa875bae1456f6d17ae6f53d4f75218)) + +* fix: change ApiError by APIError + +For follow https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles ([`98e27af`](https://github.com/supabase/auth-py/commit/98e27af6b656b78d96bb395906a3c85781ef909f)) + +* fix: add Optional to identities of User ([`21d7bda`](https://github.com/supabase/auth-py/commit/21d7bda2953f8db2d3d676939470981a144dbe05)) + +* fix: add slack adn spotify to Provider + +Reason: + +https://github.com/supabase/gotrue/commit/fc82846862435fdb96367a95b7b22503397ac0e0 + +https://github.com/supabase/gotrue/commit/79f2d6a741150a9a868fadbb1de87a2cd09da920 ([`73df8d7`](https://github.com/supabase/auth-py/commit/73df8d78573ff5599343f5f604caba56ecf3d526)) + +* fix: remove Optional to user_metadata of User + +Reason: https://github.com/supabase/gotrue/commit/62e7ccd13cd41cd5571f75c4baae5ac0fb187e7c ([`dab27ce`](https://github.com/supabase/auth-py/commit/dab27ce49345c9ab043ae91e04e460042bc49b95)) + +* fix: remove email_change_confirm_status from User + +Reason: https://github.com/supabase/gotrue/commit/df96c0678027515e5c4eb330efd0d26048aea6c6 ([`1859aa3`](https://github.com/supabase/auth-py/commit/1859aa37b2e55a1f65a4fdd6cb5c0175f20f40b1)) + +* fix: some fixes and changes requested ([`d224ec1`](https://github.com/supabase/auth-py/commit/d224ec14eeb59bb5b4d81dd2ad1e5c3cd86433b9)) + +* fix: some tipe hints ([`d1f5fc7`](https://github.com/supabase/auth-py/commit/d1f5fc774fd33283cfb4f465a39ac142b4e0b867)) + +* fix: error in client update ([`a653408`](https://github.com/supabase/auth-py/commit/a65340850db288a6f5e25d9c8370a6ad0c7de350)) + +* fix: errors in helpers and types ([`9870078`](https://github.com/supabase/auth-py/commit/98700789ab469059beb04d62617f79df0e749994)) + +* fix: ci github action ([`1ee2946`](https://github.com/supabase/auth-py/commit/1ee2946c119f9867930beab9dd6baf25c91919d6)) + +* fix: remove unnecessary async to on_auth_state_change ([`e7ebc64`](https://github.com/supabase/auth-py/commit/e7ebc64112d970673265c7b314a1e8820fc0f7e1)) + +* fix: error in remove_item of memory storage ([`9c12bd1`](https://github.com/supabase/auth-py/commit/9c12bd10d79680842cd324a50ce2ad2cee52c8a1)) + +* fix: cycle of references between helpers and types ([`fcad1af`](https://github.com/supabase/auth-py/commit/fcad1af6e0940986256678ae6059fabffdb60ec4)) + +* fix: use environ vars only if present ([`700129e`](https://github.com/supabase/auth-py/commit/700129e9f4731058506d4647ab77ed24d188cfb8)) + +* fix: reinstate lib __init__.py ([`b268219`](https://github.com/supabase/auth-py/commit/b268219db586f877e006fb7863b0504e10760e69)) + +* fix: reinstate lib.constants ([`edb9bb4`](https://github.com/supabase/auth-py/commit/edb9bb4217abd42b2c4db7b80d9a152c8fe1f3ae)) + +* fix: readjust indent in ci.yml ([`ca713c9`](https://github.com/supabase/auth-py/commit/ca713c9f58a94ae3d2bfd1137a42e44493524ba1)) + +### Test + +* test: fix docker-compose and perf ci ([`5360967`](https://github.com/supabase/auth-py/commit/53609677568216e2eae789d4416a5848269cdd0f)) + +* test: add more tests to client layer ([`a2ca372`](https://github.com/supabase/auth-py/commit/a2ca372434865675afc46582c4ca919e50fab72b)) + +* test: change message + +https://github.com/supabase/gotrue/commit/92abe187fd3d96645331542b477b37487cafce07 ([`4ec0e07`](https://github.com/supabase/auth-py/commit/4ec0e075cafd9e72369afb9c8313a71923455489)) + +* test: increases coverage + +https://github.com/supabase/gotrue-js/commit/98300a0717c36197484873e53befc5fd25c57772 ([`367a09b`](https://github.com/supabase/auth-py/commit/367a09bb59180872667b8b213c4405733eb95998)) + +* test: fix test sign up the same user twice should throw an error ([`0ea666a`](https://github.com/supabase/auth-py/commit/0ea666ab8a008284e2e03fd0a67b57db6000e640)) + +* test: fix test_sign_up_the_same_user_twice_should_throw_an_error test + +Reason: https://github.com/supabase/gotrue/commit/5bc665be3927b2ff4763c8315ee641093781ff98 ([`eb86078`](https://github.com/supabase/auth-py/commit/eb86078ea8929878d458c7d3b9324681b9780a3d)) + +* test: add various Python versions to CI ([`96eb94b`](https://github.com/supabase/auth-py/commit/96eb94bf0592587bfe34f27a9d5e6751b44eb31e)) + +* test: fix expected error message ([`1e65cc3`](https://github.com/supabase/auth-py/commit/1e65cc3bca5cd9f7f8838a20c4e7a2726515b30d)) + +* test: fix error in provider ([`e575d9b`](https://github.com/supabase/auth-py/commit/e575d9bafa66dc2318e2c9e6b7dbaa13995fc5e0)) + +* test: client with auto confirm enabled ([`b758dba`](https://github.com/supabase/auth-py/commit/b758dbae431146f0c0269ab44150904c5c899c64)) + +* test: use fixtures of pytest ([`c8daa2a`](https://github.com/supabase/auth-py/commit/c8daa2a871bcfaf306559bcbd888e303d408a39a)) + +* test: client with auto confirm disabled ([`c407a63`](https://github.com/supabase/auth-py/commit/c407a63ec79c47edc4905d08684083b77d7be091)) + +* test: client with sign ups disabled ([`08d2558`](https://github.com/supabase/auth-py/commit/08d2558557ab963ab4ed2c52153ee01489429fd2)) + +* test: handle exceptions and use context manager ([`b8330c0`](https://github.com/supabase/auth-py/commit/b8330c0c1c7193bcafe8d37dcfa4832660254160)) + +* test: implement tests for api with auto confirm disabled ([`0c29382`](https://github.com/supabase/auth-py/commit/0c29382b4f919d7a0dd8403a3bec11e7b81a8e18)) + +* test: ignore build_sync.py and conftest.py in Coverage ([`71a2c8f`](https://github.com/supabase/auth-py/commit/71a2c8f9b035bf12bcd1f2743b2791023bab7f26)) + +* test: implement provider and subscriptions tests + +Comment test_gotrue.py mean while ([`67ecf57`](https://github.com/supabase/auth-py/commit/67ecf57439e4a95d1334661d52d9a1bb05c9e87d)) + +### Unknown + +* Merge pull request #48 from supabase-community/dependabot/pip/main/faker-11.1.0 + +chore(deps-dev): bump faker from 11.0.0 to 11.1.0 ([`4590472`](https://github.com/supabase/auth-py/commit/459047270f1fc76e4103d8dc1814766ead3c3eb9)) + +* Merge pull request #47 from supabase-community/dependabot/pip/main/faker-11.0.0 + +chore(deps-dev): bump faker from 10.0.0 to 11.0.0 ([`116d9ba`](https://github.com/supabase/auth-py/commit/116d9ba296158ac8a705556287a04006167bb046)) + +* Merge pull request #46 from leynier/main + +chore(deps-dev): bump sphinx from 4.3.0 to 4.3.2 and commitizen from 2.20.0 to 2.20.3 ([`544d8f1`](https://github.com/supabase/auth-py/commit/544d8f160100ab9e104ac3adf509c572c3eb7f53)) + +* Merge pull request #10 from leynier/dependabot/pip/main/commitizen-2.20.3 + +chore(deps-dev): bump commitizen from 2.20.2 to 2.20.3 ([`5846912`](https://github.com/supabase/auth-py/commit/5846912d6e832b851bf26fca4821e2228fdf7e9f)) + +* Merge pull request #11 from leynier/dependabot/pip/main/sphinx-4.3.2 + +chore(deps-dev): bump sphinx from 4.3.1 to 4.3.2 ([`308380b`](https://github.com/supabase/auth-py/commit/308380b8685d68d57d6c0bf34d8eff61f3d38dd2)) + +* Merge pull request #37 from leynier/main + +fix: error in recovery_mode of get_session_from_url ([`740d866`](https://github.com/supabase/auth-py/commit/740d866ff0e4080c80f2cd5d1d199337ca236266)) + +* Merge remote-tracking branch 'remotes/upstream/main' ([`0f05aa8`](https://github.com/supabase/auth-py/commit/0f05aa84da19088776c48cd9f0c7289ea45d58f2)) + +* Merge pull request #39 from supabase-community/dependabot/pip/main/faker-10.0.0 + +chore(deps-dev): bump faker from 9.8.2 to 10.0.0 ([`3878d91`](https://github.com/supabase/auth-py/commit/3878d910f8abbb9d92a88bfbb476d3a482fe538f)) + +* Merge pull request #9 from leynier/dependabot/pip/main/commitizen-2.20.2 + +chore(deps-dev): bump commitizen from 2.20.0 to 2.20.2 ([`838b136`](https://github.com/supabase/auth-py/commit/838b1369591a83a55a7731189f8c87d783b75b58)) + +* Merge pull request #8 from leynier/dependabot/pip/main/faker-10.0.0 + +chore(deps-dev): bump faker from 9.9.0 to 10.0.0 ([`31b537a`](https://github.com/supabase/auth-py/commit/31b537acdda45748f9c0370702242f14d9c1345f)) + +* Merge branch 'main' into dependabot/pip/main/faker-10.0.0 ([`7408f5f`](https://github.com/supabase/auth-py/commit/7408f5f5d15e98602200bf3220000c52c4d80074)) + +* Merge pull request #7 from leynier/dependabot/pip/main/black-21.12b0 + +chore(deps-dev): bump black from 21.11b1 to 21.12b0 ([`7629707`](https://github.com/supabase/auth-py/commit/7629707b061a23d3ab90266855f0bf8b9679c840)) + +* Merge remote-tracking branch 'remotes/upstream/main' ([`669d9df`](https://github.com/supabase/auth-py/commit/669d9dfa10122933873f35eb1aadb7d22339e16c)) + +* Merge pull request #36 from supabase-community/dependabot/pip/main/pre-commit-2.16.0 + +chore(deps-dev): bump pre-commit from 2.15.0 to 2.16.0 ([`8f6c12f`](https://github.com/supabase/auth-py/commit/8f6c12f4c122700956210cd674e7490342c2e186)) + +* Merge pull request #4 from leynier/dependabot/pip/main/sphinx-4.3.1 + +chore(deps-dev): bump sphinx from 4.3.0 to 4.3.1 ([`adb5201`](https://github.com/supabase/auth-py/commit/adb52015157a28b75021b863926042886d5df01c)) + +* Merge pull request #5 from leynier/dependabot/pip/main/faker-9.9.0 + +chore(deps-dev): bump faker from 9.8.2 to 9.9.0 ([`6d8c9f0`](https://github.com/supabase/auth-py/commit/6d8c9f0357016e437945666596727f7b1e689540)) + +* Merge pull request #6 from leynier/dependabot/pip/main/pre-commit-2.16.0 + +chore(deps-dev): bump pre-commit from 2.15.0 to 2.16.0 ([`6d21ae4`](https://github.com/supabase/auth-py/commit/6d21ae4aa65087ee6ad2cf3870751981cff81b8a)) + +* bump: version 0.2.0 → 0.3.0 ([`7313b7b`](https://github.com/supabase/auth-py/commit/7313b7b45fd0c5660b0b2306df984bd8221e5490)) + +* Merge pull request #32 from leynier/feat/add-pre-commit-hook-for-conventional-commit + +feat: add pre-commit hook and action for conventional commit and semantic release ([`05f493d`](https://github.com/supabase/auth-py/commit/05f493dc8fa7a92692debf03fafb47954363c832)) + +* Merge branch 'supabase-community:main' into feat/add-pre-commit-hook-for-conventional-commit ([`19f4556`](https://github.com/supabase/auth-py/commit/19f4556f9210b7fcca67445b4d7ddc03af6733d9)) + +* Merge pull request #28 from leynier/chore/add-badges-to-readme + +chore: add badges to README.md ([`9e38a6a`](https://github.com/supabase/auth-py/commit/9e38a6af245b194ee8f77ca0c6e7c03640bca4ee)) + +* Update README.md ([`2044be7`](https://github.com/supabase/auth-py/commit/2044be7e59004055c82337bd19b6049bdeefe824)) + +* Merge pull request #29 from leynier/chore/improve-coverage + +chore: improve coverage ([`d35d971`](https://github.com/supabase/auth-py/commit/d35d971edda95b9e8dbc89d7f1c1cd1d570383d6)) + +* chore: add pragma no cover comments ([`547e245`](https://github.com/supabase/auth-py/commit/547e2455b306e205f8b94d4fc2049cd6510c0e5f)) + +* Merge pull request #26 from supabase-community/dependabot/pip/main/httpx-0.21.1 + +chore(deps): bump httpx from 0.20.0 to 0.21.1 ([`4889b38`](https://github.com/supabase/auth-py/commit/4889b3871f5c5af4d5c425d4f3d697313dad0a2f)) + +* Merge pull request #25 from supabase-community/dependabot/pip/main/flake8-4.0.1 + +chore(deps-dev): bump flake8 from 3.9.2 to 4.0.1 ([`f129103`](https://github.com/supabase/auth-py/commit/f1291037a97b7e0679efa64c211866a4a224dc72)) + +* Merge pull request #24 from supabase-community/dependabot/pip/main/black-21.11b1 + +chore(deps-dev): bump black from 21.10b0 to 21.11b1 ([`4259e38`](https://github.com/supabase/auth-py/commit/4259e3852ec723ba5c84e1d1649cdeef08aa40e2)) + +* Merge pull request #23 from leynier/feat/dataclasses + +Add dataclasses, sync/async, feature-parity with the js-client and tests ([`fb10c5c`](https://github.com/supabase/auth-py/commit/fb10c5ca6bc44dcc70ff9c10eeb8be8e4add853d)) + +* Switch isort mirror by isort pre-commit hook ([`427d5d4`](https://github.com/supabase/auth-py/commit/427d5d452c6d86b2cd1cee4cc8884286fcbe40bc)) + +* Fix typo in README.md + +Co-authored-by: Anand <40204976+anand2312@users.noreply.github.com> ([`ec0bdb5`](https://github.com/supabase/auth-py/commit/ec0bdb5b64f13115cf8fb52c73445527504ffccc)) + +* Use latest actions-poetry ([`ce47032`](https://github.com/supabase/auth-py/commit/ce4703277726ec77ea4d2b190d23e93d095260c6)) + +* Use pydantic parse_obj_as helper method ([`64162f6`](https://github.com/supabase/auth-py/commit/64162f6b03cc901edef3c4d6d1261ad49a332313)) + +* Add python 3.10 to GH Actions fix ([`57473a2`](https://github.com/supabase/auth-py/commit/57473a20fa92f36d5f35abcd106b9efd3fe40ebf)) + +* Formatting ([`0f3210f`](https://github.com/supabase/auth-py/commit/0f3210f9fccbc9ba27a2dc951b20c665fdfd5721)) + +* Add parse_response method to custom pydantic base model ([`cb4624d`](https://github.com/supabase/auth-py/commit/cb4624d7822575e03a47726a2b6bc8e25cbc3162)) + +* deps: add pydantic dependency ([`9df4927`](https://github.com/supabase/auth-py/commit/9df49274405a88e201959eda9e54c6288222c51b)) + +* Merge pull request #19 from leynier/feat/use-poetry + +Use poetry for dependency and environment management ([`9ba3192`](https://github.com/supabase/auth-py/commit/9ba3192dbdccd2f02a4819b52dd6cf51095af7e7)) + +* Merge pull request #16 from leynier/fix/title-of-license + +chore: fix title of license ([`f57b99e`](https://github.com/supabase/auth-py/commit/f57b99ea3f0a8528883c9e73a028ee2cc1b3b2fb)) + +* Merge pull request #14 from bariqhibat/bariqhibat/support-phone-otp + +Add support for phone otp ([`f237f15`](https://github.com/supabase/auth-py/commit/f237f1584cf04d1f5cb53524ad2f72e9bd0def47)) + +* fix comments + +Signed-off-by: Bariq <bariqhibat@gmail.com> ([`202346a`](https://github.com/supabase/auth-py/commit/202346a62940c55883e24b4efd1604e690a9e9a6)) + +* add new tests for signup by phone and verify otp + +Signed-off-by: Bariq <bariqhibat@gmail.com> ([`7162a47`](https://github.com/supabase/auth-py/commit/7162a472194fc59ae3647bd89722df50cb8ea72a)) + +* create new api for signup with phone + +Signed-off-by: Bariq <bariqhibat@gmail.com> ([`1870b54`](https://github.com/supabase/auth-py/commit/1870b544435ecc05da61965f4dfbe5dd4732126c)) + +* create client for otp functions + +Signed-off-by: Bariq <bariqhibat@gmail.com> ([`68f699b`](https://github.com/supabase/auth-py/commit/68f699bf28a86ff892de36fac4af2259f2733862)) + +* create api for otp functions + +Signed-off-by: Bariq <bariqhibat@gmail.com> ([`83031ad`](https://github.com/supabase/auth-py/commit/83031adee23cd74ab6fa9c6d09ce6b286a04e4f0)) + +* Merge pull request #13 from supabase-community/j0_hacktoberfest + +chore: add code of conduct and license ([`0320f45`](https://github.com/supabase/auth-py/commit/0320f451e660ea62daaf2be6d0b030c5ba8d3afa)) + +* Create CONTRIBUTING.md ([`2dea30f`](https://github.com/supabase/auth-py/commit/2dea30fe0507ad36bdc28b8906ab49802a6629f4)) + +* Merge pull request #6 from supabase/j0_fix_build + +Reinstate lib and fix build ([`ac53ea8`](https://github.com/supabase/auth-py/commit/ac53ea86e0a783e12ea55f09882928db6c97dd53)) + +* Merge pull request #3 from lawrencecchen/main + +add set_auth to gotrue client ([`49c092e`](https://github.com/supabase/auth-py/commit/49c092e3a4a6d7bb5e1c08067a4c42cc2f74b5cc)) + +* remove lib from gitignore ([`757714b`](https://github.com/supabase/auth-py/commit/757714bdd53d4a0eb8a2d33b1999470f1648d543)) + +* add set_auth to gotrue client ([`87a6e68`](https://github.com/supabase/auth-py/commit/87a6e68f6e29a8755e031a83d2022106e216d69e)) + +* Merge pull request #5 from supabase/j0_deps + +Update dependencies ([`8189c49`](https://github.com/supabase/auth-py/commit/8189c49c2f2e75db5fc34ac271af375f5a2ecb36)) + +* Merge pull request #1 from fedden/main + +Bring closer to pairity with supabase/gotrue-js ([`38eddcb`](https://github.com/supabase/auth-py/commit/38eddcb51e8a59e068a358971c09425138dd852e)) + +* remove env vars ([`608c2b1`](https://github.com/supabase/auth-py/commit/608c2b19d675361d54a4a03dfa0ff9116cc0d1ea)) + +* add return statements in documentation ([`3ad5781`](https://github.com/supabase/auth-py/commit/3ad5781db4dddf79824d3bdf15cfbea0835a8303)) + +* Update gotrue/client.py + +Co-authored-by: Lee Yi Jie Joel <lee.yi.jie.joel@gmail.com> ([`289b184`](https://github.com/supabase/auth-py/commit/289b1846a4e9bd5b88528ca2a0aff71a42d43b1e)) + +* Update gotrue/api.py + +Co-authored-by: Lee Yi Jie Joel <lee.yi.jie.joel@gmail.com> ([`f0e77aa`](https://github.com/supabase/auth-py/commit/f0e77aaa9a1dd6a6862879be1b53cb38ec3a2a6d)) + +* Update gotrue/api.py + +Co-authored-by: Lee Yi Jie Joel <lee.yi.jie.joel@gmail.com> ([`1828117`](https://github.com/supabase/auth-py/commit/1828117d628078ed4e8d96083e5050a2f85394a5)) + +* correct version ([`68ed44b`](https://github.com/supabase/auth-py/commit/68ed44b0e379b9884becc5fbfd4d6a6a7c8768f4)) + +* enable simple install that works for non-poetry envs ([`7cdf326`](https://github.com/supabase/auth-py/commit/7cdf3261fedbe0e279fc44d272a614c935e54926)) + +* updating the pyproject.toml ([`a7be2da`](https://github.com/supabase/auth-py/commit/a7be2da61269bb63382e9a18bc46e0767536fee0)) + +* add another todo ([`94470db`](https://github.com/supabase/auth-py/commit/94470db34e564fce3cff1e7808ed6757fbb2b199)) + +* ensuring tests pass ([`337746c`](https://github.com/supabase/auth-py/commit/337746cc82f0b256a823464f262b15bbd4741be1)) + +* document the tests ([`2b74646`](https://github.com/supabase/auth-py/commit/2b74646a7206d8aad08aa16e0f2f84a956636cdf)) + +* hopefully ensure the test ci works ([`7dea900`](https://github.com/supabase/auth-py/commit/7dea9002f11286a7945128453d8a93afb32eeb1f)) + +* link to js-client ([`44fc9da`](https://github.com/supabase/auth-py/commit/44fc9da6d387b61ed2167ff8bf4a748fcce6bcf1)) + +* add some basic documentation in README ([`3309684`](https://github.com/supabase/auth-py/commit/33096840e7545977a8237ba25b107dc3d6648ef0)) + +* cleanup tests etc ([`4808a34`](https://github.com/supabase/auth-py/commit/4808a3481b7b790410815a7362423f3ef3e0c446)) + +* add for working tests ([`c970dfe`](https://github.com/supabase/auth-py/commit/c970dfe99e498d18d434f257c37f469e5133e438)) + +* updates to get tests passing ([`8b3dc48`](https://github.com/supabase/auth-py/commit/8b3dc488deff62f4acdde54ff63260c83fd65aea)) + +* wrap up response cleanly into a dict for users ([`29e35ed`](https://github.com/supabase/auth-py/commit/29e35ed9115328c57dd89b7601b2c3e8dd411a4d)) + +* add gitignoire ([`d620032`](https://github.com/supabase/auth-py/commit/d620032ed3eae549f152e44193ee5ae8b4130813)) + +* rm crap ([`93f984b`](https://github.com/supabase/auth-py/commit/93f984bdcbb5ee9754a168028ba93b016950fbaa)) + +* sort typos ([`1a3bef2`](https://github.com/supabase/auth-py/commit/1a3bef2dd75128b008206cfb9596f591902c8718)) + +* add contstants ([`645136a`](https://github.com/supabase/auth-py/commit/645136a73558070331157928ab90bdca0dae7960)) + +* bringing closer to pairity with gotrue js ([`e753927`](https://github.com/supabase/auth-py/commit/e7539278fbcbef3a05971856d20a2a1587a9049e)) + +* Resolve merge conflicts ([`a34bea1`](https://github.com/supabase/auth-py/commit/a34bea153dee52044f6ce5e238c24aa5b3a96a2a)) + +* Update README.md ([`0201f09`](https://github.com/supabase/auth-py/commit/0201f090c06bd10703bd73c2678bbba795dd0d85)) + +* Allow headers and lint with black ([`d9c9533`](https://github.com/supabase/auth-py/commit/d9c9533d3f78d1f51fbab83e89846042cc4bf751)) + +* decrease minimum py version required ([`d768579`](https://github.com/supabase/auth-py/commit/d768579e3e9d7463e3df93c0b6b9360a1beaeb53)) + +* Update README.md ([`3a36696`](https://github.com/supabase/auth-py/commit/3a36696550805796daf5c9f5d0e2d04247aad01c)) + +* Remove user and admin ([`5aca335`](https://github.com/supabase/auth-py/commit/5aca335b7c4c6d5f72aa0a3e2b951356fafe9562)) + +* Add documentation ([`b575640`](https://github.com/supabase/auth-py/commit/b5756408790097425c0d78f9a5871ecf2a2daecd)) + +* Add usage component to README.md ([`8a8d3eb`](https://github.com/supabase/auth-py/commit/8a8d3ebd6bbdc9ed40b0d022ee744f1335204683)) + +* Switch .rst file to .md ([`b73f7ef`](https://github.com/supabase/auth-py/commit/b73f7ef2ec04db30ea75824bba1dee1d96fe2110)) + +* Add jsonify ([`18230a8`](https://github.com/supabase/auth-py/commit/18230a8cd0eb34d5f209f5fada624e939e5ad82b)) + +* Add grant token method ([`629d12b`](https://github.com/supabase/auth-py/commit/629d12b88c7159387d8325444ce2e01a35252f26)) + +* Add more methods ([`e77ef0d`](https://github.com/supabase/auth-py/commit/e77ef0d28ee0192643aa5a560bce257ffa6fc1f4)) + +* Add more methods ([`eff88f3`](https://github.com/supabase/auth-py/commit/eff88f33f192cf94bb78deb4d1bc9dbc271d9fd9)) + +* Support incremental testing ([`dda60f3`](https://github.com/supabase/auth-py/commit/dda60f327990a586f37802421f2a1fbc7b62fe1a)) + +* Add tests for settings route ([`3aa54d5`](https://github.com/supabase/auth-py/commit/3aa54d52729e32cd1ecf204eb66e050e3409035f)) + +* Restructure to support publishing to poetry ([`65d4b1c`](https://github.com/supabase/auth-py/commit/65d4b1cab978a2a221ce5f59aa1f36701127de65)) + +* Remove flake8 test for now ([`8d7502e`](https://github.com/supabase/auth-py/commit/8d7502e8a5499372b01bb9adf090c0e1374bc3b3)) + +* Fix indent level again ([`388e339`](https://github.com/supabase/auth-py/commit/388e339721d00df59644e92a78e523734d529f88)) + +* Fix indent level ([`2f8bd02`](https://github.com/supabase/auth-py/commit/2f8bd02ea011edf2fb9e2d7658944cd5ec18bbaf)) + +* Add previously removed part of ci test ([`d6c0f87`](https://github.com/supabase/auth-py/commit/d6c0f872c56569b9d693ba0a8f9adabdd6d5b085)) + +* Add placeholder test ([`b76c1cf`](https://github.com/supabase/auth-py/commit/b76c1cfaebb414f51bfe2fd1a091c73d0b7a1a64)) + +* fix branch name master->main ([`1bb5dc9`](https://github.com/supabase/auth-py/commit/1bb5dc9a456233ada58e90bcce24a1a655518a2b)) + +* Add pytest portion of workflow ([`3aeb728`](https://github.com/supabase/auth-py/commit/3aeb728e0a1a575ed4a8a513941ceffad9879252)) + +* Remove additional steps ([`2c1f4c3`](https://github.com/supabase/auth-py/commit/2c1f4c3d53926cf3645bd790375aeefca2c3a90e)) + +* Fix indentation level ([`949a33d`](https://github.com/supabase/auth-py/commit/949a33dc90e8b2fa7bcdbacb6d5319e8982ac6a9)) + +* Add .github dir ([`a958afe`](https://github.com/supabase/auth-py/commit/a958afef51270221ddff33aa2d716ad0aaa6c19d)) + +* add preliminary CI ([`5cfcae7`](https://github.com/supabase/auth-py/commit/5cfcae7814999481bc194c610472633357ade876)) + +* Test initial stubs against gotrue-example site ([`583da03`](https://github.com/supabase/auth-py/commit/583da03cfd76fe2061f4748f95775d771f6d1eb5)) + +* Update README ([`0e4fbd0`](https://github.com/supabase/auth-py/commit/0e4fbd0e1b2ef54ec3dab745dc18ae690bc04705)) + +* Restructure repository ([`4d4e554`](https://github.com/supabase/auth-py/commit/4d4e55474fdd131e8711c2d62cbaade0b5a14fb7)) + +* Initial commit ([`3d87285`](https://github.com/supabase/auth-py/commit/3d87285a1d503cdb88a91cfa3e430fac546d2564)) diff --git a/src/auth/MAINTAINERS.md b/src/auth/MAINTAINERS.md new file mode 100644 index 00000000..f2dbefe0 --- /dev/null +++ b/src/auth/MAINTAINERS.md @@ -0,0 +1,16 @@ +This page lists all active maintainers of this repository. If you were a maintainer and would like to add your name to the Emeritus list, please send us a PR. + +See CONTRIBUTING.md for general contribution guidelines. + +# Maintainers (in alphabetical order) + +- [silentworks](https://github.com/silentworks) + + +# Emeritus Maintainers (in alphabetical order) + +- [anand2312](https://github.com/anand2312) +- [dreinon](https://github.com/dreinon) +- [fedden](https://github.com/fedden) +- [J0](https://github.com/J0) +- [leynier](https://github.com/leynier) diff --git a/src/auth/Makefile b/src/auth/Makefile new file mode 100644 index 00000000..060b51cb --- /dev/null +++ b/src/auth/Makefile @@ -0,0 +1,32 @@ +tests: pytest + +pytest: start-infra + uv run --package supabase_auth pytest --cov=./ --cov-report=xml --cov-report=html -vv + +start-infra: + cd infra &&\ + docker compose down &&\ + docker compose up -d + sleep 2 + +clean-infra: + cd infra &&\ + docker compose down --remove-orphans &&\ + docker system prune -a --volumes -f + +stop-infra: + cd infra &&\ + docker compose down --remove-orphans + +sync-infra: + uv run --package supabase_auth scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra + +build-sync: + uv run --package supabase_auth scripts/run-unasync.py + +clean: + rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache + rm -f .coverage coverage.xml + +build: + uv build --package supabase_auth diff --git a/src/auth/README.md b/src/auth/README.md new file mode 100644 index 00000000..a764beab --- /dev/null +++ b/src/auth/README.md @@ -0,0 +1,159 @@ +# Auth-py + +[![CI](https://github.com/supabase-community/gotrue-py/actions/workflows/ci.yml/badge.svg)](https://github.com/supabase-community/gotrue-py/actions/workflows/ci.yml) +[![Python](https://img.shields.io/pypi/pyversions/gotrue)](https://pypi.org/project/gotrue) +[![Version](https://img.shields.io/pypi/v/gotrue?color=%2334D058)](https://pypi.org/project/gotrue) +[![Coverage Status](https://coveralls.io/repos/github/supabase/auth-py/badge.svg?branch=main)](https://coveralls.io/github/supabase/auth-py?branch=main) + +This is a Python port of the [supabase js gotrue client](https://github.com/supabase/gotrue-js). The current state is that there is a features parity but with small differences that are mentioned in the section **Differences to the JS client**. As of December 14th, we renamed to repo from `gotrue-py` to `auth-py` to mirror the changes in the JavaScript library. + +## Installation + +The package can be installed using pip or poetry: + +### Poetry + +```bash +poetry add supabase_auth +``` + +### Pip + +```bash +pip install supabase_auth +``` + +## Features + +- Full feature parity with the JavaScript client +- Support for both synchronous and asynchronous operations +- MFA (Multi-Factor Authentication) support +- OAuth and SSO integration +- Magic link and OTP authentication +- Phone number authentication +- Anonymous sign-in +- Session management with auto-refresh +- JWT token handling and verification +- User management and profile updates + +## Differences to the JS client + +It should be noted there are differences to the [JS client](https://github.com/supabase/gotrue-js). If you feel particulaly strongly about them and want to motivate a change, feel free to make a GitHub issue and we can discuss it there. + +Firstly, feature pairity is not 100% with the [JS client](https://github.com/supabase/gotrue-js). In most cases we match the methods and attributes of the [JS client](https://github.com/supabase/gotrue-js) and api classes, but is some places (e.g for browser specific code) it didn't make sense to port the code line for line. + +There is also a divergence in terms of how errors are raised. In the [JS client](https://github.com/supabase/gotrue-js), the errors are returned as part of the object, which the user can choose to process in whatever way they see fit. In this Python client, we raise the errors directly where they originate, as it was felt this was more Pythonic and adhered to the idioms of the language more directly. + +In JS we return the error, but in Python we just raise it. + +```js +const { data, error } = client.sign_up(...) +``` + +The other key difference is we do not use pascalCase to encode variable and method names. Instead we use the snake_case convention adopted in the Python language. + +Also, the `supabase_auth` library for Python parses the date-time string into `datetime` Python objects. The [JS client](https://github.com/supabase/gotrue-js) keeps the date-time as strings. + +## Usage + +The library provides both synchronous and asynchronous clients. Here are some examples: + +### Synchronous Client + +```python +from supabase_auth import SyncGoTrueClient + +headers = { + "apiKey": "my-mega-awesome-api-key", + # ... any other headers you might need. +} +client: SyncGoTrueClient = SyncGoTrueClient(url="www.genericauthwebsite.com", headers=headers) + +# Sign up with email and password +user = client.sign_up(email="example@gmail.com", password="*********") + +# Sign in with email and password +user = client.sign_in_with_password(email="example@gmail.com", password="*********") + +# Sign in with magic link +user = client.sign_in_with_otp(email="example@gmail.com") + +# Sign in with phone number +user = client.sign_in_with_otp(phone="+1234567890") + +# Sign in with OAuth +user = client.sign_in_with_oauth(provider="google") + +# Sign out +client.sign_out() + +# Get current user +user = client.get_user() + +# Update user profile +user = client.update_user({"data": {"name": "John Doe"}}) +``` + +### Asynchronous Client + +```python +from supabase_auth import AsyncGoTrueClient + +headers = { + "apiKey": "my-mega-awesome-api-key", + # ... any other headers you might need. +} +client: AsyncGoTrueClient = AsyncGoTrueClient(url="www.genericauthwebsite.com", headers=headers) + +async def main(): + # Sign up with email and password + user = await client.sign_up(email="example@gmail.com", password="*********") + + # Sign in with email and password + user = await client.sign_in_with_password(email="example@gmail.com", password="*********") + + # Sign in with magic link + user = await client.sign_in_with_otp(email="example@gmail.com") + + # Sign in with phone number + user = await client.sign_in_with_otp(phone="+1234567890") + + # Sign in with OAuth + user = await client.sign_in_with_oauth(provider="google") + + # Sign out + await client.sign_out() + + # Get current user + user = await client.get_user() + + # Update user profile + user = await client.update_user({"data": {"name": "John Doe"}}) + +# Run the async code +import asyncio +asyncio.run(main()) +``` + +### MFA Support + +The library includes support for Multi-Factor Authentication: + +```python +# List MFA factors +factors = client.mfa.list_factors() + +# Enroll a new MFA factor +enrolled_factor = client.mfa.enroll({"factor_type": "totp"}) + +# Challenge and verify MFA +challenge = client.mfa.challenge({"factor_id": "factor_id"}) +verified = client.mfa.verify({"factor_id": "factor_id", "code": "123456"}) + +# Unenroll a factor +client.mfa.unenroll({"factor_id": "factor_id"}) +``` + +## Contributions + +We would be immensely grateful for any contributions to this project. diff --git a/src/auth/docs/Makefile b/src/auth/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/src/auth/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/src/auth/docs/make.bat b/src/auth/docs/make.bat new file mode 100644 index 00000000..9534b018 --- /dev/null +++ b/src/auth/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/src/auth/docs/source/api/api.rst b/src/auth/docs/source/api/api.rst new file mode 100644 index 00000000..2f2a673d --- /dev/null +++ b/src/auth/docs/source/api/api.rst @@ -0,0 +1,5 @@ +API +====== + +.. autoclass:: gotrue._async.api.AsyncGoTrueAPI + :inherited-members: diff --git a/src/auth/docs/source/api/client.rst b/src/auth/docs/source/api/client.rst new file mode 100644 index 00000000..8ca9e744 --- /dev/null +++ b/src/auth/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/src/auth/docs/source/api/index.rst b/src/auth/docs/source/api/index.rst new file mode 100644 index 00000000..00a4f835 --- /dev/null +++ b/src/auth/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/src/auth/docs/source/conf.py b/src/auth/docs/source/conf.py new file mode 100644 index 00000000..78ccf251 --- /dev/null +++ b/src/auth/docs/source/conf.py @@ -0,0 +1,54 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- + +project = "gotrue" +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 = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.extlinks", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# 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 = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/src/auth/docs/source/examples/index.rst b/src/auth/docs/source/examples/index.rst new file mode 100644 index 00000000..d3c48433 --- /dev/null +++ b/src/auth/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/src/auth/docs/source/gotrue.rst b/src/auth/docs/source/gotrue.rst new file mode 100644 index 00000000..2efeaea4 --- /dev/null +++ b/src/auth/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/src/auth/docs/source/index.rst b/src/auth/docs/source/index.rst new file mode 100644 index 00000000..db72b04c --- /dev/null +++ b/src/auth/docs/source/index.rst @@ -0,0 +1,25 @@ + +.. gotrue documentation master file, created by + sphinx-quickstart on Sat Jan 30 15:50:30 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to gotrue's documentation! +================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + API Reference + Examples + Miscellaneous Modules + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/src/auth/docs/source/modules.rst b/src/auth/docs/source/modules.rst new file mode 100644 index 00000000..8d733c93 --- /dev/null +++ b/src/auth/docs/source/modules.rst @@ -0,0 +1,7 @@ +gotrue-py +========= + +.. toctree:: + :maxdepth: 4 + + gotrue diff --git a/src/auth/infra/db/00-schema.sql b/src/auth/infra/db/00-schema.sql new file mode 100644 index 00000000..229af46f --- /dev/null +++ b/src/auth/infra/db/00-schema.sql @@ -0,0 +1,85 @@ +CREATE SCHEMA IF NOT EXISTS auth AUTHORIZATION postgres; +-- auth.users definition +CREATE TABLE auth.users ( + instance_id uuid NULL, + id uuid NOT NULL, + aud varchar(255) NULL, + "role" varchar(255) NULL, + email varchar(255) NULL, + encrypted_password varchar(255) NULL, + confirmed_at timestamptz NULL, + invited_at timestamptz NULL, + confirmation_token varchar(255) NULL, + confirmation_sent_at timestamptz NULL, + recovery_token varchar(255) NULL, + recovery_sent_at timestamptz NULL, + email_change_token varchar(255) NULL, + email_change varchar(255) NULL, + email_change_sent_at timestamptz NULL, + last_sign_in_at timestamptz NULL, + raw_app_meta_data jsonb NULL, + raw_user_meta_data jsonb NULL, + is_super_admin bool NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT users_pkey PRIMARY KEY (id) +); +CREATE INDEX users_instance_id_email_idx ON auth.users USING btree (instance_id, email); +CREATE INDEX users_instance_id_idx ON auth.users USING btree (instance_id); +-- auth.refresh_tokens definition +CREATE TABLE auth.refresh_tokens ( + instance_id uuid NULL, + id bigserial NOT NULL, + "token" varchar(255) NULL, + user_id varchar(255) NULL, + revoked bool NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT refresh_tokens_pkey PRIMARY KEY (id) +); +CREATE INDEX refresh_tokens_instance_id_idx ON auth.refresh_tokens USING btree (instance_id); +CREATE INDEX refresh_tokens_instance_id_user_id_idx ON auth.refresh_tokens USING btree (instance_id, user_id); +CREATE INDEX refresh_tokens_token_idx ON auth.refresh_tokens USING btree (token); +-- auth.instances definition +CREATE TABLE auth.instances ( + id uuid NOT NULL, + uuid uuid NULL, + raw_base_config text NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + CONSTRAINT instances_pkey PRIMARY KEY (id) +); +-- auth.audit_log_entries definition +CREATE TABLE auth.audit_log_entries ( + instance_id uuid NULL, + id uuid NOT NULL, + payload json NULL, + created_at timestamptz NULL, + CONSTRAINT audit_log_entries_pkey PRIMARY KEY (id) +); +CREATE INDEX audit_logs_instance_id_idx ON auth.audit_log_entries USING btree (instance_id); +-- auth.schema_migrations definition +CREATE TABLE auth.schema_migrations ( + "version" varchar(255) NOT NULL, + CONSTRAINT schema_migrations_pkey PRIMARY KEY ("version") +); +INSERT INTO auth.schema_migrations (version) +VALUES ('20171026211738'), + ('20171026211808'), + ('20171026211834'), + ('20180103212743'), + ('20180108183307'), + ('20180119214651'), + ('20180125194653'); +-- Gets the User ID from the request cookie +create or replace function auth.uid() returns uuid as $$ + select nullif(current_setting('request.jwt.claim.sub', true), '')::uuid; +$$ language sql stable; +-- Gets the User ID from the request cookie +create or replace function auth.role() returns text as $$ + select nullif(current_setting('request.jwt.claim.role', true), '')::text; +$$ language sql stable; +GRANT ALL PRIVILEGES ON SCHEMA auth TO postgres; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO postgres; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA auth TO postgres; +ALTER USER postgres SET search_path = "auth"; diff --git a/src/auth/infra/docker-compose.yml b/src/auth/infra/docker-compose.yml new file mode 100644 index 00000000..b3a4fe19 --- /dev/null +++ b/src/auth/infra/docker-compose.yml @@ -0,0 +1,150 @@ +# docker-compose.yml +version: '3' +services: + gotrue: # Signup enabled, autoconfirm off + image: supabase/auth:v2.178.0 + ports: + - '9999:9999' + environment: + GOTRUE_MAILER_URLPATHS_CONFIRMATION: '/verify' + GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' + GOTRUE_JWT_EXP: 3600 + GOTRUE_DB_DRIVER: postgres + DB_NAMESPACE: auth + GOTRUE_API_HOST: 0.0.0.0 + PORT: 9999 + GOTRUE_DISABLE_SIGNUP: 'false' + API_EXTERNAL_URL: http://localhost:9999 + GOTRUE_SITE_URL: http://localhost:9999 + GOTRUE_URI_ALLOW_LIST: https://supabase.io/docs + GOTRUE_MAILER_AUTOCONFIRM: 'false' + GOTRUE_LOG_LEVEL: DEBUG + GOTRUE_OPERATOR_TOKEN: super-secret-operator-token + DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' + GOTRUE_EXTERNAL_GOOGLE_ENABLED: 'true' + GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: 53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com + GOTRUE_EXTERNAL_GOOGLE_SECRET: Sm3s8RE85rDcS36iMy8YjrpC + GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: http://localhost:9999/callback + GOTRUE_SMTP_HOST: mail + GOTRUE_SMTP_PORT: 2500 + GOTRUE_SMTP_USER: GOTRUE_SMTP_USER + GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS + GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com + GOTRUE_MAILER_SUBJECTS_CONFIRMATION: 'Please confirm' + GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' + GOTRUE_SMS_PROVIDER: 'twilio' + GOTRUE_SMS_TWILIO_ACCOUNT_SID: '${GOTRUE_SMS_TWILIO_ACCOUNT_SID}' + GOTRUE_SMS_TWILIO_AUTH_TOKEN: '${GOTRUE_SMS_TWILIO_AUTH_TOKEN}' + GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: '${GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID}' + GOTRUE_SMS_AUTOCONFIRM: 'false' + GOTRUE_COOKIE_KEY: 'sb' + depends_on: + - db + restart: on-failure + autoconfirm: # Signup enabled, autoconfirm on + image: supabase/auth:v2.178.0 + ports: + - '9998:9998' + environment: + GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' + GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["sign", "verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["verify"],"alg":"RS256"}]' + GOTRUE_JWT_EXP: 3600 + GOTRUE_DB_DRIVER: postgres + DB_NAMESPACE: auth + GOTRUE_API_HOST: 0.0.0.0 + PORT: 9998 + GOTRUE_DISABLE_SIGNUP: 'false' + API_EXTERNAL_URL: http://localhost:9998 + GOTRUE_SITE_URL: http://localhost:9998 + GOTRUE_MAILER_AUTOCONFIRM: 'true' + GOTRUE_SMS_AUTOCONFIRM: 'true' + GOTRUE_LOG_LEVEL: DEBUG + GOTRUE_OPERATOR_TOKEN: super-secret-operator-token + DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' + GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' + GOTRUE_SMTP_HOST: mail + GOTRUE_SMTP_PORT: 2500 + GOTRUE_SMTP_USER: GOTRUE_SMTP_USER + GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS + GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com + GOTRUE_COOKIE_KEY: 'sb' + depends_on: + - db + restart: on-failure + autoconfirm_with_asymmetric_keys: # Signup enabled, autoconfirm on + image: supabase/auth:v2.169.0 + ports: + - '9996:9996' + environment: + GOTRUE_JWT_SECRET: 'Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo' + GOTRUE_JWT_KEYS: '[{"kty":"oct","k":"Z7-AyPyChGNcQsX16cPBV-pPBo4q-zckDxkq1VZjATo","kid":"12580317-221c-49b6-894a-f4473b8afe39","key_ops":["verify"],"alg":"HS256"},{"kty":"RSA","n":"y3KQnIXK6wkPQ5m0XWp7z54BNZzXJk4IxXy81zFophdBBqz6u5OCMqWkC6i3WB7rlax4xjmxxyGyYRODooqCQTGahmpXryAAKc3g-gDIAq2MqVwlpmvXDavCVRK4hK7DZ6wK4MHrliSNHCuCkwIH3ofxTxgUwpSkOT58iU1ZOua5E1Y6R_Ozt3gLHha0Xa7a4V23pkP7n0xBvJPzIqiS3MZ4CQ_pz-buXYRgCPQkUJvXFFcuxmyqoYzorwQ1YVBOmH2XMx26RrCIxgj7geo9eVQ9u5qCPpQCGV5biqYMC4_m1kurOGf62URGRzXtmVzrW1PZJAeGoqMz5Fcfr8hiwQ","e":"AQAB","d":"C4XxquvpEmbw9mM-VAwz9w58Aw1fIkxJMuZdy9KAmue2RyqFCRrRxQycvgxQVi1qKpAaRx_9ccn20IjKa-psdkTY-8QKM2EcoUGH_KEOsxghX3ZYq5RwGdYgq7DjwqAjcTvNYe2Z6mcnlvDf9HOo_nG0uUYj5uGEa7meVCiNZUiSVdNGs-vOTUD8yB5pbZ4ute8ebuUzCWGQ3YwSNoWLa-dbECSO7jeobCapdB52MjEwE3_Ii8BWoySeDP-DEFX_5RTM2Zeh81zXAgmOxpZYTkjMsrznyxxBbXn7CdT8WMEXrreGZwIt3Mu6XpsLF5mwmTQ_ZyoM6tJpn5LeAhnCAQ","p":"6xy1skrnlrGUWtZFSHixn_eRA_O3GXKNBE4wziWodGZaFYsmFijZHbuQT0WFqc0epvLHNdNPvubFrVfV-U7ZIarfSSq6qBwBzDrDQS060MvjJIjrI16pKlx2X727FR1ZuwxT27dNg-wRTgKcZqXEalkvFOTEYBlCtw2-vzI0aRs","q":"3YWwOAs4GRZ9eq_fqNujACWJFyUO9QgEDPDOMg0EZhY7WkAlehTxxVXg65spWnfx_0GSc72I5N5qdbY-yDh2Dl7zIxvwnqZaKMJn4PEFkeAfyg62XlJlkHIwOVSj6vLNUDdDmG7bO2k6MyQ59jeuAemIljf9WhALNy8c9R0K3VM","dp":"KJ4LHcQnAjeng5Hk4kJHnXUtjls6VKEfj5DaiaKj2YgdI_-oEsf3ylUu9yLxloYjN4BVvgzFiBtiJzI3exyOEmzsqj1Bhe1guiGkvcvMj2nJ0fP9e1zNKM5UfPHQMjOh3tigXCLst0-_JZT55BnbNuw1YAytiFSU2_755xoLR-U","dq":"dCP7V-bJ6p1X_FLpOGau9wy262OKi_0_4mj-Mk-Q1tUhGRg4jeEdQRDdc6lN7Rilz-ZZGkVs2FGkD0MVd3PisXYmk2m6pfMhoe0K-WxkNy8Ce7Vq99jLVwgHMIenyS6zZjMTRYAZgPSShu2fVe-rU2VVLyz7r5RpzOzuibRIVfE","qi":"i7ND2teiVLkbaAs6rHfo5DiD1nlsORNYnn8Y_FjF6utb5OUljZ6-5WyEDJN9oIUX8o_Il9E6js-z7nhvPfFZHQN7ZWuYI0rO5qmsCDS9jWJ4GR61SgzZuLT7Jpp_KtwjW70x5wZ1Y-GugOP1Wct1YZWHn5YyLhvO6X_vttSmcS0","kid":"638c54b8-28c2-4b12-9598-ba12ef610a29","key_ops":["sign","verify"],"alg":"RS256"}]' + GOTRUE_JWT_EXP: 3600 + GOTRUE_DB_DRIVER: postgres + DB_NAMESPACE: auth + GOTRUE_API_HOST: 0.0.0.0 + PORT: 9996 + GOTRUE_DISABLE_SIGNUP: 'false' + API_EXTERNAL_URL: http://localhost:9996 + GOTRUE_SITE_URL: http://localhost:9996 + GOTRUE_MAILER_AUTOCONFIRM: 'true' + GOTRUE_SMS_AUTOCONFIRM: 'true' + GOTRUE_LOG_LEVEL: DEBUG + GOTRUE_OPERATOR_TOKEN: super-secret-operator-token + DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' + GOTRUE_EXTERNAL_PHONE_ENABLED: 'true' + GOTRUE_SMTP_HOST: mail + GOTRUE_SMTP_PORT: 2500 + GOTRUE_SMTP_USER: GOTRUE_SMTP_USER + GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS + GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com + GOTRUE_COOKIE_KEY: 'sb' + depends_on: + - db + restart: on-failure + disabled: # Signup disabled + image: supabase/auth:v2.178.0 + ports: + - '9997:9997' + environment: + GOTRUE_JWT_SECRET: '37c304f8-51aa-419a-a1af-06154e63707a' + GOTRUE_JWT_EXP: 3600 + GOTRUE_DB_DRIVER: postgres + DB_NAMESPACE: auth + GOTRUE_API_HOST: 0.0.0.0 + PORT: 9997 + GOTRUE_DISABLE_SIGNUP: 'true' + API_EXTERNAL_URL: http://localhost:9997 + GOTRUE_SITE_URL: http://localhost:9997 + GOTRUE_MAILER_AUTOCONFIRM: 'false' + GOTRUE_LOG_LEVEL: DEBUG + GOTRUE_OPERATOR_TOKEN: super-secret-operator-token + DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable' + GOTRUE_EXTERNAL_PHONE_ENABLED: 'false' + GOTRUE_EXTERNAL_EMAIL_ENABLED: 'false' + GOTRUE_SMTP_HOST: mail + GOTRUE_SMTP_PORT: 2500 + GOTRUE_SMTP_USER: GOTRUE_SMTP_USER + GOTRUE_SMTP_PASS: GOTRUE_SMTP_PASS + GOTRUE_SMTP_ADMIN_EMAIL: admin@email.com + GOTRUE_COOKIE_KEY: 'sb' + depends_on: + - db + restart: on-failure + mail: + image: phamhieu/inbucket:latest + ports: + - '2500:2500' # SMTP + - '9000:9000' # web interface + - '1100:1100' # POP3 + db: + image: supabase/postgres:15.1.1.46 + ports: + - '5432:5432' + command: postgres -c config_file=/etc/postgresql/postgresql.conf + volumes: + - ./db:/docker-entrypoint-initdb.d/ + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_PORT: 5432 diff --git a/src/auth/pyproject.toml b/src/auth/pyproject.toml new file mode 100644 index 00000000..cb932f32 --- /dev/null +++ b/src/auth/pyproject.toml @@ -0,0 +1,74 @@ +[project] +name = "supabase_auth" +version = "2.12.3" # {x-release-please-version} +description = "Python Client Library for Supabase Auth" +authors = [ + {name = "Joel Lee", email = "joel@joellee.org" } +] +readme = "README.md" +license = "MIT" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +requires-python = ">=3.9" +dependencies = [ + "httpx[http2] >=0.26,<0.29", + "pydantic >=1.10,<3", + "pyjwt[crypto] >=2.10.1", +] + +[project.urls] +homepage = "https://github.com/supabase/auth-py" +repository = "https://github.com/supabase/auth-py" +documentation = "https://github.com/supabase/auth-py" + +# [project.scripts] +# gh-download = "scripts.gh-download:main" + +[dependency-groups] +tests = [ + "Faker >= 37.4.0", + "respx >=0.20.2,<0.23.0", + "pytest >= 8.4.1", + "pytest-mock >= 3.14.0", + "pytest-cov >= 6.2.1", + "pytest-depends >= 1.0.1", + "pytest-asyncio >= 1.0.0", +] +lints = [ + "ruff >=0.12.1", + "unasync >= 0.6.0", +] +dev = [{ include-group = "lints" }, {include-group = "tests" }] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + # "B", + # flake8-simplify + # "SIM", + # isort + "I", +] +ignore = ["F401", "F403", "F841", "E712", "E501", "E402", "E722", "E731", "UP006", "UP035"] +# isort.required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true + + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[build-system] +requires = ["uv_build>=0.8.3,<0.9.0"] +build-backend = "uv_build" diff --git a/src/auth/scripts/gh-download.py b/src/auth/scripts/gh-download.py new file mode 100644 index 00000000..78934f3d --- /dev/null +++ b/src/auth/scripts/gh-download.py @@ -0,0 +1,127 @@ +# /// script +# dependencies = [ +# "pygithub >=1.57,<3.0", +# ] +# /// + +# 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(f"Processing {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") + 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, OSError, 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(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:]) diff --git a/src/auth/scripts/run-unasync.py b/src/auth/scripts/run-unasync.py new file mode 100644 index 00000000..aaf3052d --- /dev/null +++ b/src/auth/scripts/run-unasync.py @@ -0,0 +1,13 @@ +from pathlib import Path + +import unasync + +paths = Path("../src/supabase").glob("**/*.py") +tests = Path("tests").glob("**/*.py") + +rules = (unasync._DEFAULT_RULE,) + +files = [str(p) for p in list(paths) + list(tests)] + +if __name__ == "__main__": + unasync.unasync_files(files, rules=rules) diff --git a/src/auth/src/supabase_auth/__init__.py b/src/auth/src/supabase_auth/__init__.py new file mode 100644 index 00000000..159a72cf --- /dev/null +++ b/src/auth/src/supabase_auth/__init__.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +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 + 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 + SyncSupportedStorage, # type: ignore # noqa: F401 +) +from .types import * # type: ignore # noqa: F401, F403 +from .version import __version__ diff --git a/src/auth/src/supabase_auth/_async/__init__.py b/src/auth/src/supabase_auth/_async/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/src/auth/src/supabase_auth/_async/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/src/auth/src/supabase_auth/_async/gotrue_admin_api.py b/src/auth/src/supabase_auth/_async/gotrue_admin_api.py new file mode 100644 index 00000000..408f3da0 --- /dev/null +++ b/src/auth/src/supabase_auth/_async/gotrue_admin_api.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from functools import partial +from typing import Dict, List, Optional + +from ..helpers import ( + is_valid_uuid, + model_validate, + parse_link_response, + parse_user_response, +) +from ..http_clients import AsyncClient +from ..types import ( + AdminUserAttributes, + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, + GenerateLinkParams, + GenerateLinkResponse, + InviteUserByEmailOptions, + SignOutScope, + User, + UserResponse, +) +from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI +from .gotrue_base_api import AsyncGoTrueBaseAPI + + +class AsyncGoTrueAdminAPI(AsyncGoTrueBaseAPI): + def __init__( + self, + *, + url: str = "", + headers: Dict[str, str] = {}, + http_client: Optional[AsyncClient] = None, + verify: bool = True, + proxy: Optional[str] = None, + ) -> None: + AsyncGoTrueBaseAPI.__init__( + self, + url=url, + headers=headers, + http_client=http_client, + verify=verify, + proxy=proxy, + ) + self.mfa = AsyncGoTrueAdminMFAAPI() + self.mfa.list_factors = self._list_factors + self.mfa.delete_factor = self._delete_factor + + async def sign_out(self, jwt: str, scope: SignOutScope = "global") -> None: + """ + Removes a logged-in session. + """ + return await self._request( + "POST", + "logout", + query={"scope": scope}, + jwt=jwt, + no_resolve_json=True, + ) + + async def invite_user_by_email( + self, + email: str, + options: InviteUserByEmailOptions = {}, + ) -> 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, page: int = None, per_page: int = None) -> 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", + query={"page": page, "per_page": per_page}, + xform=lambda data: ( + [model_validate(User, user) for user in data["users"]] + if "users" in data + else [] + ), + ) + + 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. + """ + self._validate_uuid(uid) + + 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. + """ + self._validate_uuid(uid) + return await self._request( + "PUT", + f"admin/users/{uid}", + body=attributes, + xform=parse_user_response, + ) + + async def delete_user(self, id: str, should_soft_delete: bool = False) -> 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. + """ + self._validate_uuid(id) + body = {"should_soft_delete": should_soft_delete} + return await self._request("DELETE", f"admin/users/{id}", body=body) + + async def _list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + self._validate_uuid(params.get("user_id")) + return await self._request( + "GET", + f"admin/users/{params.get('user_id')}/factors", + xform=partial(model_validate, AuthMFAAdminListFactorsResponse), + ) + + async def _delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + self._validate_uuid(params.get("user_id")) + self._validate_uuid(params.get("id")) + return await self._request( + "DELETE", + f"admin/users/{params.get('user_id')}/factors/{params.get('id')}", + xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse), + ) + + def _validate_uuid(self, id: str) -> None: + if not is_valid_uuid(id): + raise ValueError(f"Invalid id, '{id}' is not a valid uuid") diff --git a/src/auth/src/supabase_auth/_async/gotrue_admin_mfa_api.py b/src/auth/src/supabase_auth/_async/gotrue_admin_mfa_api.py new file mode 100644 index 00000000..ca812fcd --- /dev/null +++ b/src/auth/src/supabase_auth/_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() # pragma: no cover + + 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() # pragma: no cover diff --git a/src/auth/src/supabase_auth/_async/gotrue_base_api.py b/src/auth/src/supabase_auth/_async/gotrue_base_api.py new file mode 100644 index 00000000..21d0b444 --- /dev/null +++ b/src/auth/src/supabase_auth/_async/gotrue_base_api.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from typing import Any, Callable, Dict, Optional, TypeVar, overload + +from httpx import Response +from pydantic import BaseModel +from typing_extensions import Literal, Self + +from ..constants import API_VERSION_HEADER_NAME, API_VERSIONS +from ..helpers import handle_exception, model_dump +from ..http_clients import AsyncClient + +T = TypeVar("T") + + +class AsyncGoTrueBaseAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + http_client: Optional[AsyncClient], + verify: bool = True, + proxy: Optional[str] = None, + ): + self._url = url + self._headers = headers + self._http_client = http_client or AsyncClient( + verify=bool(verify), + proxy=proxy, + follow_redirects=True, + http2=True, + ) + + 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: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: Literal[False] = False, + xform: Callable[[Any], T], + ) -> T: ... # pragma: no cover + + @overload + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: Literal[True], + xform: Callable[[Response], T], + ) -> T: ... # pragma: no cover + + @overload + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: bool = False, + ) -> None: ... # pragma: no cover + + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: bool = False, + xform: Optional[Callable[[Any], T]] = None, + ) -> Optional[T]: + url = f"{self._url}/{path}" + headers = {**self._headers, **(headers or {})} + if API_VERSION_HEADER_NAME not in headers: + headers[API_VERSION_HEADER_NAME] = API_VERSIONS["2024-01-01"].get("name") + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json;charset=UTF-8" + 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=model_dump(body) 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/src/auth/src/supabase_auth/_async/gotrue_client.py b/src/auth/src/supabase_auth/_async/gotrue_client.py new file mode 100644 index 00000000..25facea0 --- /dev/null +++ b/src/auth/src/supabase_auth/_async/gotrue_client.py @@ -0,0 +1,1256 @@ +from __future__ import annotations + +import time +from contextlib import suppress +from functools import partial +from json import loads +from typing import Callable, Dict, List, Optional, Tuple +from urllib.parse import parse_qs, urlencode, urlparse +from uuid import uuid4 + +from jwt import get_algorithm_by_name + +from ..constants import ( + DEFAULT_HEADERS, + EXPIRY_MARGIN, + GOTRUE_URL, + MAX_RETRIES, + RETRY_INTERVAL, + STORAGE_KEY, +) +from ..errors import ( + AuthApiError, + AuthImplicitGrantRedirectError, + AuthInvalidCredentialsError, + AuthInvalidJwtError, + AuthRetryableError, + AuthSessionMissingError, +) +from ..helpers import ( + decode_jwt, + generate_pkce_challenge, + generate_pkce_verifier, + model_dump, + model_dump_json, + model_validate, + parse_auth_otp_response, + parse_auth_response, + parse_jwks, + parse_link_identity_response, + parse_sso_response, + parse_user_response, + validate_exp, +) +from ..http_clients import AsyncClient +from ..timer import Timer +from ..types import ( + JWK, + AuthChangeEvent, + AuthenticatorAssuranceLevels, + AuthFlowType, + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + AuthOtpResponse, + AuthResponse, + ClaimsResponse, + CodeExchangeParams, + IdentitiesResponse, + JWKSet, + MFAChallengeAndVerifyParams, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, + OAuthResponse, + Options, + Provider, + ResendCredentials, + Session, + SignInAnonymouslyCredentials, + SignInWithIdTokenCredentials, + SignInWithOAuthCredentials, + SignInWithPasswordCredentials, + SignInWithPasswordlessCredentials, + SignInWithSSOCredentials, + SignOutOptions, + SignUpWithPasswordCredentials, + Subscription, + UpdateUserOptions, + UserAttributes, + UserIdentity, + UserResponse, + VerifyOtpParams, +) +from .gotrue_admin_api import AsyncGoTrueAdminAPI +from .gotrue_base_api import AsyncGoTrueBaseAPI +from .gotrue_mfa_api import AsyncGoTrueMFAAPI +from .storage import AsyncMemoryStorage, AsyncSupportedStorage + + +class AsyncGoTrueClient(AsyncGoTrueBaseAPI): + def __init__( + self, + *, + url: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + storage_key: Optional[str] = None, + auto_refresh_token: bool = True, + persist_session: bool = True, + storage: Optional[AsyncSupportedStorage] = None, + http_client: Optional[AsyncClient] = None, + flow_type: AuthFlowType = "implicit", + verify: bool = True, + proxy: Optional[str] = None, + ) -> None: + AsyncGoTrueBaseAPI.__init__( + self, + url=url or GOTRUE_URL, + headers=headers or DEFAULT_HEADERS, + http_client=http_client, + verify=verify, + proxy=proxy, + ) + + self._jwks: JWKSet = {"keys": []} + self._jwks_ttl: float = 600 # 10 minutes + self._jwks_cached_at: Optional[float] = None + + 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: Optional[Session] = None + self._refresh_token_timer: Optional[Timer] = None + self._network_retries = 0 + self._state_change_emitters: Dict[str, Subscription] = {} + self._flow_type = flow_type + + self.admin = AsyncGoTrueAdminAPI( + url=self._url, + headers=self._headers, + http_client=self._http_client, + ) + 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 + ) + self.mfa.list_factors = self._list_factors + self.mfa.unenroll = self._unenroll + self.mfa.verify = self._verify + + # Initializations + + async def initialize(self, *, url: Optional[str] = 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_in_anonymously( + self, credentials: Optional[SignInAnonymouslyCredentials] = None + ) -> AuthResponse: + """ + Creates a new anonymous user. + """ + await self._remove_session() + if credentials is None: + credentials = {"options": {}} + options = credentials.get("options", {}) + data = options.get("data") or {} + captcha_token = options.get("captcha_token") + response = await self._request( + "POST", + "signup", + body={ + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + 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 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") or options.get("email_redirect_to") + data = options.get("data") or {} + channel = options.get("channel", "sms") + 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, + "channel": channel, + "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", {}) + data = options.get("data") or {} + captcha_token = options.get("captcha_token") + if email: + response = await self._request( + "POST", + "token", + body={ + "email": email, + "password": password, + "data": data, + "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, + "data": data, + "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_id_token( + self, + credentials: SignInWithIdTokenCredentials, + ) -> AuthResponse: + """ + Allows signing in with an OIDC ID token. The authentication provider used should be enabled and configured. + """ + await self._remove_session() + provider = credentials.get("provider") + token = credentials.get("token") + access_token = credentials.get("access_token") + nonce = credentials.get("nonce") + options = credentials.get("options", {}) + captcha_token = options.get("captcha_token") + + response = await self._request( + "POST", + "token", + body={ + "provider": provider, + "id_token": token, + "access_token": access_token, + "nonce": nonce, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "id_token", + }, + 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 sign_in_with_sso(self, credentials: SignInWithSSOCredentials): + """ + Attempts a single-sign on using an enterprise Identity Provider. A + successful SSO attempt will redirect the current page to the identity + provider authorization page. The redirect URL is implementation and SSO + protocol specific. + + You can use it by providing a SSO domain. Typically you can extract this + domain by asking users for their email address. If this domain is + registered on the Auth instance the redirect will use that organization's + currently active SSO Identity Provider for the login. + If you have built an organization-specific login page, you can use the + organization's SSO Identity Provider UUID directly instead. + """ + await self._remove_session() + provider_id = credentials.get("provider_id") + domain = credentials.get("domain") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + captcha_token = options.get("captcha_token") + # HTTPX currently does not follow redirects: https://www.python-httpx.org/compatibility/ + # Additionally, unlike the JS client, Python is a server side language and it's not possible + # to automatically redirect in browser for hte user + skip_http_redirect = options.get("skip_http_redirect", True) + + if domain: + return await self._request( + "POST", + "sso", + body={ + "domain": domain, + "skip_http_redirect": skip_http_redirect, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + "redirect_to": redirect_to, + }, + xform=parse_sso_response, + ) + if provider_id: + return await self._request( + "POST", + "sso", + body={ + "provider_id": provider_id, + "skip_http_redirect": skip_http_redirect, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + "redirect_to": redirect_to, + }, + xform=parse_sso_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either a domain or provider_id" + ) + + 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_with_qs, _ = await self._get_url_for_provider( + f"{self._url}/authorize", provider, params + ) + return OAuthResponse(provider=provider, url=url_with_qs) + + async def link_identity( + self, credentials: SignInWithOAuthCredentials + ) -> OAuthResponse: + 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 + params["skip_http_redirect"] = "true" + url = "user/identities/authorize" + _, query = await self._get_url_for_provider(url, provider, params) + + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + + response = await self._request( + method="GET", + path=url, + query=query, + jwt=session.access_token, + xform=parse_link_identity_response, + ) + return OAuthResponse(provider=provider, url=response.url) + + async def get_user_identities(self): + response = await self.get_user() + return ( + IdentitiesResponse(identities=response.user.identities) + if response.user + else AuthSessionMissingError() + ) + + async def unlink_identity(self, identity: UserIdentity): + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + + return await self._request( + "DELETE", + f"user/identities/{identity.identity_id}", + jwt=session.access_token, + ) + + async def sign_in_with_otp( + self, + credentials: SignInWithPasswordlessCredentials, + ) -> AuthOtpResponse: + """ + 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("should_create_user", True) + data = options.get("data") + channel = options.get("channel", "sms") + 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_otp_response, + ) + if phone: + return await self._request( + "POST", + "otp", + body={ + "phone": phone, + "data": data, + "create_user": should_create_user, + "channel": channel, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_otp_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + async def resend( + self, + credentials: ResendCredentials, + ) -> AuthOtpResponse: + """ + Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP. + """ + email = credentials.get("email") + phone = credentials.get("phone") + type = credentials.get("type") + options = credentials.get("options", {}) + email_redirect_to = options.get("email_redirect_to") + captcha_token = options.get("captcha_token") + body = { + "type": type, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + } + + if email is None and phone is None: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + body.update({"email": email} if email else {"phone": phone}) + + return await self._request( + "POST", + "resend", + body=body, + redirect_to=email_redirect_to if email else None, + xform=parse_auth_otp_response, + ) + + 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 reauthenticate(self) -> AuthResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + + return await self._request( + "GET", + "reauthenticate", + jwt=session.access_token, + xform=parse_auth_response, + ) + + async def get_session(self) -> Optional[Session]: + """ + 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: Optional[Session] = 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 + time_now = round(time.time()) + has_expired = ( + current_session.expires_at <= time_now + EXPIRY_MARGIN + if current_session.expires_at + else False + ) + return ( + await self._call_refresh_token(current_session.refresh_token) + if has_expired + else current_session + ) + + async def get_user(self, jwt: Optional[str] = None) -> Optional[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 + else: + return None + return await self._request("GET", "user", jwt=jwt, xform=parse_user_response) + + async def update_user( + self, attributes: UserAttributes, options: UpdateUserOptions = {} + ) -> 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, + redirect_to=options.get("email_redirect_to"), + 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.time()) + expires_at = time_now + has_expired = True + session: Optional[Session] = None + if access_token and access_token.split(".")[1]: + payload = decode_jwt(access_token)["payload"] + exp = payload.get("exp") + if exp: + expires_at = int(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 refresh_session( + self, refresh_token: Optional[str] = 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, options: SignOutOptions = {"scope": "global"}) -> None: + """ + `sign_out` will remove the logged in user from the + current session and log them out - removing all items from storage and then trigger a `"SIGNED_OUT"` event. + + For advanced use cases, you can revoke all refresh tokens for a user by passing a user's JWT through to `admin.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. + """ + with suppress(AuthApiError): + session = await self.get_session() + access_token = session.access_token if session else None + if access_token: + await self.admin.sign_out(access_token, options["scope"]) + + if options["scope"] != "others": + await self._remove_session() + self._notify_all_subscribers("SIGNED_OUT", None) + + def on_auth_state_change( + self, + callback: Callable[[AuthChangeEvent, Optional[Session]], 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_for_email(self, email: str, options: Options = {}) -> None: + """ + Sends a password reset request to an email address. + """ + await self._request( + "POST", + "recover", + body={ + "email": email, + "gotrue_meta_security": { + "captcha_token": options.get("captcha_token"), + }, + }, + redirect_to=options.get("redirect_to"), + ) + + async def reset_password_email( + self, + email: str, + options: Options = {}, + ) -> None: + """ + Sends a password reset request to an email address. + """ + await self.reset_password_for_email(email, options) + + # MFA methods + + async def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + session = await self.get_session() + if not session: + raise AuthSessionMissingError() + + body = { + "friendly_name": params.get("friendly_name"), + "factor_type": params.get("factor_type"), + } + + if params["factor_type"] == "phone": + body["phone"] = params.get("phone") + else: + body["issuer"] = params.get("issuer") + + response = await self._request( + "POST", + "factors", + body=body, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAEnrollResponse), + ) + if params["factor_type"] == "totp" and 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", + body={"channel": params.get("channel")}, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAChallengeResponse), + ) + + 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: + raise AuthSessionMissingError() + response = await self._request( + "POST", + f"factors/{params.get('factor_id')}/verify", + body=params, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAVerifyResponse), + ) + session = model_validate(Session, model_dump(response)) + 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=partial(model_validate, AuthMFAUnenrollResponse), + ) + + 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"] + phone = [f for f in all if f.factor_type == "phone" and f.status == "verified"] + return AuthMFAListFactorsResponse(all=all, totp=totp, phone=phone) + + 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 = decode_jwt(session.access_token)["payload"] + current_level: Optional[AuthenticatorAssuranceLevels] = None + if payload.get("aal"): + current_level = payload.get("aal") + verified_factors = [ + f for f in session.user.factors or [] if f.status == "verified" + ] + next_level = "aal2" if verified_factors else current_level + 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: + 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, Optional[str]]: + 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.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.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: + time_now = round(time.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, model_dump_json(session)) + + 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: Optional[Session], + ) -> None: + for subscription in self._state_change_emitters.values(): + subscription.callback(event, session) + + def _get_valid_session( + self, + raw_session: Optional[str], + ) -> Optional[Session]: + 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: + return model_validate(Session, data) + except Exception: + return None + + def _get_param( + self, + query_params: Dict[str, List[str]], + name: str, + ) -> Optional[str]: + return query_params[name][0] if name in query_params else 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 + + async def _get_url_for_provider( + self, + url: str, + provider: Provider, + params: Dict[str, str], + ) -> Tuple[str, Dict[str, str]]: + if self._flow_type == "pkce": + code_verifier = generate_pkce_verifier() + code_challenge = generate_pkce_challenge(code_verifier) + await self._storage.set_item( + f"{self._storage_key}-code-verifier", code_verifier + ) + code_challenge_method = ( + "plain" if code_verifier == code_challenge else "s256" + ) + params["code_challenge"] = code_challenge + params["code_challenge_method"] = code_challenge_method + + params["provider"] = provider + query = urlencode(params) + return f"{url}?{query}", params + + async def exchange_code_for_session(self, params: CodeExchangeParams): + code_verifier = params.get("code_verifier") or await self._storage.get_item( + f"{self._storage_key}-code-verifier" + ) + response = await self._request( + "POST", + "token", + query={"grant_type": "pkce"}, + body={ + "auth_code": params.get("auth_code"), + "code_verifier": code_verifier, + }, + redirect_to=params.get("redirect_to"), + xform=parse_auth_response, + ) + await self._storage.remove_item(f"{self._storage_key}-code-verifier") + if response.session: + await self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + async def _fetch_jwks(self, kid: str, jwks: JWKSet) -> JWK: + jwk: Optional[JWK] = None + + # try fetching from the suplied keys. + jwk = next((jwk for jwk in jwks["keys"] if jwk["kid"] == kid), None) + + if jwk: + return jwk + + if self._jwks and ( + self._jwks_cached_at and time.time() - self._jwks_cached_at < self._jwks_ttl + ): + # try fetching from the cache. + jwk = next( + (jwk for jwk in self._jwks["keys"] if jwk["kid"] == kid), + None, + ) + if jwk: + return jwk + + # jwk isn't cached in memory so we need to fetch it from the well-known endpoint + response = await self._request("GET", ".well-known/jwks.json", xform=parse_jwks) + if response: + self._jwks = response + self._jwks_cached_at = time.time() + + # find the signing key + jwk = next((jwk for jwk in response["keys"] if jwk["kid"] == kid), None) + if not jwk: + raise AuthInvalidJwtError("No matching signing key found in JWKS") + + return jwk + + raise AuthInvalidJwtError("JWT has no valid kid") + + async def get_claims( + self, jwt: Optional[str] = None, jwks: Optional[JWKSet] = None + ) -> Optional[ClaimsResponse]: + token = jwt + if not token: + session = await self.get_session() + if not session: + return None + + token = session.access_token + + decoded_jwt = decode_jwt(token) + + payload, header, signature = ( + decoded_jwt["payload"], + decoded_jwt["header"], + decoded_jwt["signature"], + ) + raw_header, raw_payload = ( + decoded_jwt["raw"]["header"], + decoded_jwt["raw"]["payload"], + ) + + validate_exp(payload["exp"]) + + # if symmetric algorithm, fallback to get_user + if "kid" not in header or header["alg"] == "HS256": + await self.get_user(token) + return ClaimsResponse(claims=payload, headers=header, signature=signature) + + algorithm = get_algorithm_by_name(header["alg"]) + signing_key = algorithm.from_jwk( + await self._fetch_jwks(header["kid"], jwks or {"keys": []}) + ) + + # verify the signature + is_valid = algorithm.verify( + msg=f"{raw_header}.{raw_payload}".encode(), key=signing_key, sig=signature + ) + + if not is_valid: + raise AuthInvalidJwtError("Invalid JWT signature") + + # If verification succeeds, decode and return claims + return ClaimsResponse(claims=payload, headers=header, signature=signature) + + def __del__(self) -> None: + """Clean up resources when the client is destroyed.""" + if self._refresh_token_timer: + try: + # Try to cancel the timer + self._refresh_token_timer.cancel() + except: + # Ignore errors if event loop is closed or selector is not registered + pass + finally: + # Always set to None to prevent further attempts + self._refresh_token_timer = None diff --git a/src/auth/src/supabase_auth/_async/gotrue_mfa_api.py b/src/auth/src/supabase_auth/_async/gotrue_mfa_api.py new file mode 100644 index 00000000..a30c4c73 --- /dev/null +++ b/src/auth/src/supabase_auth/_async/gotrue_mfa_api.py @@ -0,0 +1,94 @@ +from ..types import ( + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + MFAChallengeAndVerifyParams, + 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() # 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() # pragma: no cover + + 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() # 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() # pragma: no cover + + 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() # pragma: no cover + + 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() # pragma: no cover + + 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() # pragma: no cover diff --git a/src/auth/src/supabase_auth/_async/storage.py b/src/auth/src/supabase_auth/_async/storage.py new file mode 100644 index 00000000..5239dd9d --- /dev/null +++ b/src/auth/src/supabase_auth/_async/storage.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Dict, Optional + + +class AsyncSupportedStorage(ABC): + @abstractmethod + async def get_item(self, key: str) -> Optional[str]: ... # pragma: no cover + + @abstractmethod + async def set_item(self, key: str, value: str) -> None: ... # pragma: no cover + + @abstractmethod + async def remove_item(self, key: str) -> None: ... # pragma: no cover + + +class AsyncMemoryStorage(AsyncSupportedStorage): + def __init__(self): + self.storage: Dict[str, str] = {} + + async def get_item(self, key: str) -> Optional[str]: + if key in self.storage: + return self.storage[key] + + async def set_item(self, key: str, value: str) -> None: + self.storage[key] = value + + async def remove_item(self, key: str) -> None: + if key in self.storage: + del self.storage[key] diff --git a/src/auth/src/supabase_auth/_sync/__init__.py b/src/auth/src/supabase_auth/_sync/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py b/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py new file mode 100644 index 00000000..afbb75e0 --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/gotrue_admin_api.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from functools import partial +from typing import Dict, List, Optional + +from ..helpers import ( + is_valid_uuid, + model_validate, + parse_link_response, + parse_user_response, +) +from ..http_clients import SyncClient +from ..types import ( + AdminUserAttributes, + AuthMFAAdminDeleteFactorParams, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsParams, + AuthMFAAdminListFactorsResponse, + GenerateLinkParams, + GenerateLinkResponse, + InviteUserByEmailOptions, + SignOutScope, + User, + UserResponse, +) +from .gotrue_admin_mfa_api import SyncGoTrueAdminMFAAPI +from .gotrue_base_api import SyncGoTrueBaseAPI + + +class SyncGoTrueAdminAPI(SyncGoTrueBaseAPI): + def __init__( + self, + *, + url: str = "", + headers: Dict[str, str] = {}, + http_client: Optional[SyncClient] = None, + verify: bool = True, + proxy: Optional[str] = None, + ) -> None: + SyncGoTrueBaseAPI.__init__( + self, + url=url, + headers=headers, + http_client=http_client, + verify=verify, + proxy=proxy, + ) + self.mfa = SyncGoTrueAdminMFAAPI() + self.mfa.list_factors = self._list_factors + self.mfa.delete_factor = self._delete_factor + + def sign_out(self, jwt: str, scope: SignOutScope = "global") -> None: + """ + Removes a logged-in session. + """ + return self._request( + "POST", + "logout", + query={"scope": scope}, + jwt=jwt, + no_resolve_json=True, + ) + + def invite_user_by_email( + self, + email: str, + options: InviteUserByEmailOptions = {}, + ) -> 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, page: int = None, per_page: int = None) -> 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", + query={"page": page, "per_page": per_page}, + xform=lambda data: ( + [model_validate(User, user) for user in data["users"]] + if "users" in data + else [] + ), + ) + + 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. + """ + self._validate_uuid(uid) + + 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. + """ + self._validate_uuid(uid) + return self._request( + "PUT", + f"admin/users/{uid}", + body=attributes, + xform=parse_user_response, + ) + + def delete_user(self, id: str, should_soft_delete: bool = False) -> 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. + """ + self._validate_uuid(id) + body = {"should_soft_delete": should_soft_delete} + return self._request("DELETE", f"admin/users/{id}", body=body) + + def _list_factors( + self, + params: AuthMFAAdminListFactorsParams, + ) -> AuthMFAAdminListFactorsResponse: + self._validate_uuid(params.get("user_id")) + return self._request( + "GET", + f"admin/users/{params.get('user_id')}/factors", + xform=partial(model_validate, AuthMFAAdminListFactorsResponse), + ) + + def _delete_factor( + self, + params: AuthMFAAdminDeleteFactorParams, + ) -> AuthMFAAdminDeleteFactorResponse: + self._validate_uuid(params.get("user_id")) + self._validate_uuid(params.get("id")) + return self._request( + "DELETE", + f"admin/users/{params.get('user_id')}/factors/{params.get('id')}", + xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse), + ) + + def _validate_uuid(self, id: str) -> None: + if not is_valid_uuid(id): + raise ValueError(f"Invalid id, '{id}' is not a valid uuid") diff --git a/src/auth/src/supabase_auth/_sync/gotrue_admin_mfa_api.py b/src/auth/src/supabase_auth/_sync/gotrue_admin_mfa_api.py new file mode 100644 index 00000000..c3fcfc8e --- /dev/null +++ b/src/auth/src/supabase_auth/_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() # pragma: no cover + + 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() # pragma: no cover diff --git a/src/auth/src/supabase_auth/_sync/gotrue_base_api.py b/src/auth/src/supabase_auth/_sync/gotrue_base_api.py new file mode 100644 index 00000000..c6c2b7b0 --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/gotrue_base_api.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from typing import Any, Callable, Dict, Optional, TypeVar, overload + +from httpx import Response +from pydantic import BaseModel +from typing_extensions import Literal, Self + +from ..constants import API_VERSION_HEADER_NAME, API_VERSIONS +from ..helpers import handle_exception, model_dump +from ..http_clients import SyncClient + +T = TypeVar("T") + + +class SyncGoTrueBaseAPI: + def __init__( + self, + *, + url: str, + headers: Dict[str, str], + http_client: Optional[SyncClient], + verify: bool = True, + proxy: Optional[str] = None, + ): + self._url = url + self._headers = headers + self._http_client = http_client or SyncClient( + verify=bool(verify), + proxy=proxy, + follow_redirects=True, + http2=True, + ) + + 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: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: Literal[False] = False, + xform: Callable[[Any], T], + ) -> T: ... # pragma: no cover + + @overload + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: Literal[True], + xform: Callable[[Response], T], + ) -> T: ... # pragma: no cover + + @overload + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: bool = False, + ) -> None: ... # pragma: no cover + + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + path: str, + *, + jwt: Optional[str] = None, + redirect_to: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + query: Optional[Dict[str, str]] = None, + body: Optional[Any] = None, + no_resolve_json: bool = False, + xform: Optional[Callable[[Any], T]] = None, + ) -> Optional[T]: + url = f"{self._url}/{path}" + headers = {**self._headers, **(headers or {})} + if API_VERSION_HEADER_NAME not in headers: + headers[API_VERSION_HEADER_NAME] = API_VERSIONS["2024-01-01"].get("name") + if "Content-Type" not in headers: + headers["Content-Type"] = "application/json;charset=UTF-8" + 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=model_dump(body) 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/src/auth/src/supabase_auth/_sync/gotrue_client.py b/src/auth/src/supabase_auth/_sync/gotrue_client.py new file mode 100644 index 00000000..a575f57b --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/gotrue_client.py @@ -0,0 +1,1250 @@ +from __future__ import annotations + +import time +from contextlib import suppress +from functools import partial +from json import loads +from typing import Callable, Dict, List, Optional, Tuple +from urllib.parse import parse_qs, urlencode, urlparse +from uuid import uuid4 + +from jwt import get_algorithm_by_name + +from ..constants import ( + DEFAULT_HEADERS, + EXPIRY_MARGIN, + GOTRUE_URL, + MAX_RETRIES, + RETRY_INTERVAL, + STORAGE_KEY, +) +from ..errors import ( + AuthApiError, + AuthImplicitGrantRedirectError, + AuthInvalidCredentialsError, + AuthInvalidJwtError, + AuthRetryableError, + AuthSessionMissingError, +) +from ..helpers import ( + decode_jwt, + generate_pkce_challenge, + generate_pkce_verifier, + model_dump, + model_dump_json, + model_validate, + parse_auth_otp_response, + parse_auth_response, + parse_jwks, + parse_link_identity_response, + parse_sso_response, + parse_user_response, + validate_exp, +) +from ..http_clients import SyncClient +from ..timer import Timer +from ..types import ( + JWK, + AuthChangeEvent, + AuthenticatorAssuranceLevels, + AuthFlowType, + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + AuthOtpResponse, + AuthResponse, + ClaimsResponse, + CodeExchangeParams, + IdentitiesResponse, + JWKSet, + MFAChallengeAndVerifyParams, + MFAChallengeParams, + MFAEnrollParams, + MFAUnenrollParams, + MFAVerifyParams, + OAuthResponse, + Options, + Provider, + ResendCredentials, + Session, + SignInAnonymouslyCredentials, + SignInWithIdTokenCredentials, + SignInWithOAuthCredentials, + SignInWithPasswordCredentials, + SignInWithPasswordlessCredentials, + SignInWithSSOCredentials, + SignOutOptions, + SignUpWithPasswordCredentials, + Subscription, + UpdateUserOptions, + UserAttributes, + UserIdentity, + UserResponse, + VerifyOtpParams, +) +from .gotrue_admin_api import SyncGoTrueAdminAPI +from .gotrue_base_api import SyncGoTrueBaseAPI +from .gotrue_mfa_api import SyncGoTrueMFAAPI +from .storage import SyncMemoryStorage, SyncSupportedStorage + + +class SyncGoTrueClient(SyncGoTrueBaseAPI): + def __init__( + self, + *, + url: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + storage_key: Optional[str] = None, + auto_refresh_token: bool = True, + persist_session: bool = True, + storage: Optional[SyncSupportedStorage] = None, + http_client: Optional[SyncClient] = None, + flow_type: AuthFlowType = "implicit", + verify: bool = True, + proxy: Optional[str] = None, + ) -> None: + SyncGoTrueBaseAPI.__init__( + self, + url=url or GOTRUE_URL, + headers=headers or DEFAULT_HEADERS, + http_client=http_client, + verify=verify, + proxy=proxy, + ) + + self._jwks: JWKSet = {"keys": []} + self._jwks_ttl: float = 600 # 10 minutes + self._jwks_cached_at: Optional[float] = None + + 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: Optional[Session] = None + self._refresh_token_timer: Optional[Timer] = None + self._network_retries = 0 + self._state_change_emitters: Dict[str, Subscription] = {} + self._flow_type = flow_type + + self.admin = SyncGoTrueAdminAPI( + url=self._url, + headers=self._headers, + http_client=self._http_client, + ) + 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 + ) + self.mfa.list_factors = self._list_factors + self.mfa.unenroll = self._unenroll + self.mfa.verify = self._verify + + # Initializations + + def initialize(self, *, url: Optional[str] = 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_in_anonymously( + self, credentials: Optional[SignInAnonymouslyCredentials] = None + ) -> AuthResponse: + """ + Creates a new anonymous user. + """ + self._remove_session() + if credentials is None: + credentials = {"options": {}} + options = credentials.get("options", {}) + data = options.get("data") or {} + captcha_token = options.get("captcha_token") + response = self._request( + "POST", + "signup", + body={ + "data": data, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_response, + ) + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + 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") or options.get("email_redirect_to") + data = options.get("data") or {} + channel = options.get("channel", "sms") + 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, + "channel": channel, + "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", {}) + data = options.get("data") or {} + captcha_token = options.get("captcha_token") + if email: + response = self._request( + "POST", + "token", + body={ + "email": email, + "password": password, + "data": data, + "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, + "data": data, + "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_id_token( + self, + credentials: SignInWithIdTokenCredentials, + ) -> AuthResponse: + """ + Allows signing in with an OIDC ID token. The authentication provider used should be enabled and configured. + """ + self._remove_session() + provider = credentials.get("provider") + token = credentials.get("token") + access_token = credentials.get("access_token") + nonce = credentials.get("nonce") + options = credentials.get("options", {}) + captcha_token = options.get("captcha_token") + + response = self._request( + "POST", + "token", + body={ + "provider": provider, + "id_token": token, + "access_token": access_token, + "nonce": nonce, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + query={ + "grant_type": "id_token", + }, + xform=parse_auth_response, + ) + + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + def sign_in_with_sso(self, credentials: SignInWithSSOCredentials): + """ + Attempts a single-sign on using an enterprise Identity Provider. A + successful SSO attempt will redirect the current page to the identity + provider authorization page. The redirect URL is implementation and SSO + protocol specific. + + You can use it by providing a SSO domain. Typically you can extract this + domain by asking users for their email address. If this domain is + registered on the Auth instance the redirect will use that organization's + currently active SSO Identity Provider for the login. + If you have built an organization-specific login page, you can use the + organization's SSO Identity Provider UUID directly instead. + """ + self._remove_session() + provider_id = credentials.get("provider_id") + domain = credentials.get("domain") + options = credentials.get("options", {}) + redirect_to = options.get("redirect_to") + captcha_token = options.get("captcha_token") + # HTTPX currently does not follow redirects: https://www.python-httpx.org/compatibility/ + # Additionally, unlike the JS client, Python is a server side language and it's not possible + # to automatically redirect in browser for hte user + skip_http_redirect = options.get("skip_http_redirect", True) + + if domain: + return self._request( + "POST", + "sso", + body={ + "domain": domain, + "skip_http_redirect": skip_http_redirect, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + "redirect_to": redirect_to, + }, + xform=parse_sso_response, + ) + if provider_id: + return self._request( + "POST", + "sso", + body={ + "provider_id": provider_id, + "skip_http_redirect": skip_http_redirect, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + "redirect_to": redirect_to, + }, + xform=parse_sso_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either a domain or provider_id" + ) + + 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_with_qs, _ = self._get_url_for_provider( + f"{self._url}/authorize", provider, params + ) + return OAuthResponse(provider=provider, url=url_with_qs) + + def link_identity(self, credentials: SignInWithOAuthCredentials) -> OAuthResponse: + 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 + params["skip_http_redirect"] = "true" + url = "user/identities/authorize" + _, query = self._get_url_for_provider(url, provider, params) + + session = self.get_session() + if not session: + raise AuthSessionMissingError() + + response = self._request( + method="GET", + path=url, + query=query, + jwt=session.access_token, + xform=parse_link_identity_response, + ) + return OAuthResponse(provider=provider, url=response.url) + + def get_user_identities(self): + response = self.get_user() + return ( + IdentitiesResponse(identities=response.user.identities) + if response.user + else AuthSessionMissingError() + ) + + def unlink_identity(self, identity: UserIdentity): + session = self.get_session() + if not session: + raise AuthSessionMissingError() + + return self._request( + "DELETE", + f"user/identities/{identity.identity_id}", + jwt=session.access_token, + ) + + def sign_in_with_otp( + self, + credentials: SignInWithPasswordlessCredentials, + ) -> AuthOtpResponse: + """ + 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("should_create_user", True) + data = options.get("data") + channel = options.get("channel", "sms") + 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_otp_response, + ) + if phone: + return self._request( + "POST", + "otp", + body={ + "phone": phone, + "data": data, + "create_user": should_create_user, + "channel": channel, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + }, + xform=parse_auth_otp_response, + ) + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + def resend( + self, + credentials: ResendCredentials, + ) -> AuthOtpResponse: + """ + Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP. + """ + email = credentials.get("email") + phone = credentials.get("phone") + type = credentials.get("type") + options = credentials.get("options", {}) + email_redirect_to = options.get("email_redirect_to") + captcha_token = options.get("captcha_token") + body = { + "type": type, + "gotrue_meta_security": { + "captcha_token": captcha_token, + }, + } + + if email is None and phone is None: + raise AuthInvalidCredentialsError( + "You must provide either an email or phone number" + ) + + body.update({"email": email} if email else {"phone": phone}) + + return self._request( + "POST", + "resend", + body=body, + redirect_to=email_redirect_to if email else None, + xform=parse_auth_otp_response, + ) + + 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 reauthenticate(self) -> AuthResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + + return self._request( + "GET", + "reauthenticate", + jwt=session.access_token, + xform=parse_auth_response, + ) + + def get_session(self) -> Optional[Session]: + """ + 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: Optional[Session] = 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 + time_now = round(time.time()) + has_expired = ( + current_session.expires_at <= time_now + EXPIRY_MARGIN + if current_session.expires_at + else False + ) + return ( + self._call_refresh_token(current_session.refresh_token) + if has_expired + else current_session + ) + + def get_user(self, jwt: Optional[str] = None) -> Optional[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 + else: + return None + return self._request("GET", "user", jwt=jwt, xform=parse_user_response) + + def update_user( + self, attributes: UserAttributes, options: UpdateUserOptions = {} + ) -> 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, + redirect_to=options.get("email_redirect_to"), + 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.time()) + expires_at = time_now + has_expired = True + session: Optional[Session] = None + if access_token and access_token.split(".")[1]: + payload = decode_jwt(access_token)["payload"] + exp = payload.get("exp") + if exp: + expires_at = int(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 refresh_session(self, refresh_token: Optional[str] = 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, options: SignOutOptions = {"scope": "global"}) -> None: + """ + `sign_out` will remove the logged in user from the + current session and log them out - removing all items from storage and then trigger a `"SIGNED_OUT"` event. + + For advanced use cases, you can revoke all refresh tokens for a user by passing a user's JWT through to `admin.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. + """ + with suppress(AuthApiError): + session = self.get_session() + access_token = session.access_token if session else None + if access_token: + self.admin.sign_out(access_token, options["scope"]) + + if options["scope"] != "others": + self._remove_session() + self._notify_all_subscribers("SIGNED_OUT", None) + + def on_auth_state_change( + self, + callback: Callable[[AuthChangeEvent, Optional[Session]], 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_for_email(self, email: str, options: Options = {}) -> None: + """ + Sends a password reset request to an email address. + """ + self._request( + "POST", + "recover", + body={ + "email": email, + "gotrue_meta_security": { + "captcha_token": options.get("captcha_token"), + }, + }, + redirect_to=options.get("redirect_to"), + ) + + def reset_password_email( + self, + email: str, + options: Options = {}, + ) -> None: + """ + Sends a password reset request to an email address. + """ + self.reset_password_for_email(email, options) + + # MFA methods + + def _enroll(self, params: MFAEnrollParams) -> AuthMFAEnrollResponse: + session = self.get_session() + if not session: + raise AuthSessionMissingError() + + body = { + "friendly_name": params.get("friendly_name"), + "factor_type": params.get("factor_type"), + } + + if params["factor_type"] == "phone": + body["phone"] = params.get("phone") + else: + body["issuer"] = params.get("issuer") + + response = self._request( + "POST", + "factors", + body=body, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAEnrollResponse), + ) + if params["factor_type"] == "totp" and 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", + body={"channel": params.get("channel")}, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAChallengeResponse), + ) + + 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: + raise AuthSessionMissingError() + response = self._request( + "POST", + f"factors/{params.get('factor_id')}/verify", + body=params, + jwt=session.access_token, + xform=partial(model_validate, AuthMFAVerifyResponse), + ) + session = model_validate(Session, model_dump(response)) + 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=partial(model_validate, AuthMFAUnenrollResponse), + ) + + 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"] + phone = [f for f in all if f.factor_type == "phone" and f.status == "verified"] + return AuthMFAListFactorsResponse(all=all, totp=totp, phone=phone) + + 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 = decode_jwt(session.access_token)["payload"] + current_level: Optional[AuthenticatorAssuranceLevels] = None + if payload.get("aal"): + current_level = payload.get("aal") + verified_factors = [ + f for f in session.user.factors or [] if f.status == "verified" + ] + next_level = "aal2" if verified_factors else current_level + 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: + 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, Optional[str]]: + 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.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.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: + time_now = round(time.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, model_dump_json(session)) + + 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: Optional[Session], + ) -> None: + for subscription in self._state_change_emitters.values(): + subscription.callback(event, session) + + def _get_valid_session( + self, + raw_session: Optional[str], + ) -> Optional[Session]: + 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: + return model_validate(Session, data) + except Exception: + return None + + def _get_param( + self, + query_params: Dict[str, List[str]], + name: str, + ) -> Optional[str]: + return query_params[name][0] if name in query_params else 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, + url: str, + provider: Provider, + params: Dict[str, str], + ) -> Tuple[str, Dict[str, str]]: + if self._flow_type == "pkce": + code_verifier = generate_pkce_verifier() + code_challenge = generate_pkce_challenge(code_verifier) + self._storage.set_item(f"{self._storage_key}-code-verifier", code_verifier) + code_challenge_method = ( + "plain" if code_verifier == code_challenge else "s256" + ) + params["code_challenge"] = code_challenge + params["code_challenge_method"] = code_challenge_method + + params["provider"] = provider + query = urlencode(params) + return f"{url}?{query}", params + + def exchange_code_for_session(self, params: CodeExchangeParams): + code_verifier = params.get("code_verifier") or self._storage.get_item( + f"{self._storage_key}-code-verifier" + ) + response = self._request( + "POST", + "token", + query={"grant_type": "pkce"}, + body={ + "auth_code": params.get("auth_code"), + "code_verifier": code_verifier, + }, + redirect_to=params.get("redirect_to"), + xform=parse_auth_response, + ) + self._storage.remove_item(f"{self._storage_key}-code-verifier") + if response.session: + self._save_session(response.session) + self._notify_all_subscribers("SIGNED_IN", response.session) + return response + + def _fetch_jwks(self, kid: str, jwks: JWKSet) -> JWK: + jwk: Optional[JWK] = None + + # try fetching from the suplied keys. + jwk = next((jwk for jwk in jwks["keys"] if jwk["kid"] == kid), None) + + if jwk: + return jwk + + if self._jwks and ( + self._jwks_cached_at and time.time() - self._jwks_cached_at < self._jwks_ttl + ): + # try fetching from the cache. + jwk = next( + (jwk for jwk in self._jwks["keys"] if jwk["kid"] == kid), + None, + ) + if jwk: + return jwk + + # jwk isn't cached in memory so we need to fetch it from the well-known endpoint + response = self._request("GET", ".well-known/jwks.json", xform=parse_jwks) + if response: + self._jwks = response + self._jwks_cached_at = time.time() + + # find the signing key + jwk = next((jwk for jwk in response["keys"] if jwk["kid"] == kid), None) + if not jwk: + raise AuthInvalidJwtError("No matching signing key found in JWKS") + + return jwk + + raise AuthInvalidJwtError("JWT has no valid kid") + + def get_claims( + self, jwt: Optional[str] = None, jwks: Optional[JWKSet] = None + ) -> Optional[ClaimsResponse]: + token = jwt + if not token: + session = self.get_session() + if not session: + return None + + token = session.access_token + + decoded_jwt = decode_jwt(token) + + payload, header, signature = ( + decoded_jwt["payload"], + decoded_jwt["header"], + decoded_jwt["signature"], + ) + raw_header, raw_payload = ( + decoded_jwt["raw"]["header"], + decoded_jwt["raw"]["payload"], + ) + + validate_exp(payload["exp"]) + + # if symmetric algorithm, fallback to get_user + if "kid" not in header or header["alg"] == "HS256": + self.get_user(token) + return ClaimsResponse(claims=payload, headers=header, signature=signature) + + algorithm = get_algorithm_by_name(header["alg"]) + signing_key = algorithm.from_jwk( + self._fetch_jwks(header["kid"], jwks or {"keys": []}) + ) + + # verify the signature + is_valid = algorithm.verify( + msg=f"{raw_header}.{raw_payload}".encode(), key=signing_key, sig=signature + ) + + if not is_valid: + raise AuthInvalidJwtError("Invalid JWT signature") + + # If verification succeeds, decode and return claims + return ClaimsResponse(claims=payload, headers=header, signature=signature) + + def __del__(self) -> None: + """Clean up resources when the client is destroyed.""" + if self._refresh_token_timer: + try: + # Try to cancel the timer + self._refresh_token_timer.cancel() + except: + # Ignore errors if event loop is closed or selector is not registered + pass + finally: + # Always set to None to prevent further attempts + self._refresh_token_timer = None diff --git a/src/auth/src/supabase_auth/_sync/gotrue_mfa_api.py b/src/auth/src/supabase_auth/_sync/gotrue_mfa_api.py new file mode 100644 index 00000000..16bec8d5 --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/gotrue_mfa_api.py @@ -0,0 +1,94 @@ +from ..types import ( + AuthMFAChallengeResponse, + AuthMFAEnrollResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAListFactorsResponse, + AuthMFAUnenrollResponse, + AuthMFAVerifyResponse, + MFAChallengeAndVerifyParams, + 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() # 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() # pragma: no cover + + 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() # 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() # pragma: no cover + + 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() # pragma: no cover + + 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() # pragma: no cover + + 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() # pragma: no cover diff --git a/src/auth/src/supabase_auth/_sync/storage.py b/src/auth/src/supabase_auth/_sync/storage.py new file mode 100644 index 00000000..03ede0c1 --- /dev/null +++ b/src/auth/src/supabase_auth/_sync/storage.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Dict, Optional + + +class SyncSupportedStorage(ABC): + @abstractmethod + def get_item(self, key: str) -> Optional[str]: ... # pragma: no cover + + @abstractmethod + def set_item(self, key: str, value: str) -> None: ... # pragma: no cover + + @abstractmethod + def remove_item(self, key: str) -> None: ... # pragma: no cover + + +class SyncMemoryStorage(SyncSupportedStorage): + def __init__(self): + self.storage: Dict[str, str] = {} + + def get_item(self, key: str) -> Optional[str]: + if key in self.storage: + return self.storage[key] + + def set_item(self, key: str, value: str) -> None: + self.storage[key] = value + + def remove_item(self, key: str) -> None: + if key in self.storage: + del self.storage[key] diff --git a/src/auth/src/supabase_auth/constants.py b/src/auth/src/supabase_auth/constants.py new file mode 100644 index 00000000..671510e5 --- /dev/null +++ b/src/auth/src/supabase_auth/constants.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Dict + +from .version import __version__ + +GOTRUE_URL = "http://localhost:9999" +DEFAULT_HEADERS: Dict[str, str] = { + "X-Client-Info": f"gotrue-py/{__version__}", +} +EXPIRY_MARGIN = 10 # seconds +MAX_RETRIES = 10 +RETRY_INTERVAL = 2 # deciseconds +STORAGE_KEY = "supabase.auth.token" + +API_VERSION_HEADER_NAME = "X-Supabase-Api-Version" +API_VERSIONS = { + "2024-01-01": { + "timestamp": datetime.timestamp(datetime.strptime("2024-01-01", "%Y-%m-%d")), + "name": "2024-01-01", + }, +} +BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" diff --git a/src/auth/src/supabase_auth/errors.py b/src/auth/src/supabase_auth/errors.py new file mode 100644 index 00000000..cc85b87e --- /dev/null +++ b/src/auth/src/supabase_auth/errors.py @@ -0,0 +1,238 @@ +from __future__ import annotations + +from typing import List, Literal, Optional + +from typing_extensions import TypedDict + +ErrorCode = Literal[ + "unexpected_failure", + "validation_failed", + "bad_json", + "email_exists", + "phone_exists", + "bad_jwt", + "not_admin", + "no_authorization", + "user_not_found", + "session_not_found", + "flow_state_not_found", + "flow_state_expired", + "signup_disabled", + "user_banned", + "provider_email_needs_verification", + "invite_not_found", + "bad_oauth_state", + "bad_oauth_callback", + "oauth_provider_not_supported", + "unexpected_audience", + "single_identity_not_deletable", + "email_conflict_identity_not_deletable", + "identity_already_exists", + "email_provider_disabled", + "phone_provider_disabled", + "too_many_enrolled_mfa_factors", + "mfa_factor_name_conflict", + "mfa_factor_not_found", + "mfa_ip_address_mismatch", + "mfa_challenge_expired", + "mfa_verification_failed", + "mfa_verification_rejected", + "insufficient_aal", + "captcha_failed", + "saml_provider_disabled", + "manual_linking_disabled", + "sms_send_failed", + "email_not_confirmed", + "phone_not_confirmed", + "reauth_nonce_missing", + "saml_relay_state_not_found", + "saml_relay_state_expired", + "saml_idp_not_found", + "saml_assertion_no_user_id", + "saml_assertion_no_email", + "user_already_exists", + "sso_provider_not_found", + "saml_metadata_fetch_failed", + "saml_idp_already_exists", + "sso_domain_already_exists", + "saml_entity_id_mismatch", + "conflict", + "provider_disabled", + "user_sso_managed", + "reauthentication_needed", + "same_password", + "reauthentication_not_valid", + "otp_expired", + "otp_disabled", + "identity_not_found", + "weak_password", + "over_request_rate_limit", + "over_email_send_rate_limit", + "over_sms_send_rate_limit", + "bad_code_verifier", + "anonymous_provider_disabled", + "hook_timeout", + "hook_timeout_after_retry", + "hook_payload_over_size_limit", + "hook_payload_invalid_content_type", + "request_timeout", + "mfa_phone_enroll_not_enabled", + "mfa_phone_verify_not_enabled", + "mfa_totp_enroll_not_enabled", + "mfa_totp_verify_not_enabled", + "mfa_webauthn_enroll_not_enabled", + "mfa_webauthn_verify_not_enabled", + "mfa_verified_factor_exists", + "invalid_credentials", + "email_address_not_authorized", + "email_address_invalid", +] + + +class AuthError(Exception): + def __init__(self, message: str, code: ErrorCode) -> None: + Exception.__init__(self, message) + self.message = message + self.name = "AuthError" + self.code = code + + +class AuthApiErrorDict(TypedDict): + name: str + message: str + status: int + code: ErrorCode + + +class AuthApiError(AuthError): + def __init__(self, message: str, status: int, code: ErrorCode) -> None: + AuthError.__init__(self, message, code) + self.name = "AuthApiError" + self.status = status + self.code = code + + def to_dict(self) -> AuthApiErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + "code": self.code, + } + + +class AuthUnknownError(AuthError): + def __init__(self, message: str, original_error: Exception) -> None: + AuthError.__init__(self, message, None) + self.name = "AuthUnknownError" + self.original_error = original_error + + +class CustomAuthError(AuthError): + def __init__(self, message: str, name: str, status: int, code: ErrorCode) -> None: + AuthError.__init__(self, message, code) + 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, + None, + ) + + +class AuthInvalidCredentialsError(CustomAuthError): + def __init__(self, message: str) -> None: + CustomAuthError.__init__( + self, + message, + "AuthInvalidCredentialsError", + 400, + None, + ) + + +class AuthImplicitGrantRedirectErrorDetails(TypedDict): + error: str + code: str + + +class AuthImplicitGrantRedirectErrorDict(AuthApiErrorDict): + details: Optional[AuthImplicitGrantRedirectErrorDetails] + + +class AuthImplicitGrantRedirectError(CustomAuthError): + def __init__( + self, + message: str, + details: Optional[AuthImplicitGrantRedirectErrorDetails] = None, + ) -> None: + CustomAuthError.__init__( + self, + message, + "AuthImplicitGrantRedirectError", + 500, + None, + ) + 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, + None, + ) + + +class AuthWeakPasswordError(CustomAuthError): + def __init__(self, message: str, status: int, reasons: List[str]) -> None: + CustomAuthError.__init__( + self, + message, + "AuthWeakPasswordError", + status, + "weak_password", + ) + self.reasons = reasons + + def to_dict(self) -> AuthApiErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + "reasons": self.reasons, + } + + +class AuthInvalidJwtError(CustomAuthError): + def __init__(self, message: str) -> None: + CustomAuthError.__init__( + self, + message, + "AuthInvalidJwtError", + 400, + "invalid_jwt", + ) diff --git a/src/auth/src/supabase_auth/helpers.py b/src/auth/src/supabase_auth/helpers.py new file mode 100644 index 00000000..7f9df7c8 --- /dev/null +++ b/src/auth/src/supabase_auth/helpers.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +import base64 +import hashlib +import re +import secrets +import string +import uuid +from base64 import urlsafe_b64decode +from datetime import datetime +from json import loads +from typing import Any, Dict, Optional, Type, TypedDict, TypeVar, cast +from urllib.parse import urlparse + +from httpx import HTTPStatusError, Response +from pydantic import BaseModel + +from .constants import API_VERSION_HEADER_NAME, API_VERSIONS, BASE64URL_REGEX +from .errors import ( + AuthApiError, + AuthError, + AuthInvalidJwtError, + AuthRetryableError, + AuthUnknownError, + AuthWeakPasswordError, +) +from .types import ( + AuthOtpResponse, + AuthResponse, + GenerateLinkProperties, + GenerateLinkResponse, + JWKSet, + JWTHeader, + JWTPayload, + LinkIdentityResponse, + Session, + SSOResponse, + User, + UserResponse, +) + +TBaseModel = TypeVar("TBaseModel", bound=BaseModel) + + +def model_validate(model: Type[TBaseModel], contents) -> TBaseModel: + """Compatibility layer between pydantic 1 and 2 for parsing an instance + of a BaseModel from varied""" + try: + # pydantic > 2 + return model.model_validate(contents) + except AttributeError: + # pydantic < 2 + return model.parse_obj(contents) + + +def model_dump(model: BaseModel) -> Dict[str, Any]: + """Compatibility layer between pydantic 1 and 2 for dumping a model's contents as a dict""" + try: + # pydantic > 2 + return model.model_dump() + except AttributeError: + # pydantic < 2 + return model.dict() + + +def model_dump_json(model: BaseModel) -> str: + """Compatibility layer between pydantic 1 and 2 for dumping a model's contents as json""" + try: + # pydantic > 2 + return model.model_dump_json() + except AttributeError: + # pydantic < 2 + return model.json() + + +def parse_auth_response(data: Any) -> AuthResponse: + session: Optional[Session] = 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 = model_validate(Session, data) + user_data = data.get("user", data) + user = model_validate(User, user_data) if user_data else None + return AuthResponse(session=session, user=user) + + +def parse_auth_otp_response(data: Any) -> AuthOtpResponse: + return model_validate(AuthOtpResponse, data) + + +def parse_link_identity_response(data: Any) -> LinkIdentityResponse: + return model_validate(LinkIdentityResponse, data) + + +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 = model_validate( + User, {k: v for k, v in data.items() if k not in model_dump(properties)} + ) + return GenerateLinkResponse(properties=properties, user=user) + + +def parse_user_response(data: Any) -> UserResponse: + if "user" not in data: + data = {"user": data} + return model_validate(UserResponse, data) + + +def parse_sso_response(data: Any) -> SSOResponse: + return model_validate(SSOResponse, data) + + +def parse_jwks(response: Any) -> JWKSet: + if "keys" not in response or len(response["keys"]) == 0: + raise AuthInvalidJwtError("JWKS is empty") + + return {"keys": response["keys"]} + + +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 get_error_code(error: Any) -> str: + return error.get("error_code", None) if isinstance(error, dict) else None + + +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: + 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 + ) + data = error.response.json() + + error_code = None + response_api_version = parse_response_api_version(error.response) + + if ( + response_api_version + and datetime.timestamp(response_api_version) + >= API_VERSIONS.get("2024-01-01").get("timestamp") + and isinstance(data, dict) + and data + and isinstance(data.get("code"), str) + ): + error_code = data.get("code") + elif ( + isinstance(data, dict) and data and isinstance(data.get("error_code"), str) + ): + error_code = data.get("error_code") + + if error_code is None: + if ( + isinstance(data, dict) + and data + and isinstance(data.get("weak_password"), dict) + and data.get("weak_password") + and isinstance(data.get("weak_password"), list) + and len(data.get("weak_password")) + ): + return AuthWeakPasswordError( + get_error_message(data), + error.response.status_code, + data.get("weak_password").get("reasons"), + ) + elif error_code == "weak_password": + return AuthWeakPasswordError( + get_error_message(data), + error.response.status_code, + data.get("weak_password", {}).get("reasons", {}), + ) + + return AuthApiError( + get_error_message(data), + error.response.status_code or 500, + error_code, + ) + except Exception as e: + return AuthUnknownError(get_error_message(error), e) + + +def str_from_base64url(base64url: str) -> str: + # Addding padding otherwise the following error happens: + # binascii.Error: Incorrect padding + base64url_with_padding = base64url + "=" * (-len(base64url) % 4) + return urlsafe_b64decode(base64url_with_padding).decode("utf-8") + + +def base64url_to_bytes(base64url: str) -> bytes: + # Addding padding otherwise the following error happens: + # binascii.Error: Incorrect padding + base64url_with_padding = base64url + "=" * (-len(base64url) % 4) + return urlsafe_b64decode(base64url_with_padding) + + +class DecodedJWT(TypedDict): + header: JWTHeader + payload: JWTPayload + signature: bytes + raw: Dict[str, str] + + +def decode_jwt(token: str) -> DecodedJWT: + parts = token.split(".") + if len(parts) != 3: + raise AuthInvalidJwtError("Invalid JWT structure") + + # regex check for base64url + for part in parts: + if not re.match(BASE64URL_REGEX, part, re.IGNORECASE): + raise AuthInvalidJwtError("JWT not in base64url format") + + return DecodedJWT( + header=JWTHeader(**loads(str_from_base64url(parts[0]))), + payload=JWTPayload(**loads(str_from_base64url(parts[1]))), + signature=base64url_to_bytes(parts[2]), + raw={ + "header": parts[0], + "payload": parts[1], + }, + ) + + +def generate_pkce_verifier(length=64): + """Generate a random PKCE verifier of the specified length.""" + if length < 43 or length > 128: + raise ValueError("PKCE verifier length must be between 43 and 128 characters") + + # Define characters that can be used in the PKCE verifier + charset = string.ascii_letters + string.digits + "-._~" + + return "".join(secrets.choice(charset) for _ in range(length)) + + +def generate_pkce_challenge(code_verifier): + """Generate a code challenge from a PKCE verifier.""" + # Hash the verifier using SHA-256 + verifier_bytes = code_verifier.encode("utf-8") + sha256_hash = hashlib.sha256(verifier_bytes).digest() + + return base64.urlsafe_b64encode(sha256_hash).rstrip(b"=").decode("utf-8") + + +API_VERSION_REGEX = r"^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$" + + +def parse_response_api_version(response: Response): + api_version = response.headers.get(API_VERSION_HEADER_NAME) + + if not api_version: + return None + + if re.search(API_VERSION_REGEX, api_version) is None: + return None + + try: + dt = datetime.strptime(api_version, "%Y-%m-%d") + return dt + except Exception as e: + return None + + +def is_http_url(url: str) -> bool: + return urlparse(url).scheme in {"https", "http"} + + +def validate_exp(exp: int) -> None: + if not exp: + raise AuthInvalidJwtError("JWT has no expiration time") + + time_now = datetime.now().timestamp() + if exp <= time_now: + raise AuthInvalidJwtError("JWT has expired") + + +def is_valid_uuid(value: str) -> bool: + try: + uuid.UUID(value) + return True + except ValueError: + return False diff --git a/src/auth/src/supabase_auth/http_clients.py b/src/auth/src/supabase_auth/http_clients.py new file mode 100644 index 00000000..6dbd91d9 --- /dev/null +++ b/src/auth/src/supabase_auth/http_clients.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from httpx import AsyncClient # noqa: F401 +from httpx import Client as BaseClient + + +class SyncClient(BaseClient): + def aclose(self) -> None: + self.close() diff --git a/src/auth/src/supabase_auth/py.typed b/src/auth/src/supabase_auth/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/auth/src/supabase_auth/timer.py b/src/auth/src/supabase_auth/timer.py new file mode 100644 index 00000000..11f11536 --- /dev/null +++ b/src/auth/src/supabase_auth/timer.py @@ -0,0 +1,45 @@ +import asyncio +from threading import Timer as _Timer +from typing import Any, Callable, Coroutine, Optional, cast + + +class Timer: + def __init__( + self, + seconds: float, + function: Callable[[], Optional[Coroutine[Any, Any, None]]], + ) -> None: + self._milliseconds = seconds + self._function = function + self._task: Optional[asyncio.Task] = None + self._timer: Optional[_Timer] = 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.daemon = True + 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/src/auth/src/supabase_auth/types.py b/src/auth/src/supabase_auth/types.py new file mode 100644 index 00000000..c41319c7 --- /dev/null +++ b/src/auth/src/supabase_auth/types.py @@ -0,0 +1,877 @@ +from __future__ import annotations + +from datetime import datetime +from time import time +from typing import Any, Callable, Dict, List, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field + +try: + # > 2 + from pydantic import model_validator + + model_validator_v1_v2_compat = model_validator(mode="before") +except ImportError: + # < 2 + from pydantic import root_validator + + model_validator_v1_v2_compat = root_validator + +from typing_extensions import Literal, NotRequired, TypedDict + +Provider = Literal[ + "apple", + "azure", + "bitbucket", + "discord", + "facebook", + "figma", + "fly", + "github", + "gitlab", + "google", + "kakao", + "keycloak", + "linkedin", + "linkedin_oidc", + "notion", + "slack", + "slack_oidc", + "spotify", + "twitch", + "twitter", + "workos", + "zoom", +] + +EmailOtpType = Literal[ + "signup", "invite", "magiclink", "recovery", "email_change", "email" +] + +AuthChangeEventMFA = Literal["MFA_CHALLENGE_VERIFIED"] + +AuthFlowType = Literal["pkce", "implicit"] + +AuthChangeEvent = Literal[ + "PASSWORD_RECOVERY", + "SIGNED_IN", + "SIGNED_OUT", + "TOKEN_REFRESHED", + "USER_UPDATED", + "USER_DELETED", + AuthChangeEventMFA, +] + + +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] + captcha_token: NotRequired[str] + + +class UpdateUserOptions(TypedDict): + email_redirect_to: NotRequired[str] + + +class InviteUserByEmailOptions(TypedDict): + redirect_to: NotRequired[str] + data: NotRequired[Any] + + +class AuthResponse(BaseModel): + user: Optional[User] = None + session: Optional[Session] = None + + +class AuthOtpResponse(BaseModel): + user: None = None + session: None = None + message_id: Optional[str] = None + + +class OAuthResponse(BaseModel): + provider: Provider + url: str + + +class SSOResponse(BaseModel): + url: str + + +class LinkIdentityResponse(BaseModel): + url: str + + +class IdentitiesResponse(BaseModel): + identities: List[UserIdentity] + + +class UserResponse(BaseModel): + user: User + + +class Session(BaseModel): + provider_token: Optional[str] = None + """ + The oauth provider token. If present, this can be used to make external API + requests to the oauth provider used. + """ + provider_refresh_token: Optional[str] = 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: Optional[int] = None + """ + A timestamp of when the token will expire. Returned when a login is confirmed. + """ + token_type: str + user: User + + @model_validator_v1_v2_compat + 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 UserIdentity(BaseModel): + id: str + identity_id: str + user_id: str + identity_data: Dict[str, Any] + provider: str + created_at: datetime + last_sign_in_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class Factor(BaseModel): + """ + A MFA factor. + """ + + id: str + """ + ID of the factor. + """ + friendly_name: Optional[str] = None + """ + Friendly name of the factor, useful to disambiguate between multiple factors. + """ + factor_type: Union[Literal["totp", "phone"], 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] + user_metadata: Dict[str, Any] + aud: str + confirmation_sent_at: Optional[datetime] = None + recovery_sent_at: Optional[datetime] = None + email_change_sent_at: Optional[datetime] = None + new_email: Optional[str] = None + new_phone: Optional[str] = None + invited_at: Optional[datetime] = None + action_link: Optional[str] = None + email: Optional[str] = None + phone: Optional[str] = None + created_at: datetime + confirmed_at: Optional[datetime] = None + email_confirmed_at: Optional[datetime] = None + phone_confirmed_at: Optional[datetime] = None + last_sign_in_at: Optional[datetime] = None + role: Optional[str] = None + updated_at: Optional[datetime] = None + identities: Optional[List[UserIdentity]] = None + is_anonymous: bool = False + factors: Optional[List[Factor]] = None + + +class UserAttributes(TypedDict): + email: NotRequired[str] + phone: NotRequired[str] + password: NotRequired[str] + data: NotRequired[Any] + nonce: NotRequired[str] + + +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"]]] + role: NotRequired[str] + """ + The `role` claim set in the user's access token JWT. + + When a user signs up, this role is set to `authenticated` by default. You should only modify the `role` if you need to provision several levels of admin access that have different permissions on individual columns in your database. + + Setting this role to `service_role` is not recommended as it grants the user admin privileges. + """ + password_hash: NotRequired[str] + """ + The `password_hash` for the user's password. + + Allows you to specify a password hash for the user. This is useful for migrating a user's password hash from another service. + + Supports bcrypt and argon2 password hashes. + """ + id: NotRequired[str] + """ + The `id` for the user. + + Allows you to overwrite the default `id` set for the user. + """ + + +class Subscription(BaseModel): + id: str + """ + 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 UpdatableFactorAttributes(TypedDict): + friendly_name: str + + +class SignUpWithEmailAndPasswordCredentialsOptions( + TypedDict, +): + email_redirect_to: NotRequired[str] + data: NotRequired[Any] + captcha_token: NotRequired[str] + + +class SignUpWithEmailAndPasswordCredentials(TypedDict): + email: str + password: str + options: NotRequired[SignUpWithEmailAndPasswordCredentialsOptions] + + +class SignUpWithPhoneAndPasswordCredentialsOptions(TypedDict): + data: NotRequired[Any] + captcha_token: NotRequired[str] + channel: NotRequired[Literal["sms", "whatsapp"]] + + +class SignUpWithPhoneAndPasswordCredentials(TypedDict): + phone: str + password: str + options: NotRequired[SignUpWithPhoneAndPasswordCredentialsOptions] + + +SignUpWithPasswordCredentials = Union[ + SignUpWithEmailAndPasswordCredentials, + SignUpWithPhoneAndPasswordCredentials, +] + + +class SignInWithPasswordCredentialsOptions(TypedDict): + data: NotRequired[Any] + captcha_token: NotRequired[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 SignInWithIdTokenCredentials(TypedDict): + """ + Provider name or OIDC `iss` value identifying which provider should be used to verify the provided token. Supported names: `google`, `apple`, `azure`, `facebook`, `kakao`, `keycloak` (deprecated). + """ + + provider: Literal["google", "apple", "azure", "facebook", "kakao"] + token: str + access_token: NotRequired[str] + nonce: NotRequired[str] + options: NotRequired[SignInWithIdTokenCredentialsOptions] + + +class SignInWithIdTokenCredentialsOptions(TypedDict): + captcha_token: NotRequired[str] + + +class SignInWithEmailAndPasswordlessCredentialsOptions(TypedDict): + email_redirect_to: NotRequired[str] + should_create_user: NotRequired[bool] + data: NotRequired[Any] + captcha_token: NotRequired[str] + + +class SignInWithEmailAndPasswordlessCredentials(TypedDict): + email: str + options: NotRequired[SignInWithEmailAndPasswordlessCredentialsOptions] + + +class SignInWithPhoneAndPasswordlessCredentialsOptions(TypedDict): + should_create_user: NotRequired[bool] + data: NotRequired[Any] + captcha_token: NotRequired[str] + channel: NotRequired[Literal["sms", "whatsapp"]] + + +class SignInWithPhoneAndPasswordlessCredentials(TypedDict): + phone: str + options: NotRequired[SignInWithPhoneAndPasswordlessCredentialsOptions] + + +SignInWithPasswordlessCredentials = Union[ + SignInWithEmailAndPasswordlessCredentials, + SignInWithPhoneAndPasswordlessCredentials, +] + + +class ResendEmailCredentialsOptions(TypedDict): + email_redirect_to: NotRequired[str] + captcha_token: NotRequired[str] + + +class ResendEmailCredentials(TypedDict): + type: Literal["signup", "email_change"] + email: str + options: NotRequired[ResendEmailCredentialsOptions] + + +class ResendPhoneCredentialsOptions(TypedDict): + captcha_token: NotRequired[str] + + +class ResendPhoneCredentials(TypedDict): + type: Literal["sms", "phone_change"] + phone: str + options: NotRequired[ResendPhoneCredentialsOptions] + + +ResendCredentials = Union[ResendEmailCredentials, ResendPhoneCredentials] + + +class SignInWithOAuthCredentialsOptions(TypedDict): + redirect_to: NotRequired[str] + scopes: NotRequired[str] + query_params: NotRequired[Dict[str, str]] + + +class SignInWithOAuthCredentials(TypedDict): + provider: Provider + options: NotRequired[SignInWithOAuthCredentialsOptions] + + +class SignInWithSSOCredentials(TypedDict): + provider_id: NotRequired[str] + domain: NotRequired[str] + options: NotRequired[SignInWithSSOOptions] + + +class SignInWithSSOOptions(TypedDict): + redirect_to: NotRequired[str] + skip_http_redirect: NotRequired[bool] + + +class SignInAnonymouslyCredentials(TypedDict): + options: NotRequired[SignInAnonymouslyCredentialsOptions] + + +class SignInAnonymouslyCredentialsOptions(TypedDict): + data: NotRequired[Any] + captcha_token: NotRequired[str] + + +class VerifyOtpParamsOptions(TypedDict): + redirect_to: NotRequired[str] + captcha_token: NotRequired[str] + + +class VerifyEmailOtpParams(TypedDict): + email: str + token: str + type: EmailOtpType + options: NotRequired[VerifyOtpParamsOptions] + + +class VerifyMobileOtpParams(TypedDict): + phone: str + token: str + type: Literal[ + "sms", + "phone_change", + ] + options: NotRequired[VerifyOtpParamsOptions] + + +class VerifyTokenHashParams(TypedDict): + token_hash: str + type: EmailOtpType + options: NotRequired[VerifyOtpParamsOptions] + + +VerifyOtpParams = Union[ + VerifyEmailOtpParams, VerifyMobileOtpParams, VerifyTokenHashParams +] + + +class GenerateLinkParamsOptions(TypedDict): + redirect_to: NotRequired[str] + + +class GenerateLinkParamsWithDataOptions(GenerateLinkParamsOptions, TypedDict): + data: NotRequired[Any] + + +class GenerateSignupLinkParams(TypedDict): + type: Literal["signup"] + email: str + password: str + options: NotRequired[GenerateLinkParamsWithDataOptions] + + +class GenerateInviteOrMagiclinkParams(TypedDict): + type: Literal["invite", "magiclink"] + email: str + options: NotRequired[GenerateLinkParamsWithDataOptions] + + +class GenerateRecoveryLinkParams(TypedDict): + type: Literal["recovery"] + email: str + options: NotRequired[GenerateLinkParamsOptions] + + +class GenerateEmailChangeLinkParams(TypedDict): + type: Literal["email_change_current", "email_change_new"] + email: str + new_email: str + options: NotRequired[GenerateLinkParamsOptions] + + +GenerateLinkParams = Union[ + GenerateSignupLinkParams, + GenerateInviteOrMagiclinkParams, + GenerateRecoveryLinkParams, + GenerateEmailChangeLinkParams, +] + +GenerateLinkType = Literal[ + "signup", + "invite", + "magiclink", + "recovery", + "email_change_current", + "email_change_new", +] + + +class MFAEnrollTOTPParams(TypedDict): + factor_type: Literal["totp"] + issuer: NotRequired[str] + friendly_name: NotRequired[str] + + +class MFAEnrollPhoneParams(TypedDict): + factor_type: Literal["phone"] + friendly_name: NotRequired[str] + phone: str + + +MFAEnrollParams = Union[MFAEnrollTOTPParams, MFAEnrollPhoneParams] + + +class MFAUnenrollParams(TypedDict): + factor_id: str + """ + ID of the factor being unenrolled. + """ + + +class CodeExchangeParams(TypedDict): + code_verifier: str + """ + Randomly generated string + """ + auth_code: str + """ + Code returned after completing one of the authorization flows + """ + redirect_to: str + """ + The URL to route to after a session is successfully obtained + """ + + +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. + """ + channel: NotRequired[Literal["sms", "whatsapp"]] + + +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 + """ + 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", "phone"] + """ + Type of MFA factor. Only `totp` supported for now. + """ + totp: Optional[AuthMFAEnrollResponseTotp] = None + """ + TOTP enrollment information. + """ + model_config = ConfigDict(arbitrary_types_allowed=True) + friendly_name: str + """ + Friendly name of the factor, useful for distinguishing between factors + """ + phone: Optional[str] = None + """ + Phone number of the MFA factor in E.164 format. Used to send messages + """ + + @model_validator_v1_v2_compat + def validate_phone_required_for_phone_type(cls, values: dict) -> dict: + if values.get("type") == "phone" and not values.get("phone"): + raise ValueError("phone is required when type is 'phone'") + return values + + +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. + """ + factor_type: Optional[Literal["totp", "phone"]] = Field( + validation_alias="type", default=None + ) + """ + Factor Type which generated the challenge + """ + + +class AuthMFAListFactorsResponse(BaseModel): + all: List[Factor] + """ + All available factors (verified and unverified). + """ + totp: List[Factor] + """ + Only verified TOTP factors. (A subset of `all`.) + """ + phone: List[Factor] + """ + Only verified Phone factors. (A subset of `all`.) + """ + + +AuthenticatorAssuranceLevels = Literal["aal1", "aal2"] + + +class AuthMFAGetAuthenticatorAssuranceLevelResponse(BaseModel): + current_level: Optional[AuthenticatorAssuranceLevels] = None + """ + Current AAL level of the session. + """ + next_level: Optional[AuthenticatorAssuranceLevels] = 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. + """ + + 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 + + +class DecodedJWTDict(TypedDict): + exp: NotRequired[int] + aal: NotRequired[Optional[AuthenticatorAssuranceLevels]] + amr: NotRequired[Optional[List[AMREntry]]] + + +SignOutScope = Literal["global", "local", "others"] + + +class SignOutOptions(TypedDict): + scope: NotRequired[SignOutScope] + + +class JWTHeader(TypedDict): + alg: Literal["RS256", "ES256", "HS256"] + typ: str + kid: str + + +class RequiredClaims(TypedDict): + iss: str + sub: str + auth: Union[str, List[str]] + exp: int + iat: int + role: str + aal: AuthenticatorAssuranceLevels + session_id: str + + +class JWTPayload(RequiredClaims, total=False): + pass + + +class ClaimsResponse(TypedDict): + claims: JWTPayload + headers: JWTHeader + signature: bytes + + +class JWK(TypedDict, total=False): + kty: Literal["RSA", "EC", "oct"] + key_ops: List[str] + alg: Optional[str] + kid: Optional[str] + + +class JWKSet(TypedDict): + keys: List[JWK] + + +for model in [ + AMREntry, + AuthResponse, + OAuthResponse, + UserResponse, + Session, + UserIdentity, + Factor, + User, + Subscription, + AuthMFAVerifyResponse, + AuthMFAEnrollResponseTotp, + AuthMFAEnrollResponse, + AuthMFAUnenrollResponse, + AuthMFAChallengeResponse, + AuthMFAListFactorsResponse, + AuthMFAGetAuthenticatorAssuranceLevelResponse, + AuthMFAAdminDeleteFactorResponse, + AuthMFAAdminListFactorsResponse, + GenerateLinkProperties, +]: + try: + # pydantic > 2 + model.model_rebuild() + except AttributeError: + # pydantic < 2 + model.update_forward_refs() diff --git a/src/auth/src/supabase_auth/version.py b/src/auth/src/supabase_auth/version.py new file mode 100644 index 00000000..671be61f --- /dev/null +++ b/src/auth/src/supabase_auth/version.py @@ -0,0 +1 @@ +__version__ = "2.12.3" # {x-release-please-version} diff --git a/src/auth/tests/__init__.py b/src/auth/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/auth/tests/_async/__init__.py b/src/auth/tests/_async/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/auth/tests/_async/clients.py b/src/auth/tests/_async/clients.py new file mode 100644 index 00000000..03356714 --- /dev/null +++ b/src/auth/tests/_async/clients.py @@ -0,0 +1,140 @@ +from jwt import encode + +from supabase_auth import AsyncGoTrueAdminAPI, AsyncGoTrueClient + +SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 +SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 +SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 +SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT = 9996 + +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_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON = ( + f"http://localhost:{SIGNUP_ENABLED_ASYMMETRIC_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 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, + ) + + +def auth_client_with_asymmetric_session() -> AsyncGoTrueClient: + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=False, + ) + + +def auth_subscription_client(): + return AsyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + auto_refresh_token=False, + persist_session=True, + ) + + +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}", + }, + ) + + +SERVICE_ROLE_JWT = encode( + { + "role": "service_role", + }, + GOTRUE_JWT_SECRET, +) + + +def service_role_api_client(): + return AsyncGoTrueAdminAPI( + url=GOTRUE_URL_SIGNUP_ENABLED_AUTO_CONFIRM_ON, + 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/src/auth/tests/_async/test_gotrue.py b/src/auth/tests/_async/test_gotrue.py new file mode 100644 index 00000000..0a4b512b --- /dev/null +++ b/src/auth/tests/_async/test_gotrue.py @@ -0,0 +1,907 @@ +import time +import unittest +from uuid import uuid4 + +import pytest +from jwt import encode + +from supabase_auth.errors import ( + AuthApiError, + AuthInvalidJwtError, + AuthSessionMissingError, +) +from supabase_auth.helpers import decode_jwt + +from .clients import ( + GOTRUE_JWT_SECRET, + auth_client, + auth_client_with_asymmetric_session, + auth_client_with_session, +) +from .utils import mock_user_credentials + + +async def test_get_claims_returns_none_when_session_is_none(): + claims = await auth_client().get_claims() + assert claims is None + + +async def test_get_claims_calls_get_user_if_symmetric_jwt(mocker): + client = auth_client() + spy = mocker.spy(client, "get_user") + + user = (await client.sign_up(mock_user_credentials())).user + assert user is not None + + claims = (await client.get_claims())["claims"] + assert claims["email"] == user.email + spy.assert_called_once() + + +async def test_get_claims_fetches_jwks_to_verify_asymmetric_jwt(mocker): + client = auth_client_with_asymmetric_session() + + user = (await client.sign_up(mock_user_credentials())).user + assert user is not None + + spy = mocker.spy(client, "_request") + + claims = (await client.get_claims())["claims"] + assert claims["email"] == user.email + + spy.assert_called_once() + spy.assert_called_with("GET", ".well-known/jwks.json", xform=unittest.mock.ANY) + + expected_keyid = "638c54b8-28c2-4b12-9598-ba12ef610a29" + + assert len(client._jwks["keys"]) == 1 + assert client._jwks["keys"][0]["kid"] == expected_keyid + + +async def test_jwks_ttl_cache_behavior(mocker): + client = auth_client_with_asymmetric_session() + + spy = mocker.spy(client, "_request") + + # First call should fetch JWKS from endpoint + user = (await client.sign_up(mock_user_credentials())).user + assert user is not None + + await client.get_claims() + spy.assert_called_with("GET", ".well-known/jwks.json", xform=unittest.mock.ANY) + first_call_count = spy.call_count + + # Second call within TTL should use cache + await client.get_claims() + assert spy.call_count == first_call_count # No additional JWKS request + + # Mock time to be after TTL expiry + original_time = time.time + try: + mock_time = mocker.patch("time.time") + mock_time.return_value = original_time() + 601 # TTL is 600 seconds + + # Call after TTL expiry should fetch fresh JWKS + await client.get_claims() + assert spy.call_count == first_call_count + 1 # One more JWKS request + finally: + # Restore original time function + mocker.patch("time.time", original_time) + + +async def test_set_session_with_valid_tokens(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the tokens from the signup response + access_token = signup_response.session.access_token + refresh_token = signup_response.session.refresh_token + + # Clear the session + await client._remove_session() + + # Set the session with the tokens + response = await client.set_session(access_token, refresh_token) + + # Verify the response + assert response.session is not None + assert response.session.access_token == access_token + assert response.session.refresh_token == refresh_token + assert response.user is not None + assert response.user.email == credentials.get("email") + + +async def test_set_session_with_expired_token(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the tokens from the signup response + access_token = signup_response.session.access_token + refresh_token = signup_response.session.refresh_token + + # Clear the session + await client._remove_session() + + # Create an expired token by modifying the JWT + expired_token = access_token.split(".") + payload = decode_jwt(access_token)["payload"] + payload["exp"] = int(time.time()) - 3600 # Set expiry to 1 hour ago + expired_token[1] = encode(payload, GOTRUE_JWT_SECRET, algorithm="HS256").split(".")[ + 1 + ] + expired_access_token = ".".join(expired_token) + + # Set the session with the expired token + response = await client.set_session(expired_access_token, refresh_token) + + # Verify the response has a new access token (refreshed) + assert response.session is not None + assert response.session.access_token != expired_access_token + assert response.session.refresh_token != refresh_token + assert response.user is not None + assert response.user.email == credentials.get("email") + + +async def test_set_session_without_refresh_token(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the access token from the signup response + access_token = signup_response.session.access_token + + # Clear the session + await client._remove_session() + + # Create an expired token + expired_token = access_token.split(".") + payload = decode_jwt(access_token)["payload"] + payload["exp"] = int(time.time()) - 3600 # Set expiry to 1 hour ago + expired_token[1] = encode(payload, GOTRUE_JWT_SECRET, algorithm="HS256").split(".")[ + 1 + ] + expired_access_token = ".".join(expired_token) + + # Try to set the session with an expired token but no refresh token + with pytest.raises(AuthSessionMissingError): + await client.set_session(expired_access_token, "") + + +async def test_set_session_with_invalid_token(): + client = auth_client() + + # Try to set the session with invalid tokens + with pytest.raises(AuthInvalidJwtError): + await client.set_session("invalid.token.here", "invalid_refresh_token") + + +async def test_mfa_enroll(): + client = auth_client_with_session() + + credentials = mock_user_credentials() + + # First sign up to get a valid session + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + # Test MFA enrollment + enroll_response = await client.mfa.enroll( + {"issuer": "test-issuer", "factor_type": "totp", "friendly_name": "test-factor"} + ) + + assert enroll_response.id is not None + assert enroll_response.type == "totp" + assert enroll_response.friendly_name == "test-factor" + assert enroll_response.totp.qr_code is not None + + +async def test_mfa_challenge(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + enroll_response = await client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA challenge + challenge_response = await client.mfa.challenge({"factor_id": enroll_response.id}) + assert challenge_response.id is not None + assert challenge_response.expires_at is not None + + +async def test_mfa_unenroll(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + enroll_response = await client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA unenroll + unenroll_response = await client.mfa.unenroll({"factor_id": enroll_response.id}) + assert unenroll_response.id == enroll_response.id + + +async def test_mfa_list_factors(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + await client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA list factors + list_response = await client.mfa.list_factors() + assert len(list_response.all) == 1 + + +async def test_initialize_from_url(): + # This test verifies the URL format detection and initialization from URL + client = auth_client() + + # First we'll test the _is_implicit_grant_flow method + # The method checks for access_token or error_description in the query string, not the fragment + url_with_token = "http://example.com/?access_token=test_token&other=value" + assert client._is_implicit_grant_flow(url_with_token) == True + + url_with_error = "http://example.com/?error_description=test_error&other=value" + assert client._is_implicit_grant_flow(url_with_error) == True + + url_without_token = "http://example.com/?other=value" + assert client._is_implicit_grant_flow(url_without_token) == False + + # Now test actual URL initialization with a valid URL containing auth tokens + from unittest.mock import patch + + from supabase_auth.types import Session, User, UserResponse + + # Create a mock user and session to avoid actual API calls + mock_user = User( + id="user123", + email="test@example.com", + app_metadata={}, + user_metadata={}, + aud="authenticated", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="authenticated", + updated_at="2023-01-01T00:00:00Z", + ) + + # Wrap the user in a UserResponse as that's what get_user returns + mock_user_response = UserResponse(user=mock_user) + + # Test successful initialization with tokens in URL + good_url = "http://example.com/?access_token=mock_access_token&refresh_token=mock_refresh_token&expires_in=3600&token_type=bearer" + + # We need to mock: + # 1. get_user which is called by _get_session_from_url to validate the token + # 2. _save_session which is called to store the session data + # 3. _notify_all_subscribers which is called to notify about sign-in + with patch.object(client, "get_user") as mock_get_user: + mock_get_user.return_value = mock_user_response + + with patch.object(client, "_save_session") as mock_save_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call initialize_from_url with the good URL + result = await client.initialize_from_url(good_url) + + # Verify get_user was called with the access token + mock_get_user.assert_called_once_with("mock_access_token") + + # Verify _save_session was called with a Session object + mock_save_session.assert_called_once() + session_arg = mock_save_session.call_args[0][0] + assert isinstance(session_arg, Session) + assert session_arg.access_token == "mock_access_token" + assert session_arg.refresh_token == "mock_refresh_token" + assert session_arg.expires_in == 3600 + + # Verify _notify_all_subscribers was called + mock_notify.assert_called_with("SIGNED_IN", session_arg) + + assert result is None # initialize_from_url doesn't have a return value + + # Test URL with error - need to include error_code for the test to work correctly + error_url = "http://example.com/?error=invalid_request&error_description=Invalid+request&error_code=400" + + # Should throw an error when URL contains error parameters + from supabase_auth.errors import AuthImplicitGrantRedirectError + + try: + await client.initialize_from_url(error_url) + assert False, "Expected AuthImplicitGrantRedirectError" + except AuthImplicitGrantRedirectError as e: + # The error message includes the error_description value + assert "Invalid request" in str(e) + + # Test URL with code for PKCE flow + code_url = "http://example.com/?code=authorization_code" + + # For the code URL path, we're not testing it here since it requires more mocking + # and is indirectly tested via other tests like exchange_code_for_session + + # Test URL with neither tokens nor code - should not throw but also not call anything + invalid_url = "http://example.com/?foo=bar" + with patch.object(client, "_get_session_from_url") as mock_get_session: + result = await client.initialize_from_url(invalid_url) + mock_get_session.assert_not_called() + assert result is None + + +async def test_exchange_code_for_session(): + client = auth_client() + + # We'll test the flow type setting instead of the actual exchange, since the + # actual exchange requires a live OAuth flow which isn't practical in tests + assert client._flow_type in ["implicit", "pkce"] + + # This part would normally need a live OAuth flow, so we verify the logic paths + # Get the storage key for PKCE flow + storage_key = f"{client._storage_key}-code-verifier" + + # Set the flow type to pkce + client._flow_type = "pkce" + + # Test the PKCE URL generation which is needed for exchange_code_for_session + provider = "github" + url, params = await client._get_url_for_provider( + f"{client._url}/authorize", provider, {} + ) + + # Verify PKCE parameters were added + assert "code_challenge" in params + assert "code_challenge_method" in params + + # Verify the code verifier was stored + code_verifier = await client._storage.get_item(storage_key) + assert code_verifier is not None + + +async def test_get_authenticator_assurance_level(): + client = auth_client() + credentials = mock_user_credentials() + + # Without a session, should return null values + aal_response = await client.mfa.get_authenticator_assurance_level() + assert aal_response.current_level is None + assert aal_response.next_level is None + assert aal_response.current_authentication_methods == [] + + # Sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # With a session, should return authentication methods + aal_response = await client.mfa.get_authenticator_assurance_level() + # Basic auth will have password as an authentication method + assert aal_response.current_authentication_methods is not None + + +async def test_link_identity(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + from unittest.mock import patch + + from supabase_auth.types import OAuthResponse + + # Since the test server has manual linking disabled, we'll mock the URL generation + with patch.object(client, "_get_url_for_provider") as mock_url_provider: + mock_url = "http://example.com/authorize?provider=github" + mock_params = {"provider": "github"} + mock_url_provider.return_value = (mock_url, mock_params) + + # Also mock the _request method since the server would reject it + with patch.object(client, "_request") as mock_request: + mock_request.return_value = OAuthResponse(provider="github", url=mock_url) + + # Call the method + response = await client.link_identity({"provider": "github"}) + + # Verify the response + assert response.provider == "github" + assert response.url == mock_url + + +async def test_get_user_identities(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # New users won't have any identities yet, but the call should work + identities_response = await client.get_user_identities() + assert identities_response is not None + # For a new user, identities will be an empty list or None + assert hasattr(identities_response, "identities") + + +async def test_unlink_identity(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Mock a UserIdentity to test unlink_identity + from unittest.mock import patch + + from supabase_auth.types import UserIdentity + + # Create a mock identity + mock_identity = UserIdentity( + id="user-id", + identity_id="identity-id-1", + user_id="user-id", + identity_data={"email": "user@example.com"}, + provider="github", + created_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + updated_at="2023-01-01T00:00:00Z", + ) + + # Mock the _request method since we can't actually unlink an identity that doesn't exist + with patch.object(client, "_request") as mock_request: + mock_request.return_value = None + + # Call the method + await client.unlink_identity(mock_identity) + + # Verify the request was made properly + mock_request.assert_called_once_with( + "DELETE", + "user/identities/identity-id-1", + jwt=signup_response.session.access_token, + ) + + # Test error case: no session + with patch.object(client, "get_session") as mock_get_session: + from supabase_auth.errors import AuthSessionMissingError + + mock_get_session.return_value = None + + try: + await client.unlink_identity(mock_identity) + assert False, "Expected AuthSessionMissingError" + except AuthSessionMissingError: + pass + + +async def test_verify_otp(): + client = auth_client() + + # Mock the _request method since we can't actually verify an OTP in the test + import time + from unittest.mock import patch + + from supabase_auth.types import AuthResponse, Session, User + + mock_user = User( + id="test-user-id", + app_metadata={}, + user_metadata={}, + aud="test-aud", + email="test@example.com", + phone="", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="", + updated_at="2023-01-01T00:00:00Z", + ) + + mock_session = Session( + access_token="mock-access-token", + refresh_token="mock-refresh-token", + expires_in=3600, + expires_at=round(time.time()) + 3600, + token_type="bearer", + user=mock_user, + ) + + mock_response = AuthResponse(session=mock_session, user=mock_user) + + with patch.object(client, "_request") as mock_request: + # Configure the mock to return a predefined response + mock_request.return_value = mock_response + + # Also patch _save_session to avoid actual storage interactions + with patch.object(client, "_save_session") as mock_save: + # Call verify_otp with test parameters + params = { + "type": "sms", + "phone": "+11234567890", + "token": "123456", + "options": {"redirect_to": "https://example.com/callback"}, + } + + response = await client.verify_otp(params) + + # Verify the request was made with correct parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" # method + assert args[1] == "verify" # path + assert kwargs["body"]["phone"] == "+11234567890" + assert kwargs["body"]["token"] == "123456" + assert kwargs["redirect_to"] == "https://example.com/callback" + + # Verify the session was saved + mock_save.assert_called_once_with(mock_session) + + # Verify the response + assert response == mock_response + + +async def test_sign_in_with_password(): + client = auth_client() + credentials = mock_user_credentials() + from supabase_auth.errors import AuthApiError, AuthInvalidCredentialsError + + # First create a user we can sign in with + signup_response = await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Test signing in with the same credentials (email) + signin_response = await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + # Verify the response has a valid session and user + assert signin_response.session is not None + assert signin_response.user is not None + assert signin_response.user.email == credentials.get("email") + + # Test error case: wrong password + + # We need to create a custom client to avoid affecting other tests + test_client = auth_client() + + try: + await test_client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "wrong_password", + } + ) + assert False, "Expected AuthApiError for wrong password" + except AuthApiError: + pass + + # Test error case: missing credentials + try: + await test_client.sign_in_with_password({}) + assert False, "Expected AuthInvalidCredentialsError for missing credentials" + except AuthInvalidCredentialsError: + pass + + +async def test_sign_in_with_otp(): + client = auth_client() + + # Test with email OTP + email = f"test-{uuid4()}@example.com" + + # When sign_in_with_otp is called with valid email, it should return a AuthOtpResponse + # We can't fully test the actual OTP flow since that requires email verification + from unittest.mock import patch + + from supabase_auth.types import AuthOtpResponse + + # First test for email OTP + with patch.object(client, "_request") as mock_request: + mock_response = AuthOtpResponse( + message_id="mock-message-id", email=email, phone=None, hash=None + ) + mock_request.return_value = mock_response + + response = await client.sign_in_with_otp( + { + "email": email, + "options": { + "email_redirect_to": "https://example.com/callback", + "should_create_user": True, + "data": {"custom": "data"}, + "captcha_token": "mock-captcha-token", + }, + } + ) + + # Verify request parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" + assert args[1] == "otp" + assert kwargs["body"]["email"] == email + assert kwargs["body"]["create_user"] == True + assert kwargs["body"]["data"] == {"custom": "data"} + assert ( + kwargs["body"]["gotrue_meta_security"]["captcha_token"] + == "mock-captcha-token" + ) + assert kwargs["redirect_to"] == "https://example.com/callback" + + # Verify response + assert response == mock_response + + # Test with phone OTP + phone = "+11234567890" + + with patch.object(client, "_request") as mock_request: + mock_response = AuthOtpResponse( + message_id="mock-message-id", email=None, phone=phone, hash=None + ) + mock_request.return_value = mock_response + + response = await client.sign_in_with_otp( + { + "phone": phone, + "options": { + "should_create_user": True, + "data": {"custom": "data"}, + "channel": "whatsapp", # Test alternate channel + "captcha_token": "mock-captcha-token", + }, + } + ) + + # Verify request parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" + assert args[1] == "otp" + assert kwargs["body"]["phone"] == phone + assert kwargs["body"]["create_user"] == True + assert kwargs["body"]["data"] == {"custom": "data"} + assert kwargs["body"]["channel"] == "whatsapp" + assert ( + kwargs["body"]["gotrue_meta_security"]["captcha_token"] + == "mock-captcha-token" + ) + assert kwargs.get("redirect_to") is None # No redirect for phone + + # Verify response + assert response == mock_response + + # Test with invalid parameters (missing both email and phone) + from supabase_auth.errors import AuthInvalidCredentialsError + + try: + await client.sign_in_with_otp({}) + assert False, "Expected AuthInvalidCredentialsError" + except AuthInvalidCredentialsError: + pass + + +async def test_sign_out(): + from unittest.mock import patch + + from supabase_auth.types import Session, User + + client = auth_client() + + # Create a mock user and session + mock_user = User( + id="user123", + email="test@example.com", + app_metadata={}, + user_metadata={}, + aud="authenticated", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="authenticated", + updated_at="2023-01-01T00:00:00Z", + ) + + mock_session = Session( + access_token="mock_access_token", + refresh_token="mock_refresh_token", + expires_in=3600, + token_type="bearer", + user=mock_user, + ) + + # Test sign_out with "global" scope (default) + # This should call admin.sign_out, _remove_session, and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope (global) + await client.sign_out() + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "global" + ) + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test sign_out with "local" scope + # Should behave the same as "global" for client-side + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with local scope + await client.sign_out({"scope": "local"}) + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "local" + ) + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test sign_out with "others" scope + # This should only call admin.sign_out but not _remove_session or _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with others scope + await client.sign_out({"scope": "others"}) + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "others" + ) + + # Verify that _remove_session was NOT called + mock_remove_session.assert_not_called() + + # Verify that _notify_all_subscribers was NOT called + mock_notify.assert_not_called() + + # Test sign_out with no session + # This should not call admin.sign_out but still call _remove_session and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = None + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope + await client.sign_out() + + # Verify that admin.sign_out was NOT called + mock_admin_sign_out.assert_not_called() + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test when admin.sign_out raises an error + # This should suppress the error and continue with _remove_session and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + mock_admin_sign_out.side_effect = AuthApiError( + "Test error", 401, "auth_error" + ) + + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope + await client.sign_out() + + # Verify that _remove_session was still called despite the error + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was still called despite the error + mock_notify.assert_called_once_with("SIGNED_OUT", None) diff --git a/src/auth/tests/_async/test_gotrue_admin_api.py b/src/auth/tests/_async/test_gotrue_admin_api.py new file mode 100644 index 00000000..69253c26 --- /dev/null +++ b/src/auth/tests/_async/test_gotrue_admin_api.py @@ -0,0 +1,641 @@ +import uuid + +import pytest + +from supabase_auth.errors import ( + AuthApiError, + AuthError, + AuthInvalidCredentialsError, + AuthSessionMissingError, + AuthWeakPasswordError, +) + +from .clients import ( + auth_client, + 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, +) + + +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_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 + + +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") + + +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_invalid_credential_sign_in_with_phone(): + try: + response = await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "phone": "+123456789", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_invalid_credential_sign_in_with_email(): + try: + response = await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "email": "unknown_user@unknowndomain.com", + "password": "strong_pwd", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_otp_email(): + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "email": "unknown_user@unknowndomain.com", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_otp_phone(): + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "phone": "+112345678", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_resend(): + try: + await client_api_auto_confirm_off_signups_enabled_client().resend( + {"phone": "+112345678", "type": "sms"} + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_reauthenticate(): + try: + response = await auth_client_with_session().reauthenticate() + except AuthSessionMissingError: + pass + + +async def test_refresh_session(): + try: + response = await auth_client_with_session().refresh_session() + except AuthSessionMissingError: + pass + + +async def test_reset_password_for_email(): + credentials = mock_user_credentials() + try: + response = await auth_client_with_session().reset_password_email( + email=credentials.get("email") + ) + except AuthSessionMissingError: + pass + + +async def test_resend_missing_credentials(): + try: + await client_api_auto_confirm_off_signups_enabled_client().resend( + {"type": "email_change"} + ) + except AuthInvalidCredentialsError as e: + assert e.to_dict() + + +async def test_sign_in_anonymously(): + try: + response = await auth_client_with_session().sign_in_anonymously() + assert response + except AuthApiError: + pass + + +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 == "Token has expired or is invalid" + + +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 (E.164 required)" + + +async def test_sign_in_with_id_token(): + try: + await ( + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_id_token( + { + "provider": "google", + "token": "123456", + } + ) + ) + except AuthApiError as e: + assert e.to_dict() + + +async def test_sign_in_with_sso(): + with pytest.raises(AuthApiError, match=r"SAML 2.0 is disabled") as exc: + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_sso( + { + "domain": "google", + } + ) + assert exc.value is not None + + +async def test_sign_in_with_oauth(): + assert ( + await client_api_auto_confirm_off_signups_enabled_client().sign_in_with_oauth( + { + "provider": "google", + } + ) + ) + + +async def test_link_identity_missing_session(): + with pytest.raises(AuthSessionMissingError) as exc: + await client_api_auto_confirm_off_signups_enabled_client().link_identity( + { + "provider": "google", + } + ) + assert exc.value is not None + + +async def test_get_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert await client._storage.get_item(client._storage_key) is not None + + +async def test_remove_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client._storage.remove_item(client._storage_key) + assert client._storage_key not in client._storage.storage + + +async def test_list_factors(): + credentials = mock_user_credentials() + client = auth_client() + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + factors = await client._list_factors() + assert factors + assert isinstance(factors.totp, list) and isinstance(factors.phone, list) + + +async def test_start_auto_refresh_token(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + assert await client._start_auto_refresh_token(2.0) is None + + +async def test_recover_and_refresh(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client._recover_and_refresh() + assert client._storage_key in client._storage.storage + + +async def test_get_user_identities(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert (await client.get_user_identities()).identities[0].identity_data[ + "email" + ] == credentials.get("email") + + +async def test_update_user(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + await client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + await client.update_user({"password": "123e5a"}) + await client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "123e5a", + } + ) + + +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_weak_email_password_error(): + credentials = mock_user_credentials() + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +async def test_weak_phone_password_error(): + credentials = mock_user_credentials() + try: + await client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "phone": credentials.get("phone"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +async def test_get_user_by_id_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client().get_user_by_id("invalid_id") + + +async def test_update_user_by_id_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client().update_user_by_id( + "invalid_id", {"email": "test@test.com"} + ) + + +async def test_delete_user_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client().delete_user("invalid_id") + + +async def test_list_factors_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client()._list_factors({"user_id": "invalid_id"}) + + +async def test_delete_factor_invalid_id_raises_error(): + # invalid user id + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client()._delete_factor( + {"user_id": "invalid_id", "id": "invalid_id"} + ) + + # valid user id, invalid factor id + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + await service_role_api_client()._delete_factor( + {"user_id": str(uuid.uuid4()), "id": "invalid_id"} + ) diff --git a/src/auth/tests/_async/test_utils.py b/src/auth/tests/_async/test_utils.py new file mode 100644 index 00000000..f5a144c4 --- /dev/null +++ b/src/auth/tests/_async/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") + + +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_app_metadata() + assert app_metadata + assert app_metadata.get("roles") diff --git a/src/auth/tests/_async/utils.py b/src/auth/tests/_async/utils.py new file mode 100644 index 00000000..1f648197 --- /dev/null +++ b/src/auth/tests/_async/utils.py @@ -0,0 +1,82 @@ +from random import random +from time import time +from typing import Optional + +from faker import Faker +from jwt import encode +from typing_extensions import NotRequired, TypedDict + +from supabase_auth.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[Optional[str]] + phone: NotRequired[Optional[str]] + password: NotRequired[Optional[str]] + + +class Credentials(TypedDict): + email: str + phone: str + password: str + + +def mock_user_credentials( + options: OptionalCredentials = {}, +) -> Credentials: + fake = Faker() + rand_numbers = str(int(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"], + } + + +async def create_new_user_with_email( + *, + email: Optional[str] = None, + password: Optional[str] = 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/src/auth/tests/_sync/__init__.py b/src/auth/tests/_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/auth/tests/_sync/clients.py b/src/auth/tests/_sync/clients.py new file mode 100644 index 00000000..3fee59d1 --- /dev/null +++ b/src/auth/tests/_sync/clients.py @@ -0,0 +1,140 @@ +from jwt import encode + +from supabase_auth import SyncGoTrueAdminAPI, SyncGoTrueClient + +SIGNUP_ENABLED_AUTO_CONFIRM_OFF_PORT = 9999 +SIGNUP_ENABLED_AUTO_CONFIRM_ON_PORT = 9998 +SIGNUP_DISABLED_AUTO_CONFIRM_OFF_PORT = 9997 +SIGNUP_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON_PORT = 9996 + +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_ENABLED_ASYMMETRIC_AUTO_CONFIRM_ON = ( + f"http://localhost:{SIGNUP_ENABLED_ASYMMETRIC_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_client_with_asymmetric_session() -> SyncGoTrueClient: + return SyncGoTrueClient( + url=GOTRUE_URL_SIGNUP_ENABLED_ASYMMETRIC_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/src/auth/tests/_sync/test_gotrue.py b/src/auth/tests/_sync/test_gotrue.py new file mode 100644 index 00000000..1e76c61b --- /dev/null +++ b/src/auth/tests/_sync/test_gotrue.py @@ -0,0 +1,905 @@ +import time +import unittest +from uuid import uuid4 + +import pytest +from jwt import encode + +from supabase_auth.errors import ( + AuthApiError, + AuthInvalidJwtError, + AuthSessionMissingError, +) +from supabase_auth.helpers import decode_jwt + +from .clients import ( + GOTRUE_JWT_SECRET, + auth_client, + auth_client_with_asymmetric_session, + auth_client_with_session, +) +from .utils import mock_user_credentials + + +def test_get_claims_returns_none_when_session_is_none(): + claims = auth_client().get_claims() + assert claims is None + + +def test_get_claims_calls_get_user_if_symmetric_jwt(mocker): + client = auth_client() + spy = mocker.spy(client, "get_user") + + user = (client.sign_up(mock_user_credentials())).user + assert user is not None + + claims = (client.get_claims())["claims"] + assert claims["email"] == user.email + spy.assert_called_once() + + +def test_get_claims_fetches_jwks_to_verify_asymmetric_jwt(mocker): + client = auth_client_with_asymmetric_session() + + user = (client.sign_up(mock_user_credentials())).user + assert user is not None + + spy = mocker.spy(client, "_request") + + claims = (client.get_claims())["claims"] + assert claims["email"] == user.email + + spy.assert_called_once() + spy.assert_called_with("GET", ".well-known/jwks.json", xform=unittest.mock.ANY) + + expected_keyid = "638c54b8-28c2-4b12-9598-ba12ef610a29" + + assert len(client._jwks["keys"]) == 1 + assert client._jwks["keys"][0]["kid"] == expected_keyid + + +def test_jwks_ttl_cache_behavior(mocker): + client = auth_client_with_asymmetric_session() + + spy = mocker.spy(client, "_request") + + # First call should fetch JWKS from endpoint + user = (client.sign_up(mock_user_credentials())).user + assert user is not None + + client.get_claims() + spy.assert_called_with("GET", ".well-known/jwks.json", xform=unittest.mock.ANY) + first_call_count = spy.call_count + + # Second call within TTL should use cache + client.get_claims() + assert spy.call_count == first_call_count # No additional JWKS request + + # Mock time to be after TTL expiry + original_time = time.time + try: + mock_time = mocker.patch("time.time") + mock_time.return_value = original_time() + 601 # TTL is 600 seconds + + # Call after TTL expiry should fetch fresh JWKS + client.get_claims() + assert spy.call_count == first_call_count + 1 # One more JWKS request + finally: + # Restore original time function + mocker.patch("time.time", original_time) + + +def test_set_session_with_valid_tokens(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the tokens from the signup response + access_token = signup_response.session.access_token + refresh_token = signup_response.session.refresh_token + + # Clear the session + client._remove_session() + + # Set the session with the tokens + response = client.set_session(access_token, refresh_token) + + # Verify the response + assert response.session is not None + assert response.session.access_token == access_token + assert response.session.refresh_token == refresh_token + assert response.user is not None + assert response.user.email == credentials.get("email") + + +def test_set_session_with_expired_token(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the tokens from the signup response + access_token = signup_response.session.access_token + refresh_token = signup_response.session.refresh_token + + # Clear the session + client._remove_session() + + # Create an expired token by modifying the JWT + expired_token = access_token.split(".") + payload = decode_jwt(access_token)["payload"] + payload["exp"] = int(time.time()) - 3600 # Set expiry to 1 hour ago + expired_token[1] = encode(payload, GOTRUE_JWT_SECRET, algorithm="HS256").split(".")[ + 1 + ] + expired_access_token = ".".join(expired_token) + + # Set the session with the expired token + response = client.set_session(expired_access_token, refresh_token) + + # Verify the response has a new access token (refreshed) + assert response.session is not None + assert response.session.access_token != expired_access_token + assert response.session.refresh_token != refresh_token + assert response.user is not None + assert response.user.email == credentials.get("email") + + +def test_set_session_without_refresh_token(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get valid tokens + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Get the access token from the signup response + access_token = signup_response.session.access_token + + # Clear the session + client._remove_session() + + # Create an expired token + expired_token = access_token.split(".") + payload = decode_jwt(access_token)["payload"] + payload["exp"] = int(time.time()) - 3600 # Set expiry to 1 hour ago + expired_token[1] = encode(payload, GOTRUE_JWT_SECRET, algorithm="HS256").split(".")[ + 1 + ] + expired_access_token = ".".join(expired_token) + + # Try to set the session with an expired token but no refresh token + with pytest.raises(AuthSessionMissingError): + client.set_session(expired_access_token, "") + + +def test_set_session_with_invalid_token(): + client = auth_client() + + # Try to set the session with invalid tokens + with pytest.raises(AuthInvalidJwtError): + client.set_session("invalid.token.here", "invalid_refresh_token") + + +def test_mfa_enroll(): + client = auth_client_with_session() + + credentials = mock_user_credentials() + + # First sign up to get a valid session + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + # Test MFA enrollment + enroll_response = client.mfa.enroll( + {"issuer": "test-issuer", "factor_type": "totp", "friendly_name": "test-factor"} + ) + + assert enroll_response.id is not None + assert enroll_response.type == "totp" + assert enroll_response.friendly_name == "test-factor" + assert enroll_response.totp.qr_code is not None + + +def test_mfa_challenge(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + enroll_response = client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA challenge + challenge_response = client.mfa.challenge({"factor_id": enroll_response.id}) + assert challenge_response.id is not None + assert challenge_response.expires_at is not None + + +def test_mfa_unenroll(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + enroll_response = client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA unenroll + unenroll_response = client.mfa.unenroll({"factor_id": enroll_response.id}) + assert unenroll_response.id == enroll_response.id + + +def test_mfa_list_factors(): + client = auth_client() + credentials = mock_user_credentials() + + # First sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Enroll a factor first + client.mfa.enroll( + {"factor_type": "totp", "issuer": "test-issuer", "friendly_name": "test-factor"} + ) + + # Test MFA list factors + list_response = client.mfa.list_factors() + assert len(list_response.all) == 1 + + +def test_initialize_from_url(): + # This test verifies the URL format detection and initialization from URL + client = auth_client() + + # First we'll test the _is_implicit_grant_flow method + # The method checks for access_token or error_description in the query string, not the fragment + url_with_token = "http://example.com/?access_token=test_token&other=value" + assert client._is_implicit_grant_flow(url_with_token) == True + + url_with_error = "http://example.com/?error_description=test_error&other=value" + assert client._is_implicit_grant_flow(url_with_error) == True + + url_without_token = "http://example.com/?other=value" + assert client._is_implicit_grant_flow(url_without_token) == False + + # Now test actual URL initialization with a valid URL containing auth tokens + from unittest.mock import patch + + from supabase_auth.types import Session, User, UserResponse + + # Create a mock user and session to avoid actual API calls + mock_user = User( + id="user123", + email="test@example.com", + app_metadata={}, + user_metadata={}, + aud="authenticated", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="authenticated", + updated_at="2023-01-01T00:00:00Z", + ) + + # Wrap the user in a UserResponse as that's what get_user returns + mock_user_response = UserResponse(user=mock_user) + + # Test successful initialization with tokens in URL + good_url = "http://example.com/?access_token=mock_access_token&refresh_token=mock_refresh_token&expires_in=3600&token_type=bearer" + + # We need to mock: + # 1. get_user which is called by _get_session_from_url to validate the token + # 2. _save_session which is called to store the session data + # 3. _notify_all_subscribers which is called to notify about sign-in + with patch.object(client, "get_user") as mock_get_user: + mock_get_user.return_value = mock_user_response + + with patch.object(client, "_save_session") as mock_save_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call initialize_from_url with the good URL + result = client.initialize_from_url(good_url) + + # Verify get_user was called with the access token + mock_get_user.assert_called_once_with("mock_access_token") + + # Verify _save_session was called with a Session object + mock_save_session.assert_called_once() + session_arg = mock_save_session.call_args[0][0] + assert isinstance(session_arg, Session) + assert session_arg.access_token == "mock_access_token" + assert session_arg.refresh_token == "mock_refresh_token" + assert session_arg.expires_in == 3600 + + # Verify _notify_all_subscribers was called + mock_notify.assert_called_with("SIGNED_IN", session_arg) + + assert result is None # initialize_from_url doesn't have a return value + + # Test URL with error - need to include error_code for the test to work correctly + error_url = "http://example.com/?error=invalid_request&error_description=Invalid+request&error_code=400" + + # Should throw an error when URL contains error parameters + from supabase_auth.errors import AuthImplicitGrantRedirectError + + try: + client.initialize_from_url(error_url) + assert False, "Expected AuthImplicitGrantRedirectError" + except AuthImplicitGrantRedirectError as e: + # The error message includes the error_description value + assert "Invalid request" in str(e) + + # Test URL with code for PKCE flow + code_url = "http://example.com/?code=authorization_code" + + # For the code URL path, we're not testing it here since it requires more mocking + # and is indirectly tested via other tests like exchange_code_for_session + + # Test URL with neither tokens nor code - should not throw but also not call anything + invalid_url = "http://example.com/?foo=bar" + with patch.object(client, "_get_session_from_url") as mock_get_session: + result = client.initialize_from_url(invalid_url) + mock_get_session.assert_not_called() + assert result is None + + +def test_exchange_code_for_session(): + client = auth_client() + + # We'll test the flow type setting instead of the actual exchange, since the + # actual exchange requires a live OAuth flow which isn't practical in tests + assert client._flow_type in ["implicit", "pkce"] + + # This part would normally need a live OAuth flow, so we verify the logic paths + # Get the storage key for PKCE flow + storage_key = f"{client._storage_key}-code-verifier" + + # Set the flow type to pkce + client._flow_type = "pkce" + + # Test the PKCE URL generation which is needed for exchange_code_for_session + provider = "github" + url, params = client._get_url_for_provider(f"{client._url}/authorize", provider, {}) + + # Verify PKCE parameters were added + assert "code_challenge" in params + assert "code_challenge_method" in params + + # Verify the code verifier was stored + code_verifier = client._storage.get_item(storage_key) + assert code_verifier is not None + + +def test_get_authenticator_assurance_level(): + client = auth_client() + credentials = mock_user_credentials() + + # Without a session, should return null values + aal_response = client.mfa.get_authenticator_assurance_level() + assert aal_response.current_level is None + assert aal_response.next_level is None + assert aal_response.current_authentication_methods == [] + + # Sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # With a session, should return authentication methods + aal_response = client.mfa.get_authenticator_assurance_level() + # Basic auth will have password as an authentication method + assert aal_response.current_authentication_methods is not None + + +def test_link_identity(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + from unittest.mock import patch + + from supabase_auth.types import OAuthResponse + + # Since the test server has manual linking disabled, we'll mock the URL generation + with patch.object(client, "_get_url_for_provider") as mock_url_provider: + mock_url = "http://example.com/authorize?provider=github" + mock_params = {"provider": "github"} + mock_url_provider.return_value = (mock_url, mock_params) + + # Also mock the _request method since the server would reject it + with patch.object(client, "_request") as mock_request: + mock_request.return_value = OAuthResponse(provider="github", url=mock_url) + + # Call the method + response = client.link_identity({"provider": "github"}) + + # Verify the response + assert response.provider == "github" + assert response.url == mock_url + + +def test_get_user_identities(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # New users won't have any identities yet, but the call should work + identities_response = client.get_user_identities() + assert identities_response is not None + # For a new user, identities will be an empty list or None + assert hasattr(identities_response, "identities") + + +def test_unlink_identity(): + client = auth_client() + credentials = mock_user_credentials() + + # Sign up to get a valid session + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Mock a UserIdentity to test unlink_identity + from unittest.mock import patch + + from supabase_auth.types import UserIdentity + + # Create a mock identity + mock_identity = UserIdentity( + id="user-id", + identity_id="identity-id-1", + user_id="user-id", + identity_data={"email": "user@example.com"}, + provider="github", + created_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + updated_at="2023-01-01T00:00:00Z", + ) + + # Mock the _request method since we can't actually unlink an identity that doesn't exist + with patch.object(client, "_request") as mock_request: + mock_request.return_value = None + + # Call the method + client.unlink_identity(mock_identity) + + # Verify the request was made properly + mock_request.assert_called_once_with( + "DELETE", + "user/identities/identity-id-1", + jwt=signup_response.session.access_token, + ) + + # Test error case: no session + with patch.object(client, "get_session") as mock_get_session: + from supabase_auth.errors import AuthSessionMissingError + + mock_get_session.return_value = None + + try: + client.unlink_identity(mock_identity) + assert False, "Expected AuthSessionMissingError" + except AuthSessionMissingError: + pass + + +def test_verify_otp(): + client = auth_client() + + # Mock the _request method since we can't actually verify an OTP in the test + import time + from unittest.mock import patch + + from supabase_auth.types import AuthResponse, Session, User + + mock_user = User( + id="test-user-id", + app_metadata={}, + user_metadata={}, + aud="test-aud", + email="test@example.com", + phone="", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="", + updated_at="2023-01-01T00:00:00Z", + ) + + mock_session = Session( + access_token="mock-access-token", + refresh_token="mock-refresh-token", + expires_in=3600, + expires_at=round(time.time()) + 3600, + token_type="bearer", + user=mock_user, + ) + + mock_response = AuthResponse(session=mock_session, user=mock_user) + + with patch.object(client, "_request") as mock_request: + # Configure the mock to return a predefined response + mock_request.return_value = mock_response + + # Also patch _save_session to avoid actual storage interactions + with patch.object(client, "_save_session") as mock_save: + # Call verify_otp with test parameters + params = { + "type": "sms", + "phone": "+11234567890", + "token": "123456", + "options": {"redirect_to": "https://example.com/callback"}, + } + + response = client.verify_otp(params) + + # Verify the request was made with correct parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" # method + assert args[1] == "verify" # path + assert kwargs["body"]["phone"] == "+11234567890" + assert kwargs["body"]["token"] == "123456" + assert kwargs["redirect_to"] == "https://example.com/callback" + + # Verify the session was saved + mock_save.assert_called_once_with(mock_session) + + # Verify the response + assert response == mock_response + + +def test_sign_in_with_password(): + client = auth_client() + credentials = mock_user_credentials() + from supabase_auth.errors import AuthApiError, AuthInvalidCredentialsError + + # First create a user we can sign in with + signup_response = client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert signup_response.session is not None + + # Test signing in with the same credentials (email) + signin_response = client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + # Verify the response has a valid session and user + assert signin_response.session is not None + assert signin_response.user is not None + assert signin_response.user.email == credentials.get("email") + + # Test error case: wrong password + + # We need to create a custom client to avoid affecting other tests + test_client = auth_client() + + try: + test_client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "wrong_password", + } + ) + assert False, "Expected AuthApiError for wrong password" + except AuthApiError: + pass + + # Test error case: missing credentials + try: + test_client.sign_in_with_password({}) + assert False, "Expected AuthInvalidCredentialsError for missing credentials" + except AuthInvalidCredentialsError: + pass + + +def test_sign_in_with_otp(): + client = auth_client() + + # Test with email OTP + email = f"test-{uuid4()}@example.com" + + # When sign_in_with_otp is called with valid email, it should return a AuthOtpResponse + # We can't fully test the actual OTP flow since that requires email verification + from unittest.mock import patch + + from supabase_auth.types import AuthOtpResponse + + # First test for email OTP + with patch.object(client, "_request") as mock_request: + mock_response = AuthOtpResponse( + message_id="mock-message-id", email=email, phone=None, hash=None + ) + mock_request.return_value = mock_response + + response = client.sign_in_with_otp( + { + "email": email, + "options": { + "email_redirect_to": "https://example.com/callback", + "should_create_user": True, + "data": {"custom": "data"}, + "captcha_token": "mock-captcha-token", + }, + } + ) + + # Verify request parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" + assert args[1] == "otp" + assert kwargs["body"]["email"] == email + assert kwargs["body"]["create_user"] == True + assert kwargs["body"]["data"] == {"custom": "data"} + assert ( + kwargs["body"]["gotrue_meta_security"]["captcha_token"] + == "mock-captcha-token" + ) + assert kwargs["redirect_to"] == "https://example.com/callback" + + # Verify response + assert response == mock_response + + # Test with phone OTP + phone = "+11234567890" + + with patch.object(client, "_request") as mock_request: + mock_response = AuthOtpResponse( + message_id="mock-message-id", email=None, phone=phone, hash=None + ) + mock_request.return_value = mock_response + + response = client.sign_in_with_otp( + { + "phone": phone, + "options": { + "should_create_user": True, + "data": {"custom": "data"}, + "channel": "whatsapp", # Test alternate channel + "captcha_token": "mock-captcha-token", + }, + } + ) + + # Verify request parameters + mock_request.assert_called_once() + args, kwargs = mock_request.call_args + assert args[0] == "POST" + assert args[1] == "otp" + assert kwargs["body"]["phone"] == phone + assert kwargs["body"]["create_user"] == True + assert kwargs["body"]["data"] == {"custom": "data"} + assert kwargs["body"]["channel"] == "whatsapp" + assert ( + kwargs["body"]["gotrue_meta_security"]["captcha_token"] + == "mock-captcha-token" + ) + assert kwargs.get("redirect_to") is None # No redirect for phone + + # Verify response + assert response == mock_response + + # Test with invalid parameters (missing both email and phone) + from supabase_auth.errors import AuthInvalidCredentialsError + + try: + client.sign_in_with_otp({}) + assert False, "Expected AuthInvalidCredentialsError" + except AuthInvalidCredentialsError: + pass + + +def test_sign_out(): + from unittest.mock import patch + + from supabase_auth.types import Session, User + + client = auth_client() + + # Create a mock user and session + mock_user = User( + id="user123", + email="test@example.com", + app_metadata={}, + user_metadata={}, + aud="authenticated", + created_at="2023-01-01T00:00:00Z", + confirmed_at="2023-01-01T00:00:00Z", + last_sign_in_at="2023-01-01T00:00:00Z", + role="authenticated", + updated_at="2023-01-01T00:00:00Z", + ) + + mock_session = Session( + access_token="mock_access_token", + refresh_token="mock_refresh_token", + expires_in=3600, + token_type="bearer", + user=mock_user, + ) + + # Test sign_out with "global" scope (default) + # This should call admin.sign_out, _remove_session, and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope (global) + client.sign_out() + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "global" + ) + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test sign_out with "local" scope + # Should behave the same as "global" for client-side + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with local scope + client.sign_out({"scope": "local"}) + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "local" + ) + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test sign_out with "others" scope + # This should only call admin.sign_out but not _remove_session or _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with others scope + client.sign_out({"scope": "others"}) + + # Verify that admin.sign_out was called with correct parameters + mock_admin_sign_out.assert_called_once_with( + "mock_access_token", "others" + ) + + # Verify that _remove_session was NOT called + mock_remove_session.assert_not_called() + + # Verify that _notify_all_subscribers was NOT called + mock_notify.assert_not_called() + + # Test sign_out with no session + # This should not call admin.sign_out but still call _remove_session and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = None + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope + client.sign_out() + + # Verify that admin.sign_out was NOT called + mock_admin_sign_out.assert_not_called() + + # Verify that _remove_session was called + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was called with SIGNED_OUT + mock_notify.assert_called_once_with("SIGNED_OUT", None) + + # Test when admin.sign_out raises an error + # This should suppress the error and continue with _remove_session and _notify_all_subscribers + with patch.object(client, "get_session") as mock_get_session: + mock_get_session.return_value = mock_session + + with patch.object(client.admin, "sign_out") as mock_admin_sign_out: + mock_admin_sign_out.side_effect = AuthApiError( + "Test error", 401, "auth_error" + ) + + with patch.object(client, "_remove_session") as mock_remove_session: + with patch.object(client, "_notify_all_subscribers") as mock_notify: + # Call sign_out with default scope + client.sign_out() + + # Verify that _remove_session was still called despite the error + mock_remove_session.assert_called_once() + + # Verify that _notify_all_subscribers was still called despite the error + mock_notify.assert_called_once_with("SIGNED_OUT", None) diff --git a/src/auth/tests/_sync/test_gotrue_admin_api.py b/src/auth/tests/_sync/test_gotrue_admin_api.py new file mode 100644 index 00000000..e179fe2b --- /dev/null +++ b/src/auth/tests/_sync/test_gotrue_admin_api.py @@ -0,0 +1,641 @@ +import uuid + +import pytest + +from supabase_auth.errors import ( + AuthApiError, + AuthError, + AuthInvalidCredentialsError, + AuthSessionMissingError, + AuthWeakPasswordError, +) + +from .clients import ( + auth_client, + 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, +) + + +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_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 + + +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_invalid_credential_sign_in_with_phone(): + try: + response = ( + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "phone": "+123456789", + "password": "strong_pwd", + } + ) + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_invalid_credential_sign_in_with_email(): + try: + response = ( + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_password( + { + "email": "unknown_user@unknowndomain.com", + "password": "strong_pwd", + } + ) + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_sign_in_with_otp_email(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "email": "unknown_user@unknowndomain.com", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_sign_in_with_otp_phone(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_otp( + { + "phone": "+112345678", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_resend(): + try: + client_api_auto_confirm_off_signups_enabled_client().resend( + {"phone": "+112345678", "type": "sms"} + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_reauthenticate(): + try: + response = auth_client_with_session().reauthenticate() + except AuthSessionMissingError: + pass + + +def test_refresh_session(): + try: + response = auth_client_with_session().refresh_session() + except AuthSessionMissingError: + pass + + +def test_reset_password_for_email(): + credentials = mock_user_credentials() + try: + response = auth_client_with_session().reset_password_email( + email=credentials.get("email") + ) + except AuthSessionMissingError: + pass + + +def test_resend_missing_credentials(): + try: + client_api_auto_confirm_off_signups_enabled_client().resend( + {"type": "email_change"} + ) + except AuthInvalidCredentialsError as e: + assert e.to_dict() + + +def test_sign_in_anonymously(): + try: + response = auth_client_with_session().sign_in_anonymously() + assert response + except AuthApiError: + pass + + +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 == "Token has expired or is invalid" + + +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 (E.164 required)" + + +def test_sign_in_with_id_token(): + try: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_id_token( + { + "provider": "google", + "token": "123456", + } + ) + except AuthApiError as e: + assert e.to_dict() + + +def test_sign_in_with_sso(): + with pytest.raises(AuthApiError, match=r"SAML 2.0 is disabled") as exc: + client_api_auto_confirm_off_signups_enabled_client().sign_in_with_sso( + { + "domain": "google", + } + ) + assert exc.value is not None + + +def test_sign_in_with_oauth(): + assert client_api_auto_confirm_off_signups_enabled_client().sign_in_with_oauth( + { + "provider": "google", + } + ) + + +def test_link_identity_missing_session(): + with pytest.raises(AuthSessionMissingError) as exc: + client_api_auto_confirm_off_signups_enabled_client().link_identity( + { + "provider": "google", + } + ) + assert exc.value is not None + + +def test_get_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert client._storage.get_item(client._storage_key) is not None + + +def test_remove_item_from_memory_storage(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client._storage.remove_item(client._storage_key) + assert client._storage_key not in client._storage.storage + + +def test_list_factors(): + credentials = mock_user_credentials() + client = auth_client() + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + factors = client._list_factors() + assert factors + assert isinstance(factors.totp, list) and isinstance(factors.phone, list) + + +def test_start_auto_refresh_token(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + assert client._start_auto_refresh_token(2.0) is None + + +def test_recover_and_refresh(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client._recover_and_refresh() + assert client._storage_key in client._storage.storage + + +def test_get_user_identities(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + assert (client.get_user_identities()).identities[0].identity_data[ + "email" + ] == credentials.get("email") + + +def test_update_user(): + credentials = mock_user_credentials() + client = auth_client() + client._auto_refresh_token = True + client.sign_up( + { + "email": credentials.get("email"), + "password": credentials.get("password"), + } + ) + client.update_user({"password": "123e5a"}) + client.sign_in_with_password( + { + "email": credentials.get("email"), + "password": "123e5a", + } + ) + + +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_weak_email_password_error(): + credentials = mock_user_credentials() + try: + client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "email": credentials.get("email"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +def test_weak_phone_password_error(): + credentials = mock_user_credentials() + try: + client_api_auto_confirm_off_signups_enabled_client().sign_up( + { + "phone": credentials.get("phone"), + "password": "123", + } + ) + except (AuthWeakPasswordError, AuthApiError) as e: + assert e.to_dict() + + +def test_get_user_by_id_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client().get_user_by_id("invalid_id") + + +def test_update_user_by_id_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client().update_user_by_id( + "invalid_id", {"email": "test@test.com"} + ) + + +def test_delete_user_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client().delete_user("invalid_id") + + +def test_list_factors_invalid_id_raises_error(): + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client()._list_factors({"user_id": "invalid_id"}) + + +def test_delete_factor_invalid_id_raises_error(): + # invalid user id + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client()._delete_factor( + {"user_id": "invalid_id", "id": "invalid_id"} + ) + + # valid user id, invalid factor id + with pytest.raises( + ValueError, match=r"Invalid id, 'invalid_id' is not a valid uuid" + ): + service_role_api_client()._delete_factor( + {"user_id": str(uuid.uuid4()), "id": "invalid_id"} + ) diff --git a/src/auth/tests/_sync/test_utils.py b/src/auth/tests/_sync/test_utils.py new file mode 100644 index 00000000..23b4ac9c --- /dev/null +++ b/src/auth/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/src/auth/tests/_sync/utils.py b/src/auth/tests/_sync/utils.py new file mode 100644 index 00000000..2cc31de2 --- /dev/null +++ b/src/auth/tests/_sync/utils.py @@ -0,0 +1,82 @@ +from random import random +from time import time +from typing import Optional + +from faker import Faker +from jwt import encode +from typing_extensions import NotRequired, TypedDict + +from supabase_auth.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[Optional[str]] + phone: NotRequired[Optional[str]] + password: NotRequired[Optional[str]] + + +class Credentials(TypedDict): + email: str + phone: str + password: str + + +def mock_user_credentials( + options: OptionalCredentials = {}, +) -> Credentials: + fake = Faker() + rand_numbers = str(int(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: Optional[str] = None, + password: Optional[str] = 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 diff --git a/src/auth/tests/conftest.py b/src/auth/tests/conftest.py new file mode 100644 index 00000000..79b61d44 --- /dev/null +++ b/src/auth/tests/conftest.py @@ -0,0 +1,51 @@ +from typing import Dict, Tuple + +import pytest + +# store history of failures per test class name and per index +# in parametrize (if parametrize used) +_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {} + + +def pytest_runtest_makereport(item, call): + if "incremental" in item.keywords: + # incremental marker is used + if call.excinfo is not None: + # the test has failed + # retrieve the class name of the test + cls_name = str(item.cls) + # retrieve the index of the test (if parametrize is used + # in combination with incremental) + parametrize_index = ( + tuple(item.callspec.indices.values()) + if hasattr(item, "callspec") + else () + ) + # retrieve the name of the test function + test_name = item.originalname or item.name + # store in _test_failed_incremental the original name of the failed test + _test_failed_incremental.setdefault(cls_name, {}).setdefault( + parametrize_index, test_name + ) + + +def pytest_runtest_setup(item): + if "incremental" in item.keywords: + # retrieve the class name of the test + cls_name = str(item.cls) + # check if a previous test has failed for this class + if cls_name in _test_failed_incremental: + # retrieve the index of the test (if parametrize is used + # in combination with incremental) + parametrize_index = ( + tuple(item.callspec.indices.values()) + if hasattr(item, "callspec") + else () + ) + # retrieve the name of the first test function to + # fail for this class name and index + test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) + # if name found, test has failed for the combination of + # class name & test name + if test_name is not None: + pytest.xfail(f"previous test failed ({test_name})") diff --git a/src/auth/tests/test_helpers.py b/src/auth/tests/test_helpers.py new file mode 100644 index 00000000..de85baf7 --- /dev/null +++ b/src/auth/tests/test_helpers.py @@ -0,0 +1,619 @@ +from datetime import datetime +from unittest.mock import MagicMock, patch + +import httpx +import pytest +import respx +from httpx import Headers, HTTPStatusError, Response +from pydantic import BaseModel + +from supabase_auth.constants import ( + API_VERSION_HEADER_NAME, +) +from supabase_auth.errors import ( + AuthApiError, + AuthInvalidJwtError, + AuthRetryableError, + AuthUnknownError, + AuthWeakPasswordError, +) +from supabase_auth.helpers import ( + decode_jwt, + generate_pkce_challenge, + generate_pkce_verifier, + get_error_code, + handle_exception, + model_dump, + model_dump_json, + model_validate, + parse_auth_response, + parse_jwks, + parse_link_identity_response, + parse_link_response, + parse_response_api_version, + parse_sso_response, + parse_user_response, + validate_exp, +) +from supabase_auth.types import ( + GenerateLinkResponse, + Session, + User, +) + +from ._sync.utils import mock_access_token + +TEST_URL = "http://localhost" + + +def test_handle_exception_with_api_version_and_error_code(): + err = { + "name": "without API version and error code", + "code": "error_code", + "ename": "AuthApiError", + } + + with respx.mock: + respx.get(f"{TEST_URL}/hello-world").mock( + return_value=Response(status_code=200), + side_effect=AuthApiError("Error code message", 400, "error_code"), + ) + with pytest.raises(AuthApiError, match=r"Error code message") as exc: + httpx.get(f"{TEST_URL}/hello-world") + assert exc.value is not None + assert exc.value.message == "Error code message" + assert exc.value.code == err["code"] + assert exc.value.name == err["ename"] + + +def test_handle_exception_without_api_version_and_weak_password_error_code(): + err = { + "name": "without API version and weak password error code with payload", + "code": "weak_password", + "ename": "AuthWeakPasswordError", + } + + with respx.mock: + respx.get(f"{TEST_URL}/hello-world").mock( + return_value=Response(status_code=200), + side_effect=AuthWeakPasswordError( + "Error code message", 400, ["characters"] + ), + ) + with pytest.raises(AuthWeakPasswordError, match=r"Error code message") as exc: + httpx.get(f"{TEST_URL}/hello-world") + assert exc.value is not None + assert exc.value.message == "Error code message" + assert exc.value.code == err["code"] + assert exc.value.name == err["ename"] + + +def test_handle_exception_with_api_version_2024_01_01_and_error_code(): + err = { + "name": "with API version 2024-01-01 and error code", + "code": "error_code", + "ename": "AuthApiError", + } + + with respx.mock: + respx.get(f"{TEST_URL}/hello-world").mock( + return_value=Response(status_code=200), + side_effect=AuthApiError("Error code message", 400, "error_code"), + ) + with pytest.raises(AuthApiError, match=r"Error code message") as exc: + httpx.get(f"{TEST_URL}/hello-world") + assert exc.value is not None + assert exc.value.message == "Error code message" + assert exc.value.code == err["code"] + assert exc.value.name == err["ename"] + + +def test_parse_response_api_version_with_valid_date(): + headers = Headers({API_VERSION_HEADER_NAME: "2024-01-01"}) + response = Response(headers=headers, status_code=200) + api_ver = parse_response_api_version(response) + assert datetime.timestamp(api_ver) == datetime.timestamp( + datetime.strptime("2024-01-01", "%Y-%m-%d") + ) + + +def test_parse_response_api_version_with_invalid_dates(): + dates = ["2024-01-32", "", "notadate", "Sat Feb 24 2024 17:59:17 GMT+0100"] + for date in dates: + headers = Headers({API_VERSION_HEADER_NAME: date}) + response = Response(headers=headers, status_code=200) + api_ver = parse_response_api_version(response) + assert api_ver is None + + +def test_parse_link_identity_response(): + assert parse_link_identity_response({"url": f"{TEST_URL}/hello-world"}) + + +def test_get_error_code(): + assert get_error_code({}) is None + assert get_error_code({"error_code": "500"}) == "500" + + +def test_decode_jwt(): + assert decode_jwt(mock_access_token()) + + with pytest.raises(AuthInvalidJwtError, match=r"Invalid JWT structure") as exc: + decode_jwt("non-valid-jwt") + assert exc.value is not None + + +def test_generate_pkce_verifier(): + assert isinstance(generate_pkce_verifier(45), str) + with pytest.raises( + ValueError, match=r"PKCE verifier length must be between 43 and 128 characters" + ) as exc: + generate_pkce_verifier(42) + assert exc.value is not None + + +def test_generate_pkce_challenge(): + pkce = generate_pkce_verifier(45) + assert isinstance(generate_pkce_challenge(pkce), str) + + +def test_parse_response_api_version_invalid_date(): + mock_response = MagicMock(spec=Response) + mock_response.headers = {API_VERSION_HEADER_NAME: "2023-02-30"} # Invalid date + + result = parse_response_api_version(mock_response) + assert result is None + + +# Test for pydantic v1 compatibility in model_validate +def test_model_validate_pydantic_v1(): + # We need to patch the actual calls inside the function + with patch("supabase_auth.helpers.TBaseModel") as MockType: + # Mock the behavior of the try block to raise AttributeError + mock_model = MagicMock() + mock_model.model_validate.side_effect = AttributeError + mock_model.parse_obj.return_value = "parsed_obj_result" + + # Use the patched model in the actual function + result = model_validate(mock_model, {"test": "data"}) + + # Check that parse_obj was called + mock_model.parse_obj.assert_called_once_with({"test": "data"}) + assert result == "parsed_obj_result" + + +# Test for pydantic v1 compatibility in model_dump +def test_model_dump_pydantic_v1(): + # Create a mock model with necessary behavior + mock_model = MagicMock(spec=BaseModel) + mock_model.model_dump.side_effect = AttributeError + mock_model.dict.return_value = {"test": "data"} + + # Call the function + result = model_dump(mock_model) + + # Check the results + assert result == {"test": "data"} + mock_model.dict.assert_called_once() + + +# Test for pydantic v1 compatibility in model_dump_json +def test_model_dump_json_pydantic_v1(): + # Create a mock model with necessary behavior + mock_model = MagicMock(spec=BaseModel) + mock_model.model_dump_json.side_effect = AttributeError + mock_model.json.return_value = '{"test": "data"}' + + # Call the function + result = model_dump_json(mock_model) + + # Check the results + assert result == '{"test": "data"}' + mock_model.json.assert_called_once() + + +# Test for parse_auth_response with a session +def test_parse_auth_response_with_session(): + # Create our own AuthResponse object to avoid pydantic validation issues + mock_session = MagicMock(spec=Session) + mock_user = MagicMock(spec=User) + + # Test data with access_token, refresh_token, and expires_in + data = { + "access_token": "test_access_token", + "refresh_token": "test_refresh_token", + "expires_in": 3600, + "user": { + "id": "user-123", + "email": "test@example.com", + }, + } + + with patch("supabase_auth.helpers.model_validate") as mock_validate: + # First call for Session, second for User + mock_validate.side_effect = [mock_session, mock_user] + + with patch("supabase_auth.helpers.AuthResponse") as mock_auth_response: + mock_auth_response.return_value = "auth_response_result" + + result = parse_auth_response(data) + + # Verify model_validate was called for Session and User + assert mock_validate.call_count == 2 + mock_validate.assert_any_call(Session, data) + mock_validate.assert_any_call(User, data["user"]) + + # Verify AuthResponse was created with correct params + mock_auth_response.assert_called_once_with( + session=mock_session, user=mock_user + ) + assert result == "auth_response_result" + + +# Test for parse_auth_response without a session +def test_parse_auth_response_without_session(): + # Create our own User object to avoid pydantic validation issues + mock_user = MagicMock(spec=User) + + # Test data without session info + data = { + "user": { + "id": "user-123", + "email": "test@example.com", + } + } + + with patch("supabase_auth.helpers.model_validate") as mock_validate: + mock_validate.return_value = mock_user + + with patch("supabase_auth.helpers.AuthResponse") as mock_auth_response: + mock_auth_response.return_value = "auth_response_result" + + result = parse_auth_response(data) + + # Verify model_validate was called only for User + mock_validate.assert_called_once_with(User, data["user"]) + + # Verify AuthResponse was created with correct params + mock_auth_response.assert_called_once_with(session=None, user=mock_user) + assert result == "auth_response_result" + + +# Test for parse_link_response +def test_parse_link_response(): + # Create mocks to avoid pydantic validation issues + mock_user = MagicMock(spec=User) + mock_gen_link_response = MagicMock(spec=GenerateLinkResponse) + + # Test data for link response + data = { + "action_link": "https://example.com/verify", + "email_otp": "123456", + "hashed_token": "abc123", + "redirect_to": "https://example.com/app", + "verification_type": "signup", + "id": "user-123", + "email": "test@example.com", + } + + # We need to patch the GenerateLinkProperties constructor + with patch("supabase_auth.helpers.GenerateLinkProperties") as mock_gen_props: + mock_gen_props.return_value = "mock_properties" + + with patch("supabase_auth.helpers.model_dump") as mock_dump: + mock_dump.return_value = { + "action_link": "https://example.com/verify", + "email_otp": "123456", + "hashed_token": "abc123", + "redirect_to": "https://example.com/app", + "verification_type": "signup", + } + + with patch("supabase_auth.helpers.model_validate") as mock_validate: + mock_validate.return_value = mock_user + + with patch( + "supabase_auth.helpers.GenerateLinkResponse" + ) as mock_gen_link: + mock_gen_link.return_value = mock_gen_link_response + + result = parse_link_response(data) + + # Verify that props were created correctly + mock_gen_props.assert_called_once_with( + 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"), + ) + + # Verify model_validate was called for User with filtered data + mock_validate.assert_called_once() + + # Verify GenerateLinkResponse was created + mock_gen_link.assert_called_once_with( + properties="mock_properties", user=mock_user + ) + assert result == mock_gen_link_response + + +# Test for parse_user_response +def test_parse_user_response_with_user_object(): + # Test data with 'user' key + data = {"user": {"id": "user-123", "email": "test@example.com"}} + + with patch("supabase_auth.helpers.model_validate") as mock_validate: + mock_validate.return_value = "mock_user_response" + + result = parse_user_response(data) + + assert result == "mock_user_response" + mock_validate.assert_called_once() + + +# Test for parse_user_response without user object +def test_parse_user_response_without_user_object(): + # Test data without 'user' key + data = {"id": "user-123", "email": "test@example.com"} + + with patch("supabase_auth.helpers.model_validate") as mock_validate: + mock_validate.return_value = "mock_user_response" + + result = parse_user_response(data) + + assert result == "mock_user_response" + mock_validate.assert_called_once() + # Verify that it wrapped the data in a user object + expected_wrapped_data = {"user": data} + assert mock_validate.call_args[0][1] == expected_wrapped_data + + +# Test for parse_sso_response +def test_parse_sso_response(): + with patch("supabase_auth.helpers.model_validate") as mock_validate: + mock_validate.return_value = "sso_response" + + result = parse_sso_response({"provider": "google"}) + assert result == "sso_response" + + # Verify model_validate was called with correct params + from supabase_auth.types import SSOResponse + + mock_validate.assert_called_once_with(SSOResponse, {"provider": "google"}) + + +# Test for parse_jwks with empty keys +def test_parse_jwks_empty_keys(): + with pytest.raises(AuthInvalidJwtError, match="JWKS is empty"): + parse_jwks({"keys": []}) + + +# Tests for handle_exception +def test_handle_exception_non_http_error(): + # Test case for non-HTTPStatusError + exception = ValueError("Test error") + result = handle_exception(exception) + + assert isinstance(result, AuthRetryableError) + assert result.message == "Test error" + assert result.status == 0 + + +def test_handle_exception_network_error(): + # Test case for network errors (502, 503, 504) + mock_response = MagicMock(spec=Response) + mock_response.status_code = 503 + + exception = HTTPStatusError( + "Network error", request=MagicMock(), response=mock_response + ) + result = handle_exception(exception) + + assert isinstance(result, AuthRetryableError) + assert result.status == 503 + + +def test_handle_exception_with_weak_password_attribute(): + # In the implementation there's a logical error in the code: + # It checks if data.get("weak_password") is BOTH a dict AND a list + # This can never be true. Let's just test the error_code path which works. + + # Test case with error_code=None, so we take the alternate default path + mock_response = MagicMock(spec=Response) + mock_response.status_code = 400 + mock_response.json.return_value = { + "message": "Invalid request", + "error_description": "Something went wrong", + } + + exception = HTTPStatusError("Error", request=MagicMock(), response=mock_response) + + with patch("supabase_auth.helpers.parse_response_api_version", return_value=None): + result = handle_exception(exception) + + # Will return a normal AuthApiError + assert isinstance(result, AuthApiError) + assert result.message == "Invalid request" + assert result.status == 400 + assert result.code is None + + +def test_handle_exception_weak_password_with_error_code(): + # Test case for weak password identified by error_code + mock_response = MagicMock(spec=Response) + mock_response.status_code = 400 + mock_response.json.return_value = { + "message": "Password too weak", + "error_code": "weak_password", + "weak_password": {"reasons": ["Password too simple"]}, + } + + exception = HTTPStatusError( + "Password error", request=MagicMock(), response=mock_response + ) + + with patch("supabase_auth.helpers.parse_response_api_version", return_value=None): + result = handle_exception(exception) + + assert isinstance(result, AuthWeakPasswordError) + assert result.message == "Password too weak" + assert result.status == 400 + assert result.reasons == ["Password too simple"] + + +def test_handle_exception_with_new_api_version(): + # Test case for new API version with "code" field + mock_response = MagicMock(spec=Response) + mock_response.status_code = 400 + mock_response.json.return_value = { + "message": "Password too weak", + "code": "weak_password", + "weak_password": {"reasons": ["Password too simple"]}, + } + + # Mock datetime for January 2, 2024 (after 2024-01-01 API version) + mock_date = datetime(2024, 1, 2) + + exception = HTTPStatusError( + "Password error", request=MagicMock(), response=mock_response + ) + + with patch( + "supabase_auth.helpers.parse_response_api_version", return_value=mock_date + ): + result = handle_exception(exception) + + assert isinstance(result, AuthWeakPasswordError) + assert result.message == "Password too weak" + assert result.status == 400 + + +def test_handle_exception_unknown_error(): + # Test case for when json() raises an exception + mock_response = MagicMock(spec=Response) + mock_response.status_code = 500 + mock_response.json.side_effect = ValueError("Invalid JSON") + + exception = HTTPStatusError( + "Server error", request=MagicMock(), response=mock_response + ) + result = handle_exception(exception) + + assert isinstance(result, AuthUnknownError) + assert "Server error" in result.message + + +# Tests for validate_exp +def test_validate_exp_with_no_exp(): + with pytest.raises(AuthInvalidJwtError, match="JWT has no expiration time"): + validate_exp(None) + + +def test_validate_exp_with_expired_exp(): + # Set expiry to 1 hour ago + exp = int(datetime.now().timestamp()) - 3600 + + with pytest.raises(AuthInvalidJwtError, match="JWT has expired"): + validate_exp(exp) + + +def test_validate_exp_with_valid_exp(): + # Set expiry to 1 hour in the future + exp = int(datetime.now().timestamp()) + 3600 + + # Should not raise an exception + validate_exp(exp) + + +def test_is_http_url(): + from supabase_auth.helpers import is_http_url + + # Test valid HTTP URLs + assert is_http_url("http://example.com") is True + assert is_http_url("https://example.com") is True + assert is_http_url("https://example.com/path?query=value#fragment") is True + + # Test invalid URLs + assert is_http_url("ftp://example.com") is False + assert is_http_url("file:///path/to/file.txt") is False + assert is_http_url("example.com") is False # Missing scheme + assert is_http_url("") is False + assert is_http_url("not a url") is False + + +def test_handle_exception_weak_password_branch(): + """Specifically targeting the unreachable branch in handle_exception with weak_password. + + This test attempts to test the branch where weak_password needs to be both a dict and a list, + which is logically impossible, so we'll test it by mocking the implementation details. + """ + import httpx + + from supabase_auth.errors import AuthWeakPasswordError + from supabase_auth.helpers import handle_exception + + # Create a proper mock Response with headers + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 400 + mock_response.headers = {} + + # Create a special mock dict that pretends to be both a dict and a list + class WeirdDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.reasons = ["Password too short"] + + # Mock json response with our special dict + mock_response.json.return_value = { + "message": "Password too weak", + "weak_password": {"reasons": ["Password too short"]}, + } + + # Create a proper HTTPStatusError + exception = httpx.HTTPStatusError( + "Password error", request=MagicMock(spec=httpx.Request), response=mock_response + ) + + # We need to directly target the specific branch handling weak passwords + # First, we need to monkey patch the implementation temporarily to reach our branch + original_isinstance = isinstance + + def patched_isinstance(obj, cls): + # Make weak_password appear as both dict and list when needed + if obj == mock_response.json()["weak_password"] and cls in (dict, list): + return True + return original_isinstance(obj, cls) + + with ( + patch("supabase_auth.helpers.isinstance", side_effect=patched_isinstance), + patch("supabase_auth.helpers.len", return_value=1), + ): + result = handle_exception(exception) + + # Check if our test coverage reached the AuthWeakPasswordError branch + assert isinstance(result, AuthWeakPasswordError) + assert result.message == "Password too weak" + assert result.status == 400 + + +def test_parse_auth_otp_response(): + """Test for the parse_auth_otp_response function.""" + from supabase_auth.helpers import parse_auth_otp_response + from supabase_auth.types import AuthOtpResponse + + # Test with message_id field + data = {"message_id": "12345"} + result = parse_auth_otp_response(data) + assert isinstance(result, AuthOtpResponse) + assert result.message_id == "12345" + assert result.user is None + assert result.session is None + + # Test with no message_id field + data = {} + result = parse_auth_otp_response(data) + assert isinstance(result, AuthOtpResponse) + assert result.message_id is None + assert result.user is None + assert result.session is None diff --git a/src/functions/Makefile b/src/functions/Makefile index 53bb6d25..5556dba4 100644 --- a/src/functions/Makefile +++ b/src/functions/Makefile @@ -10,3 +10,10 @@ build-sync: unasync sed -i '0,/SyncMock, /{s/SyncMock, //}' tests/_sync/test_function_client.py sed -i 's/SyncMock/Mock/g' tests/_sync/test_function_client.py sed -i 's/SyncClient/Client/g' src/supabase_functions/_sync/functions_client.py tests/_sync/test_function_client.py + +clean: + rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache + rm -f .coverage coverage.xml + +build: + uv build --package supabase_functions diff --git a/src/realtime/Makefile b/src/realtime/Makefile index 4870022d..8e11fc75 100644 --- a/src/realtime/Makefile +++ b/src/realtime/Makefile @@ -1,15 +1,22 @@ .PHONY: pytest pre-commit mypy tests infra stop-infra mypy: - uv run --exact --package realtime mypy src/realtime + uv run --package realtime mypy src/realtime -infra: +start-infra: supabase start --workdir infra -x studio,mailpit,edge-runtime,logflare,vector,supavisor,imgproxy,storage-api stop-infra: supabase --workdir infra stop -tests: mypy infra pytest +tests: mypy pytest -pytest: - uv run --exact --package realtime pytest --cov=realtime --cov-report=xml --cov-report=html -vv +pytest: start-infra + uv run --package realtime pytest --cov=realtime --cov-report=xml --cov-report=html -vv + +clean: + rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache + rm -f .coverage coverage.xml + +build: + uv build --package realtime diff --git a/src/storage/Makefile b/src/storage/Makefile index a2b64561..8e1ccd08 100644 --- a/src/storage/Makefile +++ b/src/storage/Makefile @@ -1,12 +1,12 @@ -run-infra: +start-infra: supabase --workdir infra start -x studio,gotrue,postgrest,mailpit,realtime,edge-runtime,logflare,vector,supavisor stop-infra: supabase --workdir infra stop -tests: run-infra pytest +tests: pytest -pytest: +pytest: start-infra uv run --package storage3 pytest --cov=./ --cov-report=xml --cov-report=html -vv build-sync: @@ -15,3 +15,10 @@ build-sync: sed -i 's/SyncMock/Mock/g' tests/_sync/test_bucket.py tests/_sync/test_client.py sed -i 's/SyncClient/Client/g' storage3/_sync/client.py storage3/_sync/bucket.py storage3/_sync/file_api.py tests/_sync/test_bucket.py tests/_sync/test_client.py sed -i 's/self\.session\.aclose/self\.session\.close/g' storage3/_sync/client.py + +clean: + rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache + rm -f .coverage coverage.xml + +build: + uv build --package storage3 diff --git a/src/supabase/Makefile b/src/supabase/Makefile index 12be11b9..18834a0d 100644 --- a/src/supabase/Makefile +++ b/src/supabase/Makefile @@ -1,7 +1,7 @@ .PHONY: pytest pre-commit unasync build-sync tests pytest: - uv run --exact --package supabase pytest --cov=./ --cov-report=xml --cov-report=html -vv + uv run --package supabase pytest --cov=./ --cov-report=xml --cov-report=html -vv unasync: uv run run-unasync.py @@ -16,3 +16,10 @@ build-sync: unasync sed -i 's/SyncMock/Mock/g' tests/_sync/test_client.py tests: pytest + +clean: + rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache + rm -f .coverage coverage.xml + +build: + uv build --package supabase diff --git a/src/supabase/pyproject.toml b/src/supabase/pyproject.toml index 7c880ebb..6d290e2b 100644 --- a/src/supabase/pyproject.toml +++ b/src/supabase/pyproject.toml @@ -22,8 +22,8 @@ dependencies = [ "realtime", "supabase_functions", "storage3", + "supabase_auth", "postgrest == 1.1.1", - "supabase_auth == 2.12.3", "httpx >=0.26,<0.29", ] diff --git a/uv.lock b/uv.lock index 54dde19a..5029702a 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ members = [ "realtime", "storage3", "supabase", + "supabase-auth", "supabase-functions", ] @@ -271,15 +272,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.13.4" +version = "4.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, ] [[package]] @@ -362,6 +363,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -513,97 +583,97 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/4e/08b493f1f1d8a5182df0044acc970799b58a8d289608e0d891a03e9d269a/coverage-7.10.4.tar.gz", hash = "sha256:25f5130af6c8e7297fd14634955ba9e1697f47143f289e2a23284177c0061d27", size = 823798, upload-time = "2025-08-17T00:26:43.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/f4/350759710db50362685f922259c140592dba15eb4e2325656a98413864d9/coverage-7.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d92d6edb0ccafd20c6fbf9891ca720b39c2a6a4b4a6f9cf323ca2c986f33e475", size = 216403, upload-time = "2025-08-17T00:24:19.083Z" }, - { url = "https://files.pythonhosted.org/packages/29/7e/e467c2bb4d5ecfd166bfd22c405cce4c50de2763ba1d78e2729c59539a42/coverage-7.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7202da14dc0236884fcc45665ffb2d79d4991a53fbdf152ab22f69f70923cc22", size = 216802, upload-time = "2025-08-17T00:24:21.824Z" }, - { url = "https://files.pythonhosted.org/packages/62/ab/2accdd1ccfe63b890e5eb39118f63c155202df287798364868a2884a50af/coverage-7.10.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ada418633ae24ec8d0fcad5efe6fc7aa3c62497c6ed86589e57844ad04365674", size = 243558, upload-time = "2025-08-17T00:24:23.569Z" }, - { url = "https://files.pythonhosted.org/packages/43/04/c14c33d0cfc0f4db6b3504d01a47f4c798563d932a836fd5f2dbc0521d3d/coverage-7.10.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b828e33eca6c3322adda3b5884456f98c435182a44917ded05005adfa1415500", size = 245370, upload-time = "2025-08-17T00:24:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/99/71/147053061f1f51c1d3b3d040c3cb26876964a3a0dca0765d2441411ca568/coverage-7.10.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:802793ba397afcfdbe9f91f89d65ae88b958d95edc8caf948e1f47d8b6b2b606", size = 247228, upload-time = "2025-08-17T00:24:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/cc/92/7ef882205d4d4eb502e6154ee7122c1a1b1ce3f29d0166921e0fb550a5d3/coverage-7.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d0b23512338c54101d3bf7a1ab107d9d75abda1d5f69bc0887fd079253e4c27e", size = 245270, upload-time = "2025-08-17T00:24:27.424Z" }, - { url = "https://files.pythonhosted.org/packages/ab/3d/297a20603abcc6c7d89d801286eb477b0b861f3c5a4222730f1c9837be3e/coverage-7.10.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f36b7dcf72d06a8c5e2dd3aca02be2b1b5db5f86404627dff834396efce958f2", size = 243287, upload-time = "2025-08-17T00:24:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/65/f9/b04111438f41f1ddd5dc88706d5f8064ae5bb962203c49fe417fa23a362d/coverage-7.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fce316c367a1dc2c411821365592eeb335ff1781956d87a0410eae248188ba51", size = 244164, upload-time = "2025-08-17T00:24:30.393Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e5/c7d9eb7a9ea66cf92d069077719fb2b07782dcd7050b01a9b88766b52154/coverage-7.10.4-cp310-cp310-win32.whl", hash = "sha256:8c5dab29fc8070b3766b5fc85f8d89b19634584429a2da6d42da5edfadaf32ae", size = 218917, upload-time = "2025-08-17T00:24:31.67Z" }, - { url = "https://files.pythonhosted.org/packages/66/30/4d9d3b81f5a836b31a7428b8a25e6d490d4dca5ff2952492af130153c35c/coverage-7.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:4b0d114616f0fccb529a1817457d5fb52a10e106f86c5fb3b0bd0d45d0d69b93", size = 219822, upload-time = "2025-08-17T00:24:32.89Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ba/2c9817e62018e7d480d14f684c160b3038df9ff69c5af7d80e97d143e4d1/coverage-7.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05d5f98ec893d4a2abc8bc5f046f2f4367404e7e5d5d18b83de8fde1093ebc4f", size = 216514, upload-time = "2025-08-17T00:24:34.188Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/093412a959a6b6261446221ba9fb23bb63f661a5de70b5d130763c87f916/coverage-7.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9267efd28f8994b750d171e58e481e3bbd69e44baed540e4c789f8e368b24b88", size = 216914, upload-time = "2025-08-17T00:24:35.881Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/2fdf4a71cfe93b07eae845ebf763267539a7d8b7e16b062f959d56d7e433/coverage-7.10.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4456a039fdc1a89ea60823d0330f1ac6f97b0dbe9e2b6fb4873e889584b085fb", size = 247308, upload-time = "2025-08-17T00:24:37.61Z" }, - { url = "https://files.pythonhosted.org/packages/ba/16/33f6cded458e84f008b9f6bc379609a6a1eda7bffe349153b9960803fc11/coverage-7.10.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c2bfbd2a9f7e68a21c5bd191be94bfdb2691ac40d325bac9ef3ae45ff5c753d9", size = 249241, upload-time = "2025-08-17T00:24:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/84/98/9c18e47c889be58339ff2157c63b91a219272503ee32b49d926eea2337f2/coverage-7.10.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab7765f10ae1df7e7fe37de9e64b5a269b812ee22e2da3f84f97b1c7732a0d8", size = 251346, upload-time = "2025-08-17T00:24:40.507Z" }, - { url = "https://files.pythonhosted.org/packages/6d/07/00a6c0d53e9a22d36d8e95ddd049b860eef8f4b9fd299f7ce34d8e323356/coverage-7.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a09b13695166236e171ec1627ff8434b9a9bae47528d0ba9d944c912d33b3d2", size = 249037, upload-time = "2025-08-17T00:24:41.904Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0e/1e1b944d6a6483d07bab5ef6ce063fcf3d0cc555a16a8c05ebaab11f5607/coverage-7.10.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5c9e75dfdc0167d5675e9804f04a56b2cf47fb83a524654297000b578b8adcb7", size = 247090, upload-time = "2025-08-17T00:24:43.193Z" }, - { url = "https://files.pythonhosted.org/packages/62/43/2ce5ab8a728b8e25ced077111581290ffaef9efaf860a28e25435ab925cf/coverage-7.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c751261bfe6481caba15ec005a194cb60aad06f29235a74c24f18546d8377df0", size = 247732, upload-time = "2025-08-17T00:24:44.906Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f3/706c4a24f42c1c5f3a2ca56637ab1270f84d9e75355160dc34d5e39bb5b7/coverage-7.10.4-cp311-cp311-win32.whl", hash = "sha256:051c7c9e765f003c2ff6e8c81ccea28a70fb5b0142671e4e3ede7cebd45c80af", size = 218961, upload-time = "2025-08-17T00:24:46.241Z" }, - { url = "https://files.pythonhosted.org/packages/e8/aa/6b9ea06e0290bf1cf2a2765bba89d561c5c563b4e9db8298bf83699c8b67/coverage-7.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a647b152f10be08fb771ae4a1421dbff66141e3d8ab27d543b5eb9ea5af8e52", size = 219851, upload-time = "2025-08-17T00:24:48.795Z" }, - { url = "https://files.pythonhosted.org/packages/8b/be/f0dc9ad50ee183369e643cd7ed8f2ef5c491bc20b4c3387cbed97dd6e0d1/coverage-7.10.4-cp311-cp311-win_arm64.whl", hash = "sha256:b09b9e4e1de0d406ca9f19a371c2beefe3193b542f64a6dd40cfcf435b7d6aa0", size = 218530, upload-time = "2025-08-17T00:24:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4a/781c9e4dd57cabda2a28e2ce5b00b6be416015265851060945a5ed4bd85e/coverage-7.10.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a1f0264abcabd4853d4cb9b3d164adbf1565da7dab1da1669e93f3ea60162d79", size = 216706, upload-time = "2025-08-17T00:24:51.528Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8c/51255202ca03d2e7b664770289f80db6f47b05138e06cce112b3957d5dfd/coverage-7.10.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:536cbe6b118a4df231b11af3e0f974a72a095182ff8ec5f4868c931e8043ef3e", size = 216939, upload-time = "2025-08-17T00:24:53.171Z" }, - { url = "https://files.pythonhosted.org/packages/06/7f/df11131483698660f94d3c847dc76461369782d7a7644fcd72ac90da8fd0/coverage-7.10.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9a4c0d84134797b7bf3f080599d0cd501471f6c98b715405166860d79cfaa97e", size = 248429, upload-time = "2025-08-17T00:24:54.934Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/13ac5eda7300e160bf98f082e75f5c5b4189bf3a883dd1ee42dbedfdc617/coverage-7.10.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7c155fc0f9cee8c9803ea0ad153ab6a3b956baa5d4cd993405dc0b45b2a0b9e0", size = 251178, upload-time = "2025-08-17T00:24:56.353Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/f63b56a58ad0bec68a840e7be6b7ed9d6f6288d790760647bb88f5fea41e/coverage-7.10.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5f2ab6e451d4b07855d8bcf063adf11e199bff421a4ba57f5bb95b7444ca62", size = 252313, upload-time = "2025-08-17T00:24:57.692Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b6/79338f1ea27b01266f845afb4485976211264ab92407d1c307babe3592a7/coverage-7.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:685b67d99b945b0c221be0780c336b303a7753b3e0ec0d618c795aada25d5e7a", size = 250230, upload-time = "2025-08-17T00:24:59.293Z" }, - { url = "https://files.pythonhosted.org/packages/bc/93/3b24f1da3e0286a4dc5832427e1d448d5296f8287464b1ff4a222abeeeb5/coverage-7.10.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c079027e50c2ae44da51c2e294596cbc9dbb58f7ca45b30651c7e411060fc23", size = 248351, upload-time = "2025-08-17T00:25:00.676Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/d59412f869e49dcc5b89398ef3146c8bfaec870b179cc344d27932e0554b/coverage-7.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3749aa72b93ce516f77cf5034d8e3c0dfd45c6e8a163a602ede2dc5f9a0bb927", size = 249788, upload-time = "2025-08-17T00:25:02.354Z" }, - { url = "https://files.pythonhosted.org/packages/cc/52/04a3b733f40a0cc7c4a5b9b010844111dbf906df3e868b13e1ce7b39ac31/coverage-7.10.4-cp312-cp312-win32.whl", hash = "sha256:fecb97b3a52fa9bcd5a7375e72fae209088faf671d39fae67261f37772d5559a", size = 219131, upload-time = "2025-08-17T00:25:03.79Z" }, - { url = "https://files.pythonhosted.org/packages/83/dd/12909fc0b83888197b3ec43a4ac7753589591c08d00d9deda4158df2734e/coverage-7.10.4-cp312-cp312-win_amd64.whl", hash = "sha256:26de58f355626628a21fe6a70e1e1fad95702dafebfb0685280962ae1449f17b", size = 219939, upload-time = "2025-08-17T00:25:05.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/c7/058bb3220fdd6821bada9685eadac2940429ab3c97025ce53549ff423cc1/coverage-7.10.4-cp312-cp312-win_arm64.whl", hash = "sha256:67e8885408f8325198862bc487038a4980c9277d753cb8812510927f2176437a", size = 218572, upload-time = "2025-08-17T00:25:06.897Z" }, - { url = "https://files.pythonhosted.org/packages/46/b0/4a3662de81f2ed792a4e425d59c4ae50d8dd1d844de252838c200beed65a/coverage-7.10.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b8e1d2015d5dfdbf964ecef12944c0c8c55b885bb5c0467ae8ef55e0e151233", size = 216735, upload-time = "2025-08-17T00:25:08.617Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e8/e2dcffea01921bfffc6170fb4406cffb763a3b43a047bbd7923566708193/coverage-7.10.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:25735c299439018d66eb2dccf54f625aceb78645687a05f9f848f6e6c751e169", size = 216982, upload-time = "2025-08-17T00:25:10.384Z" }, - { url = "https://files.pythonhosted.org/packages/9d/59/cc89bb6ac869704d2781c2f5f7957d07097c77da0e8fdd4fd50dbf2ac9c0/coverage-7.10.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:715c06cb5eceac4d9b7cdf783ce04aa495f6aff657543fea75c30215b28ddb74", size = 247981, upload-time = "2025-08-17T00:25:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/aa/23/3da089aa177ceaf0d3f96754ebc1318597822e6387560914cc480086e730/coverage-7.10.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e017ac69fac9aacd7df6dc464c05833e834dc5b00c914d7af9a5249fcccf07ef", size = 250584, upload-time = "2025-08-17T00:25:13.483Z" }, - { url = "https://files.pythonhosted.org/packages/ad/82/e8693c368535b4e5fad05252a366a1794d481c79ae0333ed943472fd778d/coverage-7.10.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad180cc40b3fccb0f0e8c702d781492654ac2580d468e3ffc8065e38c6c2408", size = 251856, upload-time = "2025-08-17T00:25:15.27Z" }, - { url = "https://files.pythonhosted.org/packages/56/19/8b9cb13292e602fa4135b10a26ac4ce169a7fc7c285ff08bedd42ff6acca/coverage-7.10.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:becbdcd14f685fada010a5f792bf0895675ecf7481304fe159f0cd3f289550bd", size = 250015, upload-time = "2025-08-17T00:25:16.759Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/e5903990ce089527cf1c4f88b702985bd65c61ac245923f1ff1257dbcc02/coverage-7.10.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0b485ca21e16a76f68060911f97ebbe3e0d891da1dbbce6af7ca1ab3f98b9097", size = 247908, upload-time = "2025-08-17T00:25:18.232Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/7d464f116df1df7fe340669af1ddbe1a371fc60f3082ff3dc837c4f1f2ab/coverage-7.10.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d098ccfe8e1e0a1ed9a0249138899948afd2978cbf48eb1cc3fcd38469690", size = 249525, upload-time = "2025-08-17T00:25:20.141Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/722e0cdbf6c19e7235c2020837d4e00f3b07820fd012201a983238cc3a30/coverage-7.10.4-cp313-cp313-win32.whl", hash = "sha256:8630f8af2ca84b5c367c3df907b1706621abe06d6929f5045fd628968d421e6e", size = 219173, upload-time = "2025-08-17T00:25:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/97/7e/aa70366f8275955cd51fa1ed52a521c7fcebcc0fc279f53c8c1ee6006dfe/coverage-7.10.4-cp313-cp313-win_amd64.whl", hash = "sha256:f68835d31c421736be367d32f179e14ca932978293fe1b4c7a6a49b555dff5b2", size = 219969, upload-time = "2025-08-17T00:25:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/ac/96/c39d92d5aad8fec28d4606556bfc92b6fee0ab51e4a548d9b49fb15a777c/coverage-7.10.4-cp313-cp313-win_arm64.whl", hash = "sha256:6eaa61ff6724ca7ebc5326d1fae062d85e19b38dd922d50903702e6078370ae7", size = 218601, upload-time = "2025-08-17T00:25:25.295Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/34d549a6177bd80fa5db758cb6fd3057b7ad9296d8707d4ab7f480b0135f/coverage-7.10.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:702978108876bfb3d997604930b05fe769462cc3000150b0e607b7b444f2fd84", size = 217445, upload-time = "2025-08-17T00:25:27.129Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c0/433da866359bf39bf595f46d134ff2d6b4293aeea7f3328b6898733b0633/coverage-7.10.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e8f978e8c5521d9c8f2086ac60d931d583fab0a16f382f6eb89453fe998e2484", size = 217676, upload-time = "2025-08-17T00:25:28.641Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d7/2b99aa8737f7801fd95222c79a4ebc8c5dd4460d4bed7ef26b17a60c8d74/coverage-7.10.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:df0ac2ccfd19351411c45e43ab60932b74472e4648b0a9edf6a3b58846e246a9", size = 259002, upload-time = "2025-08-17T00:25:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/08/cf/86432b69d57debaef5abf19aae661ba8f4fcd2882fa762e14added4bd334/coverage-7.10.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73a0d1aaaa3796179f336448e1576a3de6fc95ff4f07c2d7251d4caf5d18cf8d", size = 261178, upload-time = "2025-08-17T00:25:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/23/78/85176593f4aa6e869cbed7a8098da3448a50e3fac5cb2ecba57729a5220d/coverage-7.10.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873da6d0ed6b3ffc0bc01f2c7e3ad7e2023751c0d8d86c26fe7322c314b031dc", size = 263402, upload-time = "2025-08-17T00:25:33.339Z" }, - { url = "https://files.pythonhosted.org/packages/88/1d/57a27b6789b79abcac0cc5805b31320d7a97fa20f728a6a7c562db9a3733/coverage-7.10.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c6446c75b0e7dda5daa876a1c87b480b2b52affb972fedd6c22edf1aaf2e00ec", size = 260957, upload-time = "2025-08-17T00:25:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e5/3e5ddfd42835c6def6cd5b2bdb3348da2e34c08d9c1211e91a49e9fd709d/coverage-7.10.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6e73933e296634e520390c44758d553d3b573b321608118363e52113790633b9", size = 258718, upload-time = "2025-08-17T00:25:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/d364f0f7ef111615dc4e05a6ed02cac7b6f2ac169884aa57faeae9eb5fa0/coverage-7.10.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52073d4b08d2cb571234c8a71eb32af3c6923149cf644a51d5957ac128cf6aa4", size = 259848, upload-time = "2025-08-17T00:25:37.754Z" }, - { url = "https://files.pythonhosted.org/packages/10/c6/bbea60a3b309621162e53faf7fac740daaf083048ea22077418e1ecaba3f/coverage-7.10.4-cp313-cp313t-win32.whl", hash = "sha256:e24afb178f21f9ceb1aefbc73eb524769aa9b504a42b26857243f881af56880c", size = 219833, upload-time = "2025-08-17T00:25:39.252Z" }, - { url = "https://files.pythonhosted.org/packages/44/a5/f9f080d49cfb117ddffe672f21eab41bd23a46179a907820743afac7c021/coverage-7.10.4-cp313-cp313t-win_amd64.whl", hash = "sha256:be04507ff1ad206f4be3d156a674e3fb84bbb751ea1b23b142979ac9eebaa15f", size = 220897, upload-time = "2025-08-17T00:25:40.772Z" }, - { url = "https://files.pythonhosted.org/packages/46/89/49a3fc784fa73d707f603e586d84a18c2e7796707044e9d73d13260930b7/coverage-7.10.4-cp313-cp313t-win_arm64.whl", hash = "sha256:f3e3ff3f69d02b5dad67a6eac68cc9c71ae343b6328aae96e914f9f2f23a22e2", size = 219160, upload-time = "2025-08-17T00:25:42.229Z" }, - { url = "https://files.pythonhosted.org/packages/b5/22/525f84b4cbcff66024d29f6909d7ecde97223f998116d3677cfba0d115b5/coverage-7.10.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a59fe0af7dd7211ba595cf7e2867458381f7e5d7b4cffe46274e0b2f5b9f4eb4", size = 216717, upload-time = "2025-08-17T00:25:43.875Z" }, - { url = "https://files.pythonhosted.org/packages/a6/58/213577f77efe44333a416d4bcb251471e7f64b19b5886bb515561b5ce389/coverage-7.10.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a6c35c5b70f569ee38dc3350cd14fdd0347a8b389a18bb37538cc43e6f730e6", size = 216994, upload-time = "2025-08-17T00:25:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/17/85/34ac02d0985a09472f41b609a1d7babc32df87c726c7612dc93d30679b5a/coverage-7.10.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acb7baf49f513554c4af6ef8e2bd6e8ac74e6ea0c7386df8b3eb586d82ccccc4", size = 248038, upload-time = "2025-08-17T00:25:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/47/4f/2140305ec93642fdaf988f139813629cbb6d8efa661b30a04b6f7c67c31e/coverage-7.10.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a89afecec1ed12ac13ed203238b560cbfad3522bae37d91c102e690b8b1dc46c", size = 250575, upload-time = "2025-08-17T00:25:48.613Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/41b5784180b82a083c76aeba8f2c72ea1cb789e5382157b7dc852832aea2/coverage-7.10.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:480442727f464407d8ade6e677b7f21f3b96a9838ab541b9a28ce9e44123c14e", size = 251927, upload-time = "2025-08-17T00:25:50.881Z" }, - { url = "https://files.pythonhosted.org/packages/78/ca/c1dd063e50b71f5aea2ebb27a1c404e7b5ecf5714c8b5301f20e4e8831ac/coverage-7.10.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a89bf193707f4a17f1ed461504031074d87f035153239f16ce86dfb8f8c7ac76", size = 249930, upload-time = "2025-08-17T00:25:52.422Z" }, - { url = "https://files.pythonhosted.org/packages/8d/66/d8907408612ffee100d731798e6090aedb3ba766ecf929df296c1a7ee4fb/coverage-7.10.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3ddd912c2fc440f0fb3229e764feec85669d5d80a988ff1b336a27d73f63c818", size = 247862, upload-time = "2025-08-17T00:25:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/29/db/53cd8ec8b1c9c52d8e22a25434785bfc2d1e70c0cfb4d278a1326c87f741/coverage-7.10.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a538944ee3a42265e61c7298aeba9ea43f31c01271cf028f437a7b4075592cf", size = 249360, upload-time = "2025-08-17T00:25:55.833Z" }, - { url = "https://files.pythonhosted.org/packages/4f/75/5ec0a28ae4a0804124ea5a5becd2b0fa3adf30967ac656711fb5cdf67c60/coverage-7.10.4-cp314-cp314-win32.whl", hash = "sha256:fd2e6002be1c62476eb862b8514b1ba7e7684c50165f2a8d389e77da6c9a2ebd", size = 219449, upload-time = "2025-08-17T00:25:57.984Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/66e2ee085ec60672bf5250f11101ad8143b81f24989e8c0e575d16bb1e53/coverage-7.10.4-cp314-cp314-win_amd64.whl", hash = "sha256:ec113277f2b5cf188d95fb66a65c7431f2b9192ee7e6ec9b72b30bbfb53c244a", size = 220246, upload-time = "2025-08-17T00:25:59.868Z" }, - { url = "https://files.pythonhosted.org/packages/37/3b/00b448d385f149143190846217797d730b973c3c0ec2045a7e0f5db3a7d0/coverage-7.10.4-cp314-cp314-win_arm64.whl", hash = "sha256:9744954bfd387796c6a091b50d55ca7cac3d08767795b5eec69ad0f7dbf12d38", size = 218825, upload-time = "2025-08-17T00:26:01.44Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2e/55e20d3d1ce00b513efb6fd35f13899e1c6d4f76c6cbcc9851c7227cd469/coverage-7.10.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5af4829904dda6aabb54a23879f0f4412094ba9ef153aaa464e3c1b1c9bc98e6", size = 217462, upload-time = "2025-08-17T00:26:03.014Z" }, - { url = "https://files.pythonhosted.org/packages/47/b3/aab1260df5876f5921e2c57519e73a6f6eeacc0ae451e109d44ee747563e/coverage-7.10.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7bba5ed85e034831fac761ae506c0644d24fd5594727e174b5a73aff343a7508", size = 217675, upload-time = "2025-08-17T00:26:04.606Z" }, - { url = "https://files.pythonhosted.org/packages/67/23/1cfe2aa50c7026180989f0bfc242168ac7c8399ccc66eb816b171e0ab05e/coverage-7.10.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57d555b0719834b55ad35045de6cc80fc2b28e05adb6b03c98479f9553b387f", size = 259176, upload-time = "2025-08-17T00:26:06.159Z" }, - { url = "https://files.pythonhosted.org/packages/9d/72/5882b6aeed3f9de7fc4049874fd7d24213bf1d06882f5c754c8a682606ec/coverage-7.10.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ba62c51a72048bb1ea72db265e6bd8beaabf9809cd2125bbb5306c6ce105f214", size = 261341, upload-time = "2025-08-17T00:26:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/1b/70/a0c76e3087596ae155f8e71a49c2c534c58b92aeacaf4d9d0cbbf2dde53b/coverage-7.10.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0acf0c62a6095f07e9db4ec365cc58c0ef5babb757e54745a1aa2ea2a2564af1", size = 263600, upload-time = "2025-08-17T00:26:11.045Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5f/27e4cd4505b9a3c05257fb7fc509acbc778c830c450cb4ace00bf2b7bda7/coverage-7.10.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1033bf0f763f5cf49ffe6594314b11027dcc1073ac590b415ea93463466deec", size = 261036, upload-time = "2025-08-17T00:26:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/02/d6/cf2ae3a7f90ab226ea765a104c4e76c5126f73c93a92eaea41e1dc6a1892/coverage-7.10.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92c29eff894832b6a40da1789b1f252305af921750b03ee4535919db9179453d", size = 258794, upload-time = "2025-08-17T00:26:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/39f222eab0d78aa2001cdb7852aa1140bba632db23a5cfd832218b496d6c/coverage-7.10.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:822c4c830989c2093527e92acd97be4638a44eb042b1bdc0e7a278d84a070bd3", size = 259946, upload-time = "2025-08-17T00:26:15.899Z" }, - { url = "https://files.pythonhosted.org/packages/74/b2/49d82acefe2fe7c777436a3097f928c7242a842538b190f66aac01f29321/coverage-7.10.4-cp314-cp314t-win32.whl", hash = "sha256:e694d855dac2e7cf194ba33653e4ba7aad7267a802a7b3fc4347d0517d5d65cd", size = 220226, upload-time = "2025-08-17T00:26:17.566Z" }, - { url = "https://files.pythonhosted.org/packages/06/b0/afb942b6b2fc30bdbc7b05b087beae11c2b0daaa08e160586cf012b6ad70/coverage-7.10.4-cp314-cp314t-win_amd64.whl", hash = "sha256:efcc54b38ef7d5bfa98050f220b415bc5bb3d432bd6350a861cf6da0ede2cdcd", size = 221346, upload-time = "2025-08-17T00:26:19.311Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/e0531c9d1525cb6eac5b5733c76f27f3053ee92665f83f8899516fea6e76/coverage-7.10.4-cp314-cp314t-win_arm64.whl", hash = "sha256:6f3a3496c0fa26bfac4ebc458747b778cff201c8ae94fa05e1391bab0dbc473c", size = 219368, upload-time = "2025-08-17T00:26:21.011Z" }, - { url = "https://files.pythonhosted.org/packages/d1/61/4e38d86d31a268778d69bb3fd1fc88e0c7a78ffdee48f2b5d9e028a3dce5/coverage-7.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:48fd4d52600c2a9d5622e52dfae674a7845c5e1dceaf68b88c99feb511fbcfd6", size = 216393, upload-time = "2025-08-17T00:26:22.648Z" }, - { url = "https://files.pythonhosted.org/packages/17/16/5c2fdb1d213f57e0ff107738397aff68582fa90a6575ca165b49eae5a809/coverage-7.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:56217b470d09d69e6b7dcae38200f95e389a77db801cb129101697a4553b18b6", size = 216779, upload-time = "2025-08-17T00:26:24.422Z" }, - { url = "https://files.pythonhosted.org/packages/26/99/3aca6b4028e3667ccfbaef9cfd9dca8d85eb14deee7868373cc48cbee553/coverage-7.10.4-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:44ac3f21a6e28c5ff7f7a47bca5f87885f6a1e623e637899125ba47acd87334d", size = 243214, upload-time = "2025-08-17T00:26:26.468Z" }, - { url = "https://files.pythonhosted.org/packages/0d/33/27a7d2557f85001b2edb6a2f14037851f87ca7d69a4ca79460e1859f4c7f/coverage-7.10.4-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3387739d72c84d17b4d2f7348749cac2e6700e7152026912b60998ee9a40066b", size = 245037, upload-time = "2025-08-17T00:26:28.071Z" }, - { url = "https://files.pythonhosted.org/packages/6d/68/92c0e18d36d34c774cb5053c9413188c27f8b3f9587e315193a30c1695ce/coverage-7.10.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f111ff20d9a6348e0125be892608e33408dd268f73b020940dfa8511ad05503", size = 246809, upload-time = "2025-08-17T00:26:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/03/22/f0618594010903401e782459c100755af3f275ea86d49b0d4f3afa3658d9/coverage-7.10.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:01a852f0a9859734b018a3f483cc962d0b381d48d350b1a0c47d618c73a0c398", size = 244695, upload-time = "2025-08-17T00:26:31.495Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/e704923a037a4a38a3c13ae6405c31236db2d274307ab28fd1a23b961cad/coverage-7.10.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:225111dd06759ba4e37cee4c0b4f3df2b15c879e9e3c37bf986389300b9917c3", size = 242766, upload-time = "2025-08-17T00:26:33.425Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b7/4dc6f2b41aa907ae330ed841deb49c9487f6ec5072a577fc3a3b284fe855/coverage-7.10.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2178d4183bd1ba608f0bb12e71e55838ba1b7dbb730264f8b08de9f8ef0c27d0", size = 243723, upload-time = "2025-08-17T00:26:35.688Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/0e58abc2ce2d9a5b906dd1c08802864f756365843c413aebf0184148ddfb/coverage-7.10.4-cp39-cp39-win32.whl", hash = "sha256:93d175fe81913aee7a6ea430abbdf2a79f1d9fd451610e12e334e4fe3264f563", size = 218927, upload-time = "2025-08-17T00:26:37.725Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3e/4668a5b5601450d9c8aa71cc4f7e6c6c259350e577c758b894443598322a/coverage-7.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:2221a823404bb941c7721cf0ef55ac6ee5c25d905beb60c0bba5e5e85415d353", size = 219838, upload-time = "2025-08-17T00:26:39.786Z" }, - { url = "https://files.pythonhosted.org/packages/bb/78/983efd23200921d9edb6bd40512e1aa04af553d7d5a171e50f9b2b45d109/coverage-7.10.4-py3-none-any.whl", hash = "sha256:065d75447228d05121e5c938ca8f0e91eed60a1eb2d1258d42d5084fecfc3302", size = 208365, upload-time = "2025-08-17T00:26:41.479Z" }, +version = "7.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" }, + { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931, upload-time = "2025-08-23T14:40:30.735Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659, upload-time = "2025-08-23T14:40:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285, upload-time = "2025-08-23T14:40:40.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185, upload-time = "2025-08-23T14:40:42.026Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" }, + { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" }, + { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466, upload-time = "2025-08-23T14:40:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327, upload-time = "2025-08-23T14:40:55.424Z" }, + { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213, upload-time = "2025-08-23T14:40:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893, upload-time = "2025-08-23T14:40:58.011Z" }, + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" }, + { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" }, + { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" }, + { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" }, + { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/de/31/41929ee53af829ea5a88e71d335ea09d0bb587a3da1c5e58e59b48473ed8/coverage-7.10.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3876385722e335d6e991c430302c24251ef9c2a9701b2b390f5473199b1b8ebf", size = 243588, upload-time = "2025-08-23T14:42:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ed/fcb0838ddf149d68d09f89af57397b0dd9d26b100cc729daf1b0caf0b2d3/coverage-7.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2d488d7d42b6ded7ea0704884f89dcabd2619505457de8fc9a6011c62106f6e5", size = 243138, upload-time = "2025-08-23T14:42:36.311Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7e/c82a8bede46217c1d944bd19b65e7106633b998640f00ab49c5f747a5844/coverage-7.10.5-cp39-cp39-win32.whl", hash = "sha256:02650a11324b80057b8c9c29487020073d5e98a498f1857f37e3f9b6ea1b2426", size = 219289, upload-time = "2025-08-23T14:42:39.827Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ac/46645ef6be543f2e7de08cc2601a0b67e130c816be3b749ab741be689fb9/coverage-7.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:b45264dd450a10f9e03237b41a9a24e85cbb1e278e5a32adb1a303f58f0017f3", size = 220199, upload-time = "2025-08-23T14:42:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" }, ] [package.optional-dependencies] @@ -611,6 +681,53 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/56/d2/4482d97c948c029be08cb29854a91bd2ae8da7eb9c4152461f1244dcea70/cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", size = 3576812, upload-time = "2025-08-05T23:59:04.833Z" }, + { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694, upload-time = "2025-08-05T23:59:06.66Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010, upload-time = "2025-08-05T23:59:08.14Z" }, + { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377, upload-time = "2025-08-05T23:59:09.584Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609, upload-time = "2025-08-05T23:59:11.923Z" }, + { url = "https://files.pythonhosted.org/packages/78/6d/c49ccf243f0a1b0781c2a8de8123ee552f0c8a417c6367a24d2ecb7c11b3/cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", size = 3322156, upload-time = "2025-08-05T23:59:13.597Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, +] + [[package]] name = "cssutils" version = "2.11.1" @@ -707,13 +824,25 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] +[[package]] +name = "faker" +version = "37.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/cd/f7679c20f07d9e2013123b7f7e13809a3450a18d938d58e86081a486ea15/faker-37.6.0.tar.gz", hash = "sha256:0f8cc34f30095184adf87c3c24c45b38b33ad81c35ef6eb0a3118f301143012c", size = 1907960, upload-time = "2025-08-26T15:56:27.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/7d/8b50e4ac772719777be33661f4bde320793400a706f5eb214e4de46f093c/faker-37.6.0-py3-none-any.whl", hash = "sha256:3c5209b23d7049d596a51db5d76403a0ccfea6fc294ffa2ecfef6a8843b1e6a7", size = 1949837, upload-time = "2025-08-26T15:56:25.33Z" }, +] + [[package]] name = "filelock" version = "3.19.1" @@ -834,6 +963,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "future-fstrings" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/e2/3874574cce18a2e3608abfe5b4b5b3c9765653c464f5da18df8971cf501d/future_fstrings-1.2.0.tar.gz", hash = "sha256:6cf41cbe97c398ab5a81168ce0dbb8ad95862d3caf23c21e4430627b90844089", size = 5786, upload-time = "2019-06-16T03:04:42.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6d/ea1d52e9038558dd37f5d30647eb9f07888c164960a5d4daa5f970c6da25/future_fstrings-1.2.0-py2.py3-none-any.whl", hash = "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63", size = 6138, upload-time = "2019-06-16T03:04:40.395Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -845,15 +983,15 @@ wheels = [ [[package]] name = "h2" -version = "4.2.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" }, + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, ] [[package]] @@ -1349,6 +1487,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload-time = "2023-06-20T04:17:17.522Z" }, ] +[[package]] +name = "networkx" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/80/a84676339aaae2f1cfdf9f418701dd634aef9cc76f708ef55c36ff39c3ca/networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6", size = 2073928, upload-time = "2023-10-28T08:41:39.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/f0/8fbc882ca80cf077f1b246c0e3c3465f7f415439bdea6b899f6b19f61f70/networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2", size = 1647772, upload-time = "2023-10-28T08:41:36.945Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version >= '3.11' and python_full_version < '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1369,11 +1544,11 @@ wheels = [ [[package]] name = "parso" -version = "0.8.4" +version = "0.8.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] [[package]] @@ -1387,11 +1562,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -1551,6 +1726,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -1693,6 +1877,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pylsp-mypy" version = "0.7.0" @@ -1752,6 +1941,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] +[[package]] +name = "pytest-depends" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "future-fstrings" }, + { name = "networkx", version = "3.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/5b/929e7381c342ca5040136577916d0bb20f97bbadded59fdb9aad084461a2/pytest-depends-1.0.1.tar.gz", hash = "sha256:90a28e2b87b75b18abd128c94015248544acac20e4392e9921e5a86f93319dfe", size = 8763, upload-time = "2020-04-05T12:14:32.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/8a/96cec5c431fd706c8b2435dcb544224db7e09f4e3cc192d4c08d8980705a/pytest_depends-1.0.1-py3-none-any.whl", hash = "sha256:a1df072bcc93d77aca3f0946903f5fed8af2d9b0056db1dfc9ed5ac164ab0642", size = 10022, upload-time = "2020-04-05T12:14:31.101Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -1791,20 +2009,21 @@ wheels = [ [[package]] name = "python-lsp-server" -version = "1.13.0" +version = "1.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "docstring-to-markdown" }, { name = "importlib-metadata", version = "8.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_version < '0'" }, { name = "jedi" }, { name = "pluggy" }, { name = "python-lsp-jsonrpc" }, { name = "ujson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/be/9c96c1e261f7ce2e27a0211e075bf606b1033513d9544a75da5c7b8d1b06/python_lsp_server-1.13.0.tar.gz", hash = "sha256:378f26b63ecf4c10864de31de5e6da7ad639de9bd60a75d4110fea36fb8d0d69", size = 119147, upload-time = "2025-07-08T16:24:25.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/92/bd60cbe7d7d6c90e5e556a90497aa1892a3f779d9915026eca6e37a0b59b/python_lsp_server-1.13.1.tar.gz", hash = "sha256:bfa3d6bbca3fc3e6d0137b27cd1eabee65783a8d4314c36e1e230c603419afa3", size = 120484, upload-time = "2025-08-26T16:51:07.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/65/ab9fb9174ea65852eb01a7998ee1e373a1f5c167d2228a9dd096277678cd/python_lsp_server-1.13.0-py3-none-any.whl", hash = "sha256:ad3a8f792915706dc65ce1ffcbaed361e5e648c973cd05e5c7a4a94c678fef97", size = 76643, upload-time = "2025-07-08T16:24:24.878Z" }, + { url = "https://files.pythonhosted.org/packages/36/03/2884cf7bd092d8a5a71a406971fd2edf5c6171147ca2d4afb3f2a7f8c0f1/python_lsp_server-1.13.1-py3-none-any.whl", hash = "sha256:fadf45275d12a9d9a13e36717a8383cee8e7cffe8a30698d38bfb3fe71b5cdcd", size = 76748, upload-time = "2025-08-26T16:51:05.873Z" }, ] [[package]] @@ -1960,6 +2179,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + [[package]] name = "roman-numerals-py" version = "3.1.0" @@ -2561,7 +2792,7 @@ requires-dist = [ { name = "postgrest", specifier = "==1.1.1" }, { name = "realtime", editable = "src/realtime" }, { name = "storage3", editable = "src/storage" }, - { name = "supabase-auth", specifier = "==2.12.3" }, + { name = "supabase-auth", editable = "src/auth" }, { name = "supabase-functions", editable = "src/functions" }, ] @@ -2592,15 +2823,70 @@ tests = [ [[package]] name = "supabase-auth" version = "2.12.3" -source = { registry = "https://pypi.org/simple" } +source = { editable = "src/auth" } dependencies = [ { name = "httpx", extra = ["http2"] }, { name = "pydantic" }, - { name = "pyjwt" }, + { name = "pyjwt", extra = ["crypto"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e9/3d6f696a604752803b9e389b04d454f4b26a29b5d155b257fea4af8dc543/supabase_auth-2.12.3.tar.gz", hash = "sha256:8d3b67543f3b27f5adbfe46b66990424c8504c6b08c1141ec572a9802761edc2", size = 38430, upload-time = "2025-07-04T06:49:22.906Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/a6/4102d5fa08a8521d9432b4d10bb58fedbd1f92b211d1b45d5394f5cb9021/supabase_auth-2.12.3-py3-none-any.whl", hash = "sha256:15c7580e1313d30ffddeb3221cb3cdb87c2a80fd220bf85d67db19cd1668435b", size = 44417, upload-time = "2025-07-04T06:49:21.351Z" }, + +[package.dev-dependencies] +dev = [ + { name = "faker" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-depends" }, + { name = "pytest-mock" }, + { name = "respx" }, + { name = "ruff" }, + { name = "unasync" }, +] +lints = [ + { name = "ruff" }, + { name = "unasync" }, +] +tests = [ + { name = "faker" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-depends" }, + { name = "pytest-mock" }, + { name = "respx" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", extras = ["http2"], specifier = ">=0.26,<0.29" }, + { name = "pydantic", specifier = ">=1.10,<3" }, + { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "faker", specifier = ">=37.4.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "pytest-depends", specifier = ">=1.0.1" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "respx", specifier = ">=0.20.2,<0.23.0" }, + { name = "ruff", specifier = ">=0.12.1" }, + { name = "unasync", specifier = ">=0.6.0" }, +] +lints = [ + { name = "ruff", specifier = ">=0.12.1" }, + { name = "unasync", specifier = ">=0.6.0" }, +] +tests = [ + { name = "faker", specifier = ">=37.4.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "pytest-depends", specifier = ">=1.0.1" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "respx", specifier = ">=0.20.2,<0.23.0" }, ] [[package]] @@ -2739,11 +3025,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -2758,6 +3044,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "ujson" version = "5.11.0"