diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d9c5970..6971285d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,31 +9,36 @@ on: jobs: test: - name: Test / OS ${{ matrix.os }} / Python ${{ matrix.python-version }} + name: Tests strategy: matrix: os: [ubuntu-latest] python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - - name: Clone Repository - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Set up Poetry - uses: abatilo/actions-poetry@v3 + - uses: abatilo/actions-poetry@v3 with: poetry-version: 1.3.2 + - run: | + pip install tox>=4 + pip install tox-gh>=1.2 + tox -re ${{ matrix.python-version }} + - uses: codecov/codecov-action@v4 - - name: Run Tests - run: poetry run tests + lint: + name: Code formatting + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + pip install tox>=4 + tox -re format - - name: Upload Coverage - uses: codecov/codecov-action@v4 publish: needs: test if: ${{ !startsWith(github.event.head_commit.message, 'bump') && !startsWith(github.event.head_commit.message, 'chore') && github.ref == 'refs/heads/main' && github.event_name == 'push' && github.repository_owner == 'supabase-community' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 560b178c..4e0b41b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,12 +87,12 @@ Co-authored-by: dependabot\[bot\] \<49699333+dependabot\[bot\]@users.noreply.git - chore(deps-dev): bump jinja2 from 3.1.2 to 3.1.3 (#661) -Signed-off-by: dependabot\[bot\] \ +Signed-off-by: dependabot\[bot\] \<> Co-authored-by: dependabot\[bot\] \<49699333+dependabot\[bot\]@users.noreply.github.com> ([`dcbd7b4`](https://github.com/supabase-community/supabase-py/commit/dcbd7b47700b3b0d0e13f518e4542d5a2adc7ac9)) - chore(deps): bump postgrest from 0.13.1 to 0.13.2 (#662) -Signed-off-by: dependabot\[bot\] \ +Signed-off-by: dependabot\[bot\] \<> Co-authored-by: dependabot\[bot\] \<49699333+dependabot\[bot\]@users.noreply.github.com> ([`82c4305`](https://github.com/supabase-community/supabase-py/commit/82c4305dcb572a372ecdadd653056d530f308f28)) ### Fix @@ -109,7 +109,7 @@ Co-authored-by: dependabot\[bot\] \<49699333+dependabot\[bot\]@users.noreply.git - chore(deps-dev): bump gitpython from 3.1.40 to 3.1.41 (#659) -Signed-off-by: dependabot\[bot\] \ +Signed-off-by: dependabot\[bot\] \<> Co-authored-by: dependabot\[bot\] \<49699333+dependabot\[bot\]@users.noreply.github.com> ([`b3fd488`](https://github.com/supabase-community/supabase-py/commit/b3fd4887e11813118a465fe57c6c28830c31466f)) ### Fix diff --git a/Makefile b/Makefile deleted file mode 100644 index a45e19b2..00000000 --- a/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -install: - poetry install - -install_poetry: - curl -sSL https://install.python-poetry.org | python - - poetry install - -tests: install tests_only tests_pre_commit - -tests_pre_commit: - poetry run pre-commit run --all-files - -run_tests: tests - -tests_only: - poetry run pytest --cov=./ --cov-report=xml --cov-report=html -vv - -build_sync: - poetry run unasync supabase tests diff --git a/README.md b/README.md index 15682e9f..ef1fe423 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,8 @@ Currently the test suites are in a state of flux. We are expanding our clients t The above test database is a blank supabase instance that has populated the `countries` table with the built in countries script that can be found in the supabase UI. You can launch the test scripts and point to the above test database by running ```bash -./test.sh +# Example: Run tests against python3.9 environment +$ tox -e py39 ``` ## Badges diff --git a/poetry.lock b/poetry.lock index ee3d890e..1ef384a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -28,6 +28,14 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "argcomplete" version = "3.2.2" @@ -39,9 +47,32 @@ python-versions = ">=3.8" [package.extras] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "black" -version = "24.2.0" +version = "23.12.1" description = "The uncompromising code formatter." category = "dev" optional = false @@ -148,6 +179,14 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" + [[package]] name = "deprecation" version = "2.1.0" @@ -186,6 +225,17 @@ python-versions = ">=3.7" [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + [[package]] name = "filelock" version = "3.13.1" @@ -354,6 +404,55 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.12.3" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + [[package]] name = "isort" version = "5.13.2" @@ -365,6 +464,22 @@ python-versions = ">=3.8.0" [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jedi" +version = "0.19.1" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + [[package]] name = "jinja2" version = "3.1.3" @@ -426,6 +541,17 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + [[package]] name = "mccabe" version = "0.7.0" @@ -526,6 +652,18 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pathspec" version = "0.12.1" @@ -534,6 +672,25 @@ category = "dev" optional = false python-versions = ">=3.8" +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "platformdirs" version = "4.2.0" @@ -598,6 +755,25 @@ python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +tests = ["pytest"] + [[package]] name = "pycodestyle" version = "2.9.1" @@ -687,9 +863,25 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -727,11 +919,11 @@ yaml = ["PyYaml (>=6.0.1)"] [[package]] name = "python-semantic-release" -version = "9.1.1" +version = "8.7.0" description = "Automatic Semantic Versioning for Python projects" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" [package.dependencies] click = ">=8,<9" @@ -744,13 +936,13 @@ python-gitlab = ">=2,<5" requests = ">=2.25,<3" rich = ">=12.5.1" shellingham = ">=1.5.0.post1" -tomlkit = ">=0.11,<1.0" +tomlkit = ">=0.10,<1.0" [package.extras] -dev = ["pre-commit", "ruff (==0.1.11)", "tox"] +dev = ["pre-commit", "ruff (==0.1.8)", "tox"] docs = ["Sphinx (<=6.0.0)", "furo (>=2023.3.27)", "sphinx-autobuild (==2021.03.14)", "sphinxcontrib-apidoc (==0.3.0)"] mypy = ["mypy", "types-requests"] -test = ["coverage[toml] (>=6,<8)", "pytest (>=7,<8)", "pytest-clarity (>=1.0.1)", "pytest-cov (>=4,<5)", "pytest-env (>=1.0,<2.0)", "pytest-lazy-fixture (>=0.6.3,<0.7.0)", "pytest-mock (>=3,<4)", "pytest-pretty (>=1.2.0,<2)", "pytest-xdist (>=2,<4)", "requests-mock (>=1.10.0,<2)", "responses (==0.23.3)", "types-pytest-lazy-fixture (>=0.6.3.3)"] +test = ["coverage[toml] (>=6,<8)", "pytest (>=7,<8)", "pytest-clarity (>=1.0.1)", "pytest-cov (>=4,<5)", "pytest-lazy-fixture (>=0.6.3,<0.7.0)", "pytest-mock (>=3,<4)", "pytest-pretty (>=1.2.0,<2)", "pytest-xdist (>=2,<4)", "requests-mock (>=1.10.0,<2)", "responses (==0.23.3)", "types-pytest-lazy-fixture (>=0.6.3.3)"] [[package]] name = "pyyaml" @@ -831,15 +1023,16 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "58.5.3" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.extras] -docs = ["furo", "jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-inline-tabs", "sphinxcontrib-towncrier"] -testing = ["flake8-2020", "jaraco.envs", "jaraco.path (>=3.2.0)", "mock", "paver", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-virtualenv (>=1.2.7)", "pytest-xdist", "sphinx", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "shellingham" @@ -873,6 +1066,22 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + [[package]] name = "storage3" version = "0.7.0" @@ -938,21 +1147,16 @@ optional = false python-versions = ">=3.7" [[package]] -name = "typer" -version = "0.4.2" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +name = "traitlets" +version = "5.14.1" +description = "Traitlets Python configuration system" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -click = ">=7.1.1,<9.0.0" +python-versions = ">=3.8" [package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typing-extensions" @@ -973,27 +1177,6 @@ python-versions = ">=3.7" [package.extras] test = ["coverage", "pytest", "pytest-cov"] -[[package]] -name = "unasync" -version = "0.5.0" -description = "The async transformation code." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[[package]] -name = "unasync-cli" -version = "0.0.9" -description = "Command line interface for unasync" -category = "dev" -optional = false -python-versions = ">=3.6.14,<4.0.0" - -[package.dependencies] -setuptools = ">=58.2.0,<59.0.0" -typer = ">=0.4.0,<0.5.0" -unasync = ">=0.5.0,<0.6.0" - [[package]] name = "urllib3" version = "2.2.1" @@ -1056,7 +1239,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "87a63f3093ba45e747b76c1473b5d861ca32931729a041ff8de5ea700a938ec9" +content-hash = "2c237e19057c9c82919e5df0bae64267f067b78c64eb7126a9d696d59dc0e111" [metadata.files] annotated-types = [ @@ -1067,33 +1250,45 @@ anyio = [ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] +appnope = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] argcomplete = [ {file = "argcomplete-3.2.2-py3-none-any.whl", hash = "sha256:e44f4e7985883ab3e73a103ef0acd27299dbfe2dfed00142c35d4ddd3005901d"}, {file = "argcomplete-3.2.2.tar.gz", hash = "sha256:f3e49e8ea59b4026ee29548e24488af46e30c9de57d48638e24f54a1ea1000a2"}, ] +asttokens = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] black = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] certifi = [ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, @@ -1265,6 +1460,10 @@ decli = [ {file = "decli-0.6.1-py3-none-any.whl", hash = "sha256:7815ac58617764e1a200d7cadac6315fcaacc24d727d182f9878dd6378ccf869"}, {file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"}, ] +decorator = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] deprecation = [ {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, @@ -1281,6 +1480,10 @@ exceptiongroup = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] +executing = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] filelock = [ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, @@ -1333,10 +1536,22 @@ iniconfig = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +ipdb = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] +ipython = [ + {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, + {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, +] isort = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] +jedi = [ + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, +] jinja2 = [ {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, @@ -1411,6 +1626,10 @@ markupsafe = [ {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1447,10 +1666,22 @@ packaging = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +parso = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] pathspec = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +pexpect = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] platformdirs = [ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, @@ -1471,6 +1702,14 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, ] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +pure-eval = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] pycodestyle = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, @@ -1576,9 +1815,13 @@ pytest-cov = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] +pytest-sugar = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] python-dotenv = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, @@ -1589,8 +1832,8 @@ python-gitlab = [ {file = "python_gitlab-4.4.0-py3-none-any.whl", hash = "sha256:cdad39d016f59664cdaad0f878f194c79cb4357630776caa9a92c1da25c8d986"}, ] python-semantic-release = [ - {file = "python-semantic-release-9.1.1.tar.gz", hash = "sha256:fe4fc40f52cdddbfe82c710070978306b35e9e4f2c7d98a77db55bf6f5e544f2"}, - {file = "python_semantic_release-9.1.1-py3-none-any.whl", hash = "sha256:4d45bc6540dd894663636ced5a98cf4d3ea5765a9f1f18f4ffef6ae0733e05a3"}, + {file = "python-semantic-release-8.7.0.tar.gz", hash = "sha256:6bbd11b1e8ac70e0946ed6d257094c851b2507edfbc393eef6093d0ed1dbe0b4"}, + {file = "python_semantic_release-8.7.0-py3-none-any.whl", hash = "sha256:a016b1cf43a5f3667ce2cfddd8e30b6210a2d52b0e2f6b487aae1164f2540eaa"}, ] pyyaml = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, @@ -1666,8 +1909,8 @@ rich = [ {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] setuptools = [ - {file = "setuptools-58.5.3-py3-none-any.whl", hash = "sha256:a481fbc56b33f5d8f6b33dce41482e64c68b668be44ff42922903b03872590bf"}, - {file = "setuptools-58.5.3.tar.gz", hash = "sha256:dae6b934a965c8a59d6d230d3867ec408bb95e73bd538ff77e71fedf1eaca729"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] shellingham = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, @@ -1685,6 +1928,10 @@ sniffio = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +stack-data = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] storage3 = [ {file = "storage3-0.7.0-py3-none-any.whl", hash = "sha256:dd2d6e68f7a3dc038047ed62fa8bdc5c2e3d6b6e56ee2951195d084bcce71605"}, {file = "storage3-0.7.0.tar.gz", hash = "sha256:9ddecc775cdc04514413bd44b9ec61bc25aad9faadabefdb6e6e88b33756f5fd"}, @@ -1709,9 +1956,9 @@ tomlkit = [ {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] -typer = [ - {file = "typer-0.4.2-py3-none-any.whl", hash = "sha256:023bae00d1baf358a6cc7cea45851639360bb716de687b42b0a4641cd99173f1"}, - {file = "typer-0.4.2.tar.gz", hash = "sha256:b8261c6c0152dd73478b5ba96ba677e5d6948c715c310f7c91079f311f62ec03"}, +traitlets = [ + {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, + {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, ] typing-extensions = [ {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, @@ -1721,14 +1968,6 @@ uc-micro-py = [ {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, ] -unasync = [ - {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, - {file = "unasync-0.5.0.tar.gz", hash = "sha256:b675d87cf56da68bd065d3b7a67ac71df85591978d84c53083c20d79a7e5096d"}, -] -unasync-cli = [ - {file = "unasync-cli-0.0.9.tar.gz", hash = "sha256:ca9d8c57ebb68911f8f8f68f243c7f6d0bb246ee3fd14743bc51c8317e276554"}, - {file = "unasync_cli-0.0.9-py3-none-any.whl", hash = "sha256:f96c42fb2862efa555ce6d6415a5983ceb162aa0e45be701656d20a955c7c540"}, -] urllib3 = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, diff --git a/poetry_scripts.py b/poetry_scripts.py deleted file mode 100644 index c6a38eab..00000000 --- a/poetry_scripts.py +++ /dev/null @@ -1,16 +0,0 @@ -import subprocess - - -def run_cmd(cmd): - subprocess.run(cmd, shell=True, check=True) - - -def run_tests(): - # Install requirements - run_cmd("poetry install") - - # Run pre-commit tests - run_cmd("poetry run pre-commit run --all-files") - - # Generate coverage report - run_cmd("poetry run pytest --cov=./ --cov-report=xml --cov-report=html -vv") diff --git a/pyproject.toml b/pyproject.toml index 524682d8..001556c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,14 @@ name = "supabase" version = "2.4.0" description = "Supabase client for Python." -authors = ["Joel Lee ", "Leon Fedden ", "Daniel Reinón García ", "Leynier Gutiérrez González ", "Anand", "Andrew Smith "] +authors = [ + "Joel Lee ", + "Leon Fedden ", + "Daniel Reinón García ", + "Leynier Gutiérrez González ", + "Anand", + "Andrew Smith " +] homepage = "https://github.com/supabase-community/supabase-py" repository = "https://github.com/supabase-community/supabase-py" documentation = "https://github.com/supabase-community/supabase-py" @@ -14,6 +21,11 @@ classifiers = [ "Operating System :: OS Independent" ] +[tool.poetry.scripts] +lint = 'poetry_scripts:lint' +install = 'poetry_scripts:install' +coverage = 'poetry_scripts:coverage' + [tool.poetry.dependencies] python = "^3.8" postgrest = ">=0.10.8,<0.17.0" @@ -23,23 +35,19 @@ httpx = ">=0.24,<0.26" storage3 = ">=0.5.3,<0.8.0" supafunc = "^0.3.1" -[tool.poetry.dev-dependencies] -pre-commit = "^3.5.0" -black = "^24.2" -pytest = "^8.0.2" +[tool.poetry.group.dev.dependencies] +black = "^23.10" +commitizen = "^3.12.0" flake8 = "^5.0.4" isort = "^5.10.1" +ipdb = "^0.13.13" +mdformat-gfm = "^0.3.6" +pre-commit = "^3.5.0" +pytest = "^8.0.0" pytest-cov = "^4.1.0" -commitizen = "^3.16.0" -python-semantic-release = "^9.1.1" +pytest-sugar = "^1.0.0" python-dotenv = "^1.0.1" - -[tool.poetry.scripts] -tests = 'poetry_scripts:run_tests' - -[tool.poetry.group.dev.dependencies] -unasync-cli = "^0.0.9" -mdformat-gfm = "^0.3.6" +python-semantic-release = "^8.3.0" [tool.semantic_release] version_variables = ["supabase/__version__.py:__version__"] @@ -51,6 +59,9 @@ upload_to_vcs_release = true branch = "main" changelog_components = "semantic_release.changelog.changelog_headers,semantic_release.changelog.compare_url" +[tool.black] +line-length = 120 + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/scripts.py b/scripts.py new file mode 100644 index 00000000..b40ac726 --- /dev/null +++ b/scripts.py @@ -0,0 +1,19 @@ +import subprocess + + +class Command: + @staticmethod + def run(cmd): + return subprocess.run(cmd, shell=True, check=True) + + +def install(): + return Command.run("poetry install --verbose") + + +def lint(): + return Command.run("poetry run pre-commit run --all-files") + + +def coverage(): + return Command.run("poetry run pytest --cov=./ --cov-report=html -vv") diff --git a/supabase/__init__.py b/supabase/__init__.py index 83733bd4..74e81647 100644 --- a/supabase/__init__.py +++ b/supabase/__init__.py @@ -3,16 +3,17 @@ from storage3.utils import StorageException from .__version__ import __version__ -from ._sync.auth_client import SyncSupabaseAuthClient as SupabaseAuthClient -from ._sync.client import ClientOptions -from ._sync.client import SyncClient as Client -from ._sync.client import SyncStorageClient as SupabaseStorageClient -from ._sync.client import create_client -from .lib.realtime_client import SupabaseRealtimeClient +from .client import create_client, SupabaseClient +from .client.services import Auth as SupabaseAuthClient +from .client.services import Storage as SupabaseStorageClient +from .lib import SupabaseRealtimeClient + + __all__ = [ "create_client", "Client", + "SupabaseClient", "SupabaseAuthClient", "SupabaseStorageClient", "SupabaseRealtimeClient", diff --git a/supabase/_async/__init__.py b/supabase/_async/__init__.py deleted file mode 100644 index 9d48db4f..00000000 --- a/supabase/_async/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import annotations diff --git a/supabase/_async/auth_client.py b/supabase/_async/auth_client.py deleted file mode 100644 index 9d69fd96..00000000 --- a/supabase/_async/auth_client.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Dict, Optional - -from gotrue import ( - AsyncGoTrueClient, - AsyncMemoryStorage, - AsyncSupportedStorage, - AuthFlowType, -) -from gotrue.http_clients import AsyncClient - - -class AsyncSupabaseAuthClient(AsyncGoTrueClient): - """SupabaseAuthClient""" - - def __init__( - self, - *, - url: str, - headers: Optional[Dict[str, str]] = None, - storage_key: Optional[str] = None, - auto_refresh_token: bool = True, - persist_session: bool = True, - storage: AsyncSupportedStorage = AsyncMemoryStorage(), - http_client: Optional[AsyncClient] = None, - flow_type: AuthFlowType = "implicit" - ): - """Instantiate SupabaseAuthClient instance.""" - if headers is None: - headers = {} - - AsyncGoTrueClient.__init__( - self, - url=url, - headers=headers, - storage_key=storage_key, - auto_refresh_token=auto_refresh_token, - persist_session=persist_session, - storage=storage, - http_client=http_client, - flow_type=flow_type, - ) diff --git a/supabase/_async/client.py b/supabase/_async/client.py deleted file mode 100644 index 94df4259..00000000 --- a/supabase/_async/client.py +++ /dev/null @@ -1,315 +0,0 @@ -import re -from typing import Any, Dict, Optional, Union - -from gotrue import AsyncMemoryStorage -from gotrue.types import AuthChangeEvent, Session -from httpx import Timeout -from postgrest import ( - AsyncPostgrestClient, - AsyncRequestBuilder, - AsyncRPCFilterRequestBuilder, -) -from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT -from storage3 import AsyncStorageClient -from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT -from supafunc import AsyncFunctionsClient - -from ..lib.client_options import ClientOptions -from .auth_client import AsyncSupabaseAuthClient - - -# Create an exception class when user does not provide a valid url or key. -class SupabaseException(Exception): - def __init__(self, message: str): - self.message = message - super().__init__(self.message) - - -class AsyncClient: - """Supabase client class.""" - - def __init__( - self, - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(storage=AsyncMemoryStorage()), - ): - """Instantiate the client. - - Parameters - ---------- - supabase_url: str - The URL to the Supabase instance that should be connected to. - supabase_key: str - The API key to the Supabase instance that should be connected to. - **options - Any extra settings to be optionally specified - also see the - `DEFAULT_OPTIONS` dict. - """ - - if not supabase_url: - raise SupabaseException("supabase_url is required") - if not supabase_key: - raise SupabaseException("supabase_key is required") - - # Check if the url and key are valid - if not re.match(r"^(https?)://.+", supabase_url): - raise SupabaseException("Invalid URL") - - # Check if the key is a valid JWT - if not re.match( - r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$", supabase_key - ): - raise SupabaseException("Invalid API key") - - self.supabase_url = supabase_url - self.supabase_key = supabase_key - self._auth_token = { - "Authorization": f"Bearer {supabase_key}", - } - options.headers.update(self._get_auth_headers()) - self.options = options - self.rest_url = f"{supabase_url}/rest/v1" - self.realtime_url = f"{supabase_url}/realtime/v1".replace("http", "ws") - self.auth_url = f"{supabase_url}/auth/v1" - self.storage_url = f"{supabase_url}/storage/v1" - self.functions_url = f"{supabase_url}/functions/v1" - self.schema = options.schema - - # Instantiate clients. - self.auth = self._init_supabase_auth_client( - auth_url=self.auth_url, - client_options=options, - ) - # TODO: Bring up to parity with JS client. - # self.realtime: SupabaseRealtimeClient = self._init_realtime_client( - # realtime_url=self.realtime_url, - # supabase_key=self.supabase_key, - # ) - self.realtime = None - self._postgrest = None - self._storage = None - self._functions = None - self.auth.on_auth_state_change(self._listen_to_auth_events) - - @classmethod - async def create( - cls, - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(), - ): - client = cls(supabase_url, supabase_key, options) - client._auth_token = await client._get_token_header() - return client - - def table(self, table_name: str) -> AsyncRequestBuilder: - """Perform a table operation. - - Note that the supabase client uses the `from` method, but in Python, - this is a reserved keyword, so we have elected to use the name `table`. - Alternatively you can use the `.from_()` method. - """ - return self.from_(table_name) - - def from_(self, table_name: str) -> AsyncRequestBuilder: - """Perform a table operation. - - See the `table` method. - """ - return self.postgrest.from_(table_name) - - def rpc( - self, fn: str, params: Optional[Dict[Any, Any]] = None - ) -> AsyncRPCFilterRequestBuilder: - """Performs a stored procedure call. - - Parameters - ---------- - fn : callable - The stored procedure call to be executed. - params : dict of any - Parameters passed into the stored procedure call. - - Returns - ------- - SyncFilterRequestBuilder - Returns a filter builder. This lets you apply filters on the response - of an RPC. - """ - if params is None: - params = {} - return self.postgrest.rpc(fn, params) - - @property - def postgrest(self): - if self._postgrest is None: - self.options.headers.update(self._auth_token) - self._postgrest = self._init_postgrest_client( - rest_url=self.rest_url, - headers=self.options.headers, - schema=self.options.schema, - timeout=self.options.postgrest_client_timeout, - ) - - return self._postgrest - - @property - def storage(self): - if self._storage is None: - headers = self._get_auth_headers() - headers.update(self._auth_token) - self._storage = self._init_storage_client( - storage_url=self.storage_url, - headers=headers, - storage_client_timeout=self.options.storage_client_timeout, - ) - return self._storage - - @property - def functions(self): - if self._functions is None: - headers = self._get_auth_headers() - headers.update(self._auth_token) - self._functions = AsyncFunctionsClient(self.functions_url, headers) - return self._functions - - # async def remove_subscription_helper(resolve): - # try: - # await self._close_subscription(subscription) - # open_subscriptions = len(self.get_subscriptions()) - # if not open_subscriptions: - # error = await self.realtime.disconnect() - # if error: - # return {"error": None, "data": { open_subscriptions}} - # except Exception as e: - # raise e - # return remove_subscription_helper(subscription) - - # async def _close_subscription(self, subscription): - # """Close a given subscription - - # Parameters - # ---------- - # subscription - # The name of the channel - # """ - # if not subscription.closed: - # await self._closeChannel(subscription) - - # def get_subscriptions(self): - # """Return all channels the client is subscribed to.""" - # return self.realtime.channels - - # @staticmethod - # def _init_realtime_client( - # realtime_url: str, supabase_key: str - # ) -> SupabaseRealtimeClient: - # """Private method for creating an instance of the realtime-py client.""" - # return SupabaseRealtimeClient( - # realtime_url, {"params": {"apikey": supabase_key}} - # ) - @staticmethod - def _init_storage_client( - storage_url: str, - headers: Dict[str, str], - storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT, - ) -> AsyncStorageClient: - return AsyncStorageClient(storage_url, headers, storage_client_timeout) - - @staticmethod - def _init_supabase_auth_client( - auth_url: str, - client_options: ClientOptions, - ) -> AsyncSupabaseAuthClient: - """Creates a wrapped instance of the GoTrue Client.""" - return AsyncSupabaseAuthClient( - url=auth_url, - auto_refresh_token=client_options.auto_refresh_token, - persist_session=client_options.persist_session, - storage=client_options.storage, - headers=client_options.headers, - flow_type=client_options.flow_type, - ) - - @staticmethod - def _init_postgrest_client( - rest_url: str, - headers: Dict[str, str], - schema: str, - timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - ) -> AsyncPostgrestClient: - """Private helper for creating an instance of the Postgrest client.""" - return AsyncPostgrestClient( - rest_url, headers=headers, schema=schema, timeout=timeout - ) - - def _create_auth_header(self, token: str): - return { - "Authorization": f"Bearer {token}", - } - - def _get_auth_headers(self) -> Dict[str, str]: - """Helper method to get auth headers.""" - return { - "apiKey": self.supabase_key, - "Authorization": f"Bearer {self.supabase_key}", - } - - async def _get_token_header(self): - try: - session = await self.auth.get_session() - access_token = session.access_token - except Exception as err: - access_token = self.supabase_key - - return self._create_auth_header(access_token) - - def _listen_to_auth_events( - self, event: AuthChangeEvent, session: Union[Session, None] - ): - access_token = self.supabase_key - if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: - # reset postgrest and storage instance on event change - self._postgrest = None - self._storage = None - self._functions = None - access_token = session.access_token if session else self.supabase_key - - self._auth_token = self._create_auth_header(access_token) - - -async def create_client( - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(storage=AsyncMemoryStorage()), -) -> AsyncClient: - """Create client function to instantiate supabase client like JS runtime. - - Parameters - ---------- - supabase_url: str - The URL to the Supabase instance that should be connected to. - supabase_key: str - The API key to the Supabase instance that should be connected to. - **options - Any extra settings to be optionally specified - also see the - `DEFAULT_OPTIONS` dict. - - Examples - -------- - Instantiating the client. - >>> import os - >>> from supabase import create_client, Client - >>> - >>> url: str = os.environ.get("SUPABASE_TEST_URL") - >>> key: str = os.environ.get("SUPABASE_TEST_KEY") - >>> supabase: Client = create_client(url, key) - - Returns - ------- - Client - """ - return await AsyncClient.create( - supabase_url=supabase_url, supabase_key=supabase_key, options=options - ) diff --git a/supabase/_sync/__init__.py b/supabase/_sync/__init__.py deleted file mode 100644 index 9d48db4f..00000000 --- a/supabase/_sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import annotations diff --git a/supabase/_sync/auth_client.py b/supabase/_sync/auth_client.py deleted file mode 100644 index 5a544dd2..00000000 --- a/supabase/_sync/auth_client.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Dict, Optional - -from gotrue import ( - AuthFlowType, - SyncGoTrueClient, - SyncMemoryStorage, - SyncSupportedStorage, -) -from gotrue.http_clients import SyncClient - - -class SyncSupabaseAuthClient(SyncGoTrueClient): - """SupabaseAuthClient""" - - def __init__( - self, - *, - url: str, - headers: Optional[Dict[str, str]] = None, - storage_key: Optional[str] = None, - auto_refresh_token: bool = True, - persist_session: bool = True, - storage: SyncSupportedStorage = SyncMemoryStorage(), - http_client: Optional[SyncClient] = None, - flow_type: AuthFlowType = "implicit" - ): - """Instantiate SupabaseAuthClient instance.""" - if headers is None: - headers = {} - - SyncGoTrueClient.__init__( - self, - url=url, - headers=headers, - storage_key=storage_key, - auto_refresh_token=auto_refresh_token, - persist_session=persist_session, - storage=storage, - http_client=http_client, - flow_type=flow_type, - ) diff --git a/supabase/client.py b/supabase/client.py deleted file mode 100644 index 04092d57..00000000 --- a/supabase/client.py +++ /dev/null @@ -1,24 +0,0 @@ -from postgrest import APIError as PostgrestAPIError -from postgrest import APIResponse as PostgrestAPIResponse -from storage3.utils import StorageException - -from .__version__ import __version__ -from ._sync.auth_client import SyncSupabaseAuthClient as SupabaseAuthClient -from ._sync.client import ClientOptions -from ._sync.client import SyncClient as Client -from ._sync.client import SyncStorageClient as SupabaseStorageClient -from ._sync.client import create_client -from .lib.realtime_client import SupabaseRealtimeClient - -__all__ = [ - "PostgrestAPIError", - "PostgrestAPIResponse", - "StorageException", - "SupabaseAuthClient", - "__version__", - "create_client", - "Client", - "ClientOptions", - "SupabaseStorageClient", - "SupabaseRealtimeClient", -] diff --git a/supabase/client/__init__.py b/supabase/client/__init__.py new file mode 100644 index 00000000..bf4d10e6 --- /dev/null +++ b/supabase/client/__init__.py @@ -0,0 +1,66 @@ + +from postgrest import APIError as PostgrestAPIError +from postgrest import APIResponse as PostgrestAPIResponse +from storage3.utils import StorageException + +from ..__version__ import __version__ +from ..lib.realtime_client import SupabaseRealtimeClient + +from ..lib.client_options import ClientOptions +from .default import APIClient, AsyncAPI +from .default import SyncStorageClient as SupabaseStorageClient + + +class SupabaseClient: + @classmethod + def create(cls, url, key, options=ClientOptions()): + if options.is_async: + return AsyncAPI.create(url, key, options) + return APIClient.create(url, key, options) + + +def create_client( + supabase_url: str, + supabase_key: str, + options: ClientOptions = ClientOptions(), +) -> SupabaseClient: + """Create client function to instantiate supabase client like JS runtime. + + Parameters + ---------- + supabase_url: str + The URL to the Supabase instance that should be connected to. + supabase_key: str + The API key to the Supabase instance that should be connected to. + **options + Any extra settings to be optionally specified - also see the + `DEFAULT_OPTIONS` dict. + + Examples + -------- + Instantiating the client. + >>> import os + >>> from supabase import create_client, Client + >>> + >>> url: str = os.environ.get("SUPABASE_TEST_URL") + >>> key: str = os.environ.get("SUPABASE_TEST_KEY") + >>> supabase: Client = create_client(url, key) + + Returns + ------- + Client + """ + return SupabaseClient.create(supabase_url, supabase_key, options) + + +__all__ = [ + "__version__", + "create_client", + "ClientOptions", + "PostgrestAPIError", + "PostgrestAPIResponse", + "StorageException", + "SupabaseClient", + "SupabaseRealtimeClient", + "SupabaseStorageClient", +] diff --git a/supabase/_sync/client.py b/supabase/client/default.py similarity index 52% rename from supabase/_sync/client.py rename to supabase/client/default.py index faefc6c0..31963c18 100644 --- a/supabase/_sync/client.py +++ b/supabase/client/default.py @@ -1,79 +1,69 @@ + +# pylint: disable=invalid-overridden-method + import re -from typing import Any, Dict, Optional, Union +import contextlib +from typing import Any, Dict, Union -from gotrue import SyncMemoryStorage -from gotrue.types import AuthChangeEvent, Session from httpx import Timeout +from gotrue import AsyncMemoryStorage, AuthChangeEvent, Session, SyncGoTrueClient, SyncMemoryStorage from postgrest import ( - SyncPostgrestClient, - SyncRequestBuilder, - SyncRPCFilterRequestBuilder, + AsyncFilterRequestBuilder, AsyncRequestBuilder, AsyncPostgrestClient, + SyncFilterRequestBuilder, SyncPostgrestClient, SyncRequestBuilder ) from postgrest.constants import DEFAULT_POSTGREST_CLIENT_TIMEOUT -from storage3 import SyncStorageClient -from storage3.constants import DEFAULT_TIMEOUT as DEFAULT_STORAGE_CLIENT_TIMEOUT -from supafunc import SyncFunctionsClient - -from ..lib.client_options import ClientOptions -from .auth_client import SyncSupabaseAuthClient - +from storage3 import DEFAULT_TIMEOUT as DEFAULT_STORAGE_TIMEOUT +from storage3 import AsyncStorageClient, SyncStorageClient +from supafunc import AsyncFunctionsClient, SyncFunctionsClient -# Create an exception class when user does not provide a valid url or key. -class SupabaseException(Exception): - def __init__(self, message: str): - self.message = message - super().__init__(self.message) +from supabase.client.exceptions import ConfigurationError +from supabase.client.services import Auth, Functions, PgREST, Storage +from supabase.lib.client_options import ClientOptions -class SyncClient: - """Supabase client class.""" +class APIClient: + """Supabase synchronous client class.""" def __init__( - self, - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(storage=SyncMemoryStorage()), - ): + self, url: str, key: str, options: ClientOptions = ClientOptions(), + ) -> None: """Instantiate the client. Parameters ---------- - supabase_url: str + url: str The URL to the Supabase instance that should be connected to. - supabase_key: str + key: str The API key to the Supabase instance that should be connected to. **options Any extra settings to be optionally specified - also see the `DEFAULT_OPTIONS` dict. """ - if not supabase_url: - raise SupabaseException("supabase_url is required") - if not supabase_key: - raise SupabaseException("supabase_key is required") + if not (url and isinstance(url, str)): + raise ConfigurationError("supabase url is required") + if not (key and isinstance(key, str)): + raise ConfigurationError("supabase key is required") # Check if the url and key are valid - if not re.match(r"^(https?)://.+", supabase_url): - raise SupabaseException("Invalid URL") + if not re.match(r"^(https?)://.+", url): + raise ConfigurationError("Invalid URL") # Check if the key is a valid JWT - if not re.match( - r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$", supabase_key - ): - raise SupabaseException("Invalid API key") - - self.supabase_url = supabase_url - self.supabase_key = supabase_key - self._auth_token = { - "Authorization": f"Bearer {supabase_key}", - } + if not re.match(r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$", key): + raise ConfigurationError("Invalid API key") + + self.supabase_url = url + self.supabase_key = key + self._auth_token = {"Authorization": f"Bearer {key}"} options.headers.update(self._get_auth_headers()) + options.storage = options.storage or SyncMemoryStorage() self.options = options - self.rest_url = f"{supabase_url}/rest/v1" - self.realtime_url = f"{supabase_url}/realtime/v1".replace("http", "ws") - self.auth_url = f"{supabase_url}/auth/v1" - self.storage_url = f"{supabase_url}/storage/v1" - self.functions_url = f"{supabase_url}/functions/v1" + self.rest_url = f"{self.supabase_url}/rest/v1" + self.realtime_url = f"{self.supabase_url}/realtime/v1".replace("http", "ws") + self.auth_url = f"{self.supabase_url}/auth/v1" + self.storage_url = f"{self.supabase_url}/storage/v1" + self.functions_url = f"{self.supabase_url}/functions/v1" self.schema = options.schema # Instantiate clients. @@ -93,13 +83,8 @@ def __init__( self.auth.on_auth_state_change(self._listen_to_auth_events) @classmethod - def create( - cls, - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(), - ): - client = cls(supabase_url, supabase_key, options) + def create(cls, url: str, key: str, options: ClientOptions = ClientOptions()): + client = cls(url, key, options) client._auth_token = client._get_token_header() return client @@ -119,9 +104,7 @@ def from_(self, table_name: str) -> SyncRequestBuilder: """ return self.postgrest.from_(table_name) - def rpc( - self, fn: str, params: Optional[Dict[Any, Any]] = None - ) -> SyncRPCFilterRequestBuilder: + def rpc(self, fn: str, params: Dict[Any, Any]) -> SyncFilterRequestBuilder: """Performs a stored procedure call. Parameters @@ -137,12 +120,10 @@ def rpc( Returns a filter builder. This lets you apply filters on the response of an RPC. """ - if params is None: - params = {} return self.postgrest.rpc(fn, params) @property - def postgrest(self): + def postgrest(self) -> SyncPostgrestClient: if self._postgrest is None: self.options.headers.update(self._auth_token) self._postgrest = self._init_postgrest_client( @@ -150,12 +131,13 @@ def postgrest(self): headers=self.options.headers, schema=self.options.schema, timeout=self.options.postgrest_client_timeout, + client_options=self.options ) return self._postgrest @property - def storage(self): + def storage(self) -> SyncStorageClient: if self._storage is None: headers = self._get_auth_headers() headers.update(self._auth_token) @@ -163,15 +145,16 @@ def storage(self): storage_url=self.storage_url, headers=headers, storage_client_timeout=self.options.storage_client_timeout, + client_options=self.options ) return self._storage @property - def functions(self): + def functions(self) -> SyncFunctionsClient: if self._functions is None: headers = self._get_auth_headers() headers.update(self._auth_token) - self._functions = SyncFunctionsClient(self.functions_url, headers) + self._functions = Functions.create(self.functions_url, headers, is_async=self.options.is_async) return self._functions # async def remove_subscription_helper(resolve): @@ -213,23 +196,21 @@ def functions(self): def _init_storage_client( storage_url: str, headers: Dict[str, str], - storage_client_timeout: int = DEFAULT_STORAGE_CLIENT_TIMEOUT, + storage_client_timeout: int = DEFAULT_STORAGE_TIMEOUT, + client_options=ClientOptions() ) -> SyncStorageClient: - return SyncStorageClient(storage_url, headers, storage_client_timeout) + return Storage.create(storage_url, headers, storage_client_timeout, is_async=client_options.is_async) @staticmethod - def _init_supabase_auth_client( - auth_url: str, - client_options: ClientOptions, - ) -> SyncSupabaseAuthClient: - """Creates a wrapped instance of the GoTrue Client.""" - return SyncSupabaseAuthClient( + def _init_supabase_auth_client(auth_url: str, client_options: ClientOptions) -> SyncGoTrueClient: + return Auth.create( url=auth_url, auto_refresh_token=client_options.auto_refresh_token, persist_session=client_options.persist_session, storage=client_options.storage, headers=client_options.headers, flow_type=client_options.flow_type, + is_async=client_options.is_async, ) @staticmethod @@ -238,16 +219,12 @@ def _init_postgrest_client( headers: Dict[str, str], schema: str, timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, + client_options: ClientOptions = ClientOptions() ) -> SyncPostgrestClient: - """Private helper for creating an instance of the Postgrest client.""" - return SyncPostgrestClient( - rest_url, headers=headers, schema=schema, timeout=timeout - ) + return PgREST.create(rest_url, headers, schema, timeout, is_async=client_options.is_async) def _create_auth_header(self, token: str): - return { - "Authorization": f"Bearer {token}", - } + return {"Authorization": f"Bearer {token}"} def _get_auth_headers(self) -> Dict[str, str]: """Helper method to get auth headers.""" @@ -257,17 +234,14 @@ def _get_auth_headers(self) -> Dict[str, str]: } def _get_token_header(self): - try: + access_token = self.supabase_key + with contextlib.suppress(Exception): session = self.auth.get_session() - access_token = session.access_token - except Exception as err: - access_token = self.supabase_key - + if session: + access_token = session.access_token return self._create_auth_header(access_token) - def _listen_to_auth_events( - self, event: AuthChangeEvent, session: Union[Session, None] - ): + def _listen_to_auth_events(self, event: AuthChangeEvent, session: Union[Session, None]): access_token = self.supabase_key if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: # reset postgrest and storage instance on event change @@ -279,37 +253,75 @@ def _listen_to_auth_events( self._auth_token = self._create_auth_header(access_token) -def create_client( - supabase_url: str, - supabase_key: str, - options: ClientOptions = ClientOptions(storage=SyncMemoryStorage()), -) -> SyncClient: - """Create client function to instantiate supabase client like JS runtime. - - Parameters - ---------- - supabase_url: str - The URL to the Supabase instance that should be connected to. - supabase_key: str - The API key to the Supabase instance that should be connected to. - **options - Any extra settings to be optionally specified - also see the - `DEFAULT_OPTIONS` dict. - - Examples - -------- - Instantiating the client. - >>> import os - >>> from supabase import create_client, Client - >>> - >>> url: str = os.environ.get("SUPABASE_TEST_URL") - >>> key: str = os.environ.get("SUPABASE_TEST_KEY") - >>> supabase: Client = create_client(url, key) - - Returns - ------- - Client - """ - return SyncClient.create( - supabase_url=supabase_url, supabase_key=supabase_key, options=options - ) +class AsyncAPI(APIClient): + """Supabase asynchronous client class""" + + def __init__( + self, url: str, key: str, options: ClientOptions = ClientOptions(is_async=True) + ) -> None: + options.storage = options.storage or AsyncMemoryStorage() + if not isinstance(options.storage, AsyncMemoryStorage): + raise ConfigurationError( + "AsyncAPI client storage must be configured to use AsyncMemoryStorage") + + super().__init__(url, key, options) + + @property + def functions(self) -> AsyncFunctionsClient: + return super().functions + + @property + def postgrest(self) -> AsyncPostgrestClient: + return super().postgrest + + @property + def storage(self) -> AsyncStorageClient: + return super().storage + + def table(self, table_name: str) -> AsyncRequestBuilder: + """Perform an asynchronous table operation. + + Note that the supabase client uses the `from` method, but in Python, + this is a reserved keyword, so we have elected to use the name `table`. + Alternatively you can use the `.from_()` method. + """ + return super().table(table_name) + + def from_(self, table_name: str) -> AsyncRequestBuilder: + """Perform an asynchronous table operation. + + See the `table` method. + """ + return super().from_(table_name) + + def rpc(self, fn: str, params: Dict[Any, Any]) -> AsyncFilterRequestBuilder: + """Performs an asynchronous stored procedure call. + + Parameters + ---------- + fn : callable + The stored procedure call to be executed. + params : dict of any + Parameters passed into the stored procedure call. + + Returns + ------- + AsyncFilterRequestBuilder + Returns a filter builder. This lets you apply filters on the response + of an RPC. + """ + return super().rpc(fn, params) + + @classmethod + async def create(cls, url: str, key: str, options: ClientOptions = ClientOptions(is_async=True)): + client = cls(url, key, options) + client._auth_token = await client._get_token_header() + return client + + async def _get_token_header(self): + access_token = self.supabase_key + with contextlib.suppress(Exception): + session = await self.auth.get_session() + if session: + access_token = session.acess_token + return self._create_auth_header(access_token) diff --git a/supabase/client/exceptions.py b/supabase/client/exceptions.py new file mode 100644 index 00000000..dbee66c4 --- /dev/null +++ b/supabase/client/exceptions.py @@ -0,0 +1,9 @@ +""" +Create an exception class when user does not provide a valid url or key. +""" + + +class ConfigurationError(Exception): + def __init__(self, message: str): + self.message = message + super().__init__(self.message) diff --git a/supabase/client/services.py b/supabase/client/services.py new file mode 100644 index 00000000..1e13354e --- /dev/null +++ b/supabase/client/services.py @@ -0,0 +1,58 @@ + +from typing import Union + +from gotrue import AsyncMemoryStorage, AsyncGoTrueClient, SyncMemoryStorage, SyncGoTrueClient +from postgrest import AsyncPostgrestClient, SyncPostgrestClient +from storage3 import SyncStorageClient, AsyncStorageClient +from supafunc import AsyncFunctionsClient, SyncFunctionsClient + + +class Auth: + @classmethod + def create(cls, *, url, headers=None, storage_key=None, auto_refresh_token=None, + persist_session=True, storage=None, http_client=None, flow_type='implicit', + **kwargs) -> Union[SyncGoTrueClient, AsyncGoTrueClient]: + args = { + "url": url, + "headers": headers or {}, + "storage_key": storage_key, + "auto_refresh_token": auto_refresh_token, + "persist_session": persist_session, + "storage": storage, + "http_client": http_client, + "flow_type": flow_type, + } + if bool(kwargs.get('is_async', False)): + # Ensure async memory storage + args.update({'storage': AsyncMemoryStorage()}) + return AsyncGoTrueClient(**args) + + # Ensure sync memory storage + args.update({'storage': SyncMemoryStorage()}) + return SyncGoTrueClient(**args) + + +class Functions: + @classmethod + def create(cls, url, headers, **kwargs) -> Union[SyncFunctionsClient, AsyncFunctionsClient]: + if bool(kwargs.get('is_async', False)): + return AsyncFunctionsClient(url, headers) + return SyncFunctionsClient(url, headers) + + +class PgREST: + @classmethod + def create( + cls, url, headers=None, schema=None, timeout=None, **kwargs + ) -> Union[SyncPostgrestClient, AsyncPostgrestClient]: + if bool(kwargs.get('is_async', False)): + return AsyncPostgrestClient(url, headers=headers, schema=schema, timeout=timeout) + return SyncPostgrestClient(url, headers=headers, schema=schema, timeout=timeout) + + +class Storage: + @classmethod + def create(cls, url, headers=None, timeout=None, **kwargs) -> Union[SyncStorageClient, AsyncStorageClient]: + if bool(kwargs.get('is_async', False)): + return AsyncStorageClient(url, headers, timeout) + return SyncStorageClient(url, headers, timeout) diff --git a/supabase/lib/__init__.py b/supabase/lib/__init__.py index b1f57430..e01fa349 100644 --- a/supabase/lib/__init__.py +++ b/supabase/lib/__init__.py @@ -1,4 +1,6 @@ -from supabase._async import auth_client -from supabase.lib import realtime_client -__all__ = ["auth_client", "realtime_client"] +from .client_options import ClientOptions +from .realtime_client import SupabaseRealtimeClient + + +__all__ = ["ClientOptions", "SupabaseRealtimeClient"] diff --git a/supabase/lib/client_options.py b/supabase/lib/client_options.py index 0c55a159..352e6826 100644 --- a/supabase/lib/client_options.py +++ b/supabase/lib/client_options.py @@ -34,9 +34,7 @@ class ClientOptions: realtime: Optional[Dict[str, Any]] = None """Options passed to the realtime-py instance""" - postgrest_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_POSTGREST_CLIENT_TIMEOUT + postgrest_client_timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT """Timeout passed to the SyncPostgrestClient instance.""" storage_client_timeout: Union[int, float, Timeout] = DEFAULT_STORAGE_CLIENT_TIMEOUT @@ -45,6 +43,9 @@ class ClientOptions: flow_type: AuthFlowType = "implicit" """flow type to use for authentication""" + is_async: bool = False + """Signals if a client should asynchronous operations""" + def replace( self, schema: Optional[str] = None, @@ -53,29 +54,21 @@ def replace( persist_session: Optional[bool] = None, storage: Optional[SyncSupportedStorage] = None, realtime: Optional[Dict[str, Any]] = None, - postgrest_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, - storage_client_timeout: Union[ - int, float, Timeout - ] = DEFAULT_STORAGE_CLIENT_TIMEOUT, + postgrest_client_timeout: Union[int, float, Timeout] = DEFAULT_POSTGREST_CLIENT_TIMEOUT, + storage_client_timeout: Union[int, float, Timeout] = DEFAULT_STORAGE_CLIENT_TIMEOUT, flow_type: Optional[AuthFlowType] = None, + is_async: bool = None, ) -> "ClientOptions": """Create a new SupabaseClientOptions with changes""" client_options = ClientOptions() client_options.schema = schema or self.schema client_options.headers = headers or self.headers - client_options.auto_refresh_token = ( - auto_refresh_token or self.auto_refresh_token - ) + client_options.auto_refresh_token = auto_refresh_token or self.auto_refresh_token client_options.persist_session = persist_session or self.persist_session client_options.storage = storage or self.storage client_options.realtime = realtime or self.realtime - client_options.postgrest_client_timeout = ( - postgrest_client_timeout or self.postgrest_client_timeout - ) - client_options.storage_client_timeout = ( - storage_client_timeout or self.storage_client_timeout - ) + client_options.postgrest_client_timeout = postgrest_client_timeout or self.postgrest_client_timeout + client_options.storage_client_timeout = storage_client_timeout or self.storage_client_timeout client_options.flow_type = flow_type or self.flow_type + client_options.is_async = is_async or self.is_async return client_options diff --git a/supabase/lib/realtime_client.py b/supabase/lib/realtime_client.py index 9f5eae63..4e126606 100644 --- a/supabase/lib/realtime_client.py +++ b/supabase/lib/realtime_client.py @@ -6,11 +6,7 @@ class SupabaseRealtimeClient: def __init__(self, socket: Socket, schema: str, table_name: str): - topic = ( - f"realtime:{schema}" - if table_name == "*" - else f"realtime:{schema}:{table_name}" - ) + topic = f"realtime:{schema}" if table_name == "*" else f"realtime:{schema}:{table_name}" self.subscription = socket.set_channel(topic) @staticmethod @@ -43,10 +39,6 @@ def cb(payload): def subscribe(self, callback: Callable[..., Any]): # TODO: Handle state change callbacks for error and close self.subscription.join().on("ok", callback("SUBSCRIBED")) - self.subscription.join().on( - "error", lambda x: callback("SUBSCRIPTION_ERROR", x) - ) - self.subscription.join().on( - "timeout", lambda: callback("RETRYING_AFTER_TIMEOUT") - ) + self.subscription.join().on("error", lambda x: callback("SUBSCRIPTION_ERROR", x)) + self.subscription.join().on("timeout", lambda: callback("RETRYING_AFTER_TIMEOUT")) return self.subscription diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..bf1a11a9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,47 @@ +import os + +from unittest import IsolatedAsyncioTestCase, TestCase +from gotrue import AsyncMemoryStorage + +from supabase import SupabaseClient +from supabase.lib.client_options import ClientOptions + + +class TestClient(TestCase): + @classmethod + def setUpClass(cls): + cls.table = "countries" + cls.url = os.getenv("SUPABASE_URL") + cls.key = os.getenv("SUPABASE_KEY") + cls.schema = os.getenv("SUPABASE_TEST_SCHEMA", "public") + cls.countries = set(["Argentina", "Brazil", "Canada", "Dubai"]) + + def setUp(self): + self.opts = ClientOptions(schema=self.schema) + self.client = SupabaseClient.create(self.url, self.key, options=self.opts) + for country in self.countries: + self.client.table(self.table).insert({"name": country}).execute() + + def tearDown(self): + for country in self.countries: + self.client.table(self.table).delete().eq("name", country).execute() + + +class AsyncTestClient(IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + cls.table = "countries" + cls.url = os.getenv("SUPABASE_URL") + cls.key = os.getenv("SUPABASE_KEY") + cls.schema = os.getenv("SUPABASE_TEST_SCHEMA", "public") + cls.countries = set(["Argentina", "Brazil", "Canada", "Dubai"]) + + async def asyncSetUp(self): + self.opts = ClientOptions(schema=self.schema, is_async=True, storage=AsyncMemoryStorage()) + self.client = await SupabaseClient.create(self.url, self.key, options=self.opts) + for country in self.countries: + await self.client.table(self.table).insert({"name": country}).execute() + + async def asyncTearDown(self): + for country in self.countries: + await self.client.table(self.table).delete().eq("name", country).execute() diff --git a/tests/conftest.py b/tests/conftest.py index 0f616ade..3bd6964a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import pytest from dotenv import load_dotenv -from supabase import Client, create_client +from supabase import SupabaseClient, create_client def pytest_configure(config) -> None: @@ -13,7 +13,7 @@ def pytest_configure(config) -> None: @pytest.fixture(scope="session") -def supabase() -> Client: +def supabase() -> SupabaseClient: url = os.environ.get("SUPABASE_TEST_URL") assert url is not None, "Must provide SUPABASE_TEST_URL environment variable" key = os.environ.get("SUPABASE_TEST_KEY") diff --git a/tests/test_client.py b/tests/test_client.py index c4bf0d27..d8a1c34d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,17 +1,20 @@ -from __future__ import annotations +import os +import unittest -from typing import Any +from supabase import SupabaseClient, create_client +from supabase.client.exceptions import ConfigurationError -import pytest +class TestDefaultClient(unittest.TestCase): + def setUp(self) -> None: + self.url = os.getenv("SUPABASE_URL") + self.key = os.getenv("SUPABASE_KEY") + self.client = SupabaseClient.create(self.url, self.key) -@pytest.mark.xfail( - reason="None of these values should be able to instantiate a client object" -) -@pytest.mark.parametrize("url", ["", None, "valeefgpoqwjgpj", 139, -1, {}, []]) -@pytest.mark.parametrize("key", ["", None, "valeefgpoqwjgpj", 139, -1, {}, []]) -def test_incorrect_values_dont_instantiate_client(url: Any, key: Any) -> None: - """Ensure we can't instantiate client with invalid values.""" - from supabase import Client, create_client - - _: Client = create_client(url, key) + def test_invalid_create_client(self): + for url in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + for key in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + try: + self.assertIsNone(create_client(url, key)) + except ConfigurationError: + self.assertTrue(1 == 1) diff --git a/tests/test_client_default.py b/tests/test_client_default.py new file mode 100644 index 00000000..09ecd5f0 --- /dev/null +++ b/tests/test_client_default.py @@ -0,0 +1,141 @@ +import os +import unittest + +from postgrest.exceptions import APIError + +from supabase import SupabaseClient, create_client +from supabase.client.exceptions import ConfigurationError + +from . import AsyncTestClient, TestClient + + +def test_create_client(): + for url in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + for key in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + try: + assert create_client(url, key) + except ConfigurationError: + assert True + assert create_client(os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY")) + + +class TestSync(TestClient): + def test_create(self): + for url in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + for key in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + with self.assertRaises(ConfigurationError): + SupabaseClient.create(url, key) + self.assertIsNotNone(SupabaseClient.create(self.url, self.key)) + + def test_table_delete(self): + with self.assertRaises(APIError): + self.client.table("foobar").delete().eq("id", 1).execute() + + op = self.client.table(self.table).delete().eq("name", "Canada").execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Canada") + + def test_table_insert(self): + with self.assertRaises(APIError): + self.client.table("foobar").insert({"name": "barfoo"}).execute() + + self.countries.add("Wadiya") + op = self.client.table(self.table).insert({"name": "Wadiya"}).execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Wadiya") + + def test_table_select(self): + with self.assertRaises(APIError): + self.client.table("foobar").select().eq("id", 1).execute() + + op = self.client.table(self.table).select("*").execute() + self.assertIsNotNone(op) + self.assertEqual(len(op.data), len(self.countries)) + + op = self.client.table(self.table).select("*").eq("name", "Canada").execute() + self.assertIsNotNone(op) + self.assertEqual(len(op.data), 1) + + def test_table_update(self): + with self.assertRaises(APIError): + self.client.table("foobar").update({"foo": "bar"}).execute() + self.client.table(self.table).update({"foo": "bar"}).eq("name", "Canada").execute() + self.client.table(self.table).update({"name": "Australia"}).eq("foo", "bar").execute() + + self.countries.add("Denmark") + op = self.client.table(self.table).update({"name": "Denmark"}).eq("name", "Dubai").execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Denmark") + + def test_rpc(self): + with self.assertRaises(APIError): + self.client.rpc("dead", {"foo": "bar"}).execute() + + op = self.client.rpc("alive", {}).execute() + self.assertTrue(op.data) + + @unittest.skip("TODO") + def test_listen_to_auth_events(self): + pass + + +class TestAsync(AsyncTestClient): + async def test_create(self): + for url in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + for key in ("", None, "valeefgpoqwjgpj", 139, -1, {}, []): + with self.assertRaises(ConfigurationError): + await SupabaseClient.create(url, key, options=self.opts) + self.assertIsNotNone(await SupabaseClient.create(self.url, self.key, options=self.opts)) + + async def test_table_delete(self): + with self.assertRaises(APIError): + await self.client.table("foobar").delete().eq("id", 1).execute() + + op = await self.client.table(self.table).delete().eq("name", "Canada").execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Canada") + + async def test_table_insert(self): + with self.assertRaises(APIError): + await self.client.table("foobar").insert({"name": "barfoo"}).execute() + + self.countries.add("Wadiya") + op = await self.client.table(self.table).insert({"name": "Wadiya"}).execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Wadiya") + + async def test_table_select(self): + with self.assertRaises(APIError): + await self.client.table("foobar").select().eq("id", 1).execute() + + op = await self.client.table(self.table).select("*").execute() + self.assertIsNotNone(op) + self.assertEqual(len(op.data), len(self.countries)) + + op = await self.client.table(self.table).select("*").eq("name", "Canada").execute() + self.assertIsNotNone(op) + self.assertEqual(len(op.data), 1) + + async def test_table_update(self): + with self.assertRaises(APIError): + await self.client.table("foobar").update({"foo": "bar"}).execute() + await self.client.table(self.table).update({"foo": "bar"}).eq("name", "Canada").execute() + await self.client.table(self.table).update({"name": "Australia"}).eq("foo", "bar").execute() + + self.countries.add("Denmark") + op = await self.client.table(self.table).update({"name": "Denmark"}).eq("name", "Dubai").execute() + self.assertIsNotNone(op) + for d in op.data: + self.assertEqual(d.get("name"), "Denmark") + + async def test_rpc(self): + with self.assertRaises(APIError): + await self.client.rpc("dead", {"foo": "bar"}).execute() + + op = await self.client.rpc("alive", {}).execute() + self.assertTrue(op.data) diff --git a/tests/test_client_options.py b/tests/test_client_options.py index 75361722..d958a45d 100644 --- a/tests/test_client_options.py +++ b/tests/test_client_options.py @@ -1,43 +1,38 @@ +import unittest + from gotrue import SyncMemoryStorage from supabase.lib.client_options import ClientOptions -class TestClientOptions: - def test_replace_returns_updated_options(self): - storage = SyncMemoryStorage() - storage.set_item("key", "value") - options = ClientOptions( +class TestClientOptions(unittest.TestCase): + def setUp(self) -> None: + self.storage = SyncMemoryStorage() + self.storage.set_item("key", "value") + self.options = ClientOptions( schema="schema", headers={"key": "value"}, auto_refresh_token=False, persist_session=False, - storage=storage, + storage=self.storage, realtime={"key": "value"}, ) - actual = options.replace(schema="new schema") - expected = ClientOptions( - schema="new schema", - headers={"key": "value"}, - auto_refresh_token=False, - persist_session=False, - storage=storage, - realtime={"key": "value"}, + def test_replace_returns_updated_options(self) -> None: + self.assertEqual( + self.options.replace(schema="new schema"), + ClientOptions( + schema="new schema", + headers={"key": "value"}, + auto_refresh_token=False, + persist_session=False, + storage=self.storage, + realtime={"key": "value"}, + ), ) - assert actual == expected - def test_replace_updates_only_new_options(self): - # Arrange - storage = SyncMemoryStorage() - storage.set_item("key", "value") - options = ClientOptions(storage=storage) - new_options = options.replace() - - # Act - new_options.storage.set_item("key", "new_value") - - # Assert - assert options.storage.get_item("key") == "new_value" - assert new_options.storage.get_item("key") == "new_value" + modified = self.options.replace() + modified.storage.set_item("key", "new_value") + self.assertEqual(modified.storage.get_item("key"), "new_value") + self.assertEqual(self.options.storage.get_item("key"), "new_value") diff --git a/tests/test_function_configuration.py b/tests/test_function_configuration.py index 0e451d03..f09b0c18 100644 --- a/tests/test_function_configuration.py +++ b/tests/test_function_configuration.py @@ -1,14 +1,18 @@ -import supabase +import unittest +from supabase import SupabaseClient -def test_functions_client_initialization() -> None: - ref = "ooqqmozurnggtljmjkii" - url = f"https://{ref}.supabase.co" - # Sample JWT Key - key = "xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx" - sp = supabase.Client(url, key) - assert sp.functions_url == f"https://{ref}.supabase.co/functions/v1" - url = "https://localhost:54322" - sp_local = supabase.Client(url, key) - assert sp_local.functions_url == f"{url}/functions/v1" +class TestFunctionsClient(unittest.TestCase): + def setUp(self) -> None: + self.ref = "ooqqmozurnggtljmjkii" + self.url = f"https://{self.ref}.supabase.co" + self.key = "xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxxxx" + self.client = SupabaseClient.create(self.url, self.key) + + def test_functions_client_initialization(self): + assert self.client.functions_url == f"https://{self.ref}.supabase.co/functions/v1" + + url = "https://localhost:54322" + client = SupabaseClient.create(url, self.key) + assert client.functions_url == f"{url}/functions/v1" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..c6310cad --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +[tox] +isolated_build = True +envlist = py37, py38, py39, py310, py311 + +[gh] +python = + 3.11 = py311 + 3.10 = py310 + 3.9 = py39 + 3.8 = py38 + +[testenv] +allowlist_externals = poetry +require_locked_deps = true +poetry_dep_groups = + dev +setenv = + PYTHONPATH = {toxinidir} +passenv = + SUPABASE_URL + SUPABASE_KEY +commands = + poetry install --no-root -v + poetry run pytest -s --cov=./ --cov-report=term --cov-report=html + +; Eg: Launch a single test +; +; $ tox -e debug -- tests/test_client_default.py::TestSync::test_from_ +[testenv:debug] +deps = pytest +commands = + poetry install --no-root + poetry run pytest -s --disable-warnings {posargs} + +[testenv:format] +deps = pre-commit +commands = pre-commit run --all-files --show-diff-on-failure