├── .codespellrc ├── .flake8 ├── .github ├── scripts │ └── remove_source_code.py └── workflows │ ├── packing.yml │ ├── reusable_testing.yml │ └── testing.yml ├── .gitignore ├── .gitmodules ├── .pylintrc ├── .readthedocs.yaml ├── AUTHORS ├── CHANGELOG.md ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── README.txt ├── debian ├── changelog ├── compat ├── control ├── rules └── source │ ├── format │ └── options ├── docs ├── requirements.txt └── source │ ├── _static │ └── favicon │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── generate-png.sh │ │ ├── icon-128x128.png │ │ ├── icon-16x16.png │ │ ├── icon-196x196.png │ │ ├── icon-32x32.png │ │ ├── icon-96x96.png │ │ └── icon.svg │ ├── api │ ├── module-tarantool.rst │ ├── submodule-connection-pool.rst │ ├── submodule-connection.rst │ ├── submodule-crud.rst │ ├── submodule-dbapi.rst │ ├── submodule-error.rst │ ├── submodule-mesh-connection.rst │ ├── submodule-msgpack-ext-types.rst │ ├── submodule-msgpack-ext.rst │ ├── submodule-request.rst │ ├── submodule-response.rst │ ├── submodule-schema.rst │ ├── submodule-space.rst │ ├── submodule-types.rst │ └── submodule-utils.rst │ ├── conf.py │ ├── dev-guide.rst │ ├── index.rst │ └── quick-start.rst ├── requirements-test.txt ├── requirements.txt ├── rpm └── SPECS │ └── python-tarantool.spec ├── setup.py ├── tarantool ├── __init__.py ├── connection.py ├── connection_pool.py ├── const.py ├── crud.py ├── dbapi.py ├── error.py ├── mesh_connection.py ├── msgpack_ext │ ├── __init__.py │ ├── datetime.py │ ├── decimal.py │ ├── error.py │ ├── interval.py │ ├── packer.py │ ├── types │ │ ├── __init__.py │ │ ├── datetime.py │ │ ├── interval.py │ │ └── timezones │ │ │ ├── __init__.py │ │ │ ├── gen-timezones.sh │ │ │ └── timezones.py │ ├── unpacker.py │ └── uuid.py ├── request.py ├── response.py ├── schema.py ├── space.py ├── types.py └── utils.py └── test ├── __init__.py ├── data ├── ca.crt ├── empty ├── generate.sh ├── invalidhost.crt ├── invalidpasswords ├── localhost.crt ├── localhost.enc.key ├── localhost.key └── passwords ├── setup_command.py └── suites ├── __init__.py ├── box.lua ├── crud_mock_server.lua ├── crud_server.lua ├── lib ├── __init__.py ├── remote_tarantool_server.py ├── skip.py ├── tarantool_admin.py ├── tarantool_python_ci.lua └── tarantool_server.py ├── sidecar.py ├── test_connection.py ├── test_crud.py ├── test_datetime.py ├── test_dbapi.py ├── test_decimal.py ├── test_dml.py ├── test_encoding.py ├── test_error_ext.py ├── test_execute.py ├── test_interval.py ├── test_mesh.py ├── test_package.py ├── test_pool.py ├── test_protocol.py ├── test_push.py ├── test_reconnect.py ├── test_schema.py ├── test_socket_fd.py ├── test_ssl.py ├── test_uuid.py └── utils.py /.codespellrc: -------------------------------------------------------------------------------- 1 | [codespell] 2 | skip = tarantool/msgpack_ext/types/timezones/timezones.py 3 | ignore-words-list = ans,gost,ro,assertIn 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # Use pylint for lines length check 3 | ignore=E501,W503 4 | -------------------------------------------------------------------------------- /.github/scripts/remove_source_code.py: -------------------------------------------------------------------------------- 1 | import os, shutil 2 | 3 | required_paths = ['.git', '.github', 'test', 'requirements-test.txt', 'Makefile'] 4 | 5 | for path in os.listdir(): 6 | if path in required_paths: 7 | continue 8 | 9 | if os.path.isfile(path) or os.path.islink(path): 10 | os.remove(path) 11 | elif os.path.isdir(path): 12 | shutil.rmtree(path) 13 | else: 14 | raise ValueError(f"{path} is not a file, link or dir") 15 | -------------------------------------------------------------------------------- /.github/workflows/reusable_testing.yml: -------------------------------------------------------------------------------- 1 | name: reusable_testing 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | artifact_name: 7 | description: The name of the tarantool build artifact 8 | default: ubuntu-focal 9 | required: false 10 | type: string 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - name: Clone the tarantool-python connector 17 | uses: actions/checkout@v4 18 | with: 19 | repository: ${{ github.repository_owner }}/tarantool-python 20 | 21 | - name: Download the tarantool build artifact 22 | uses: actions/download-artifact@v4.1.8 23 | with: 24 | name: ${{ inputs.artifact_name }} 25 | 26 | - name: Install tarantool 27 | # Now we're lucky: all dependencies are already installed. Check package 28 | # dependencies when migrating to other OS version. 29 | run: sudo dpkg -i tarantool*.deb 30 | 31 | - name: Setup python3 for tests 32 | uses: actions/setup-python@v5 33 | with: 34 | python-version: '3.11' 35 | 36 | - name: Install connector requirements 37 | run: pip3 install -r requirements.txt 38 | 39 | - name: Install test requirements 40 | run: pip3 install -r requirements-test.txt 41 | 42 | - name: Install the crud module for testing purposes 43 | run: | 44 | curl -L https://tarantool.io/release/2/installer.sh | bash 45 | sudo apt install -y tt 46 | pip3 install cmake==3.15.3 47 | tt rocks install crud 48 | 49 | - run: make test 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.wpr 4 | *~ 5 | /build/ 6 | /dist/ 7 | /MANIFEST 8 | *.snap 9 | sophia 10 | *.egg-info 11 | .tox 12 | 13 | *.xlog 14 | *.snap 15 | *.vylog 16 | 17 | # Vim Swap files 18 | *.sw[a-z] 19 | .idea 20 | 21 | venv/* 22 | 23 | .eggs 24 | 25 | tarantool/version.py 26 | pip_dist 27 | 28 | # Cannot ignore a directory and negate a single file 29 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 30 | rpm/SOURCES 31 | rpm/SRPMS 32 | rpm/BUILDROOT 33 | rpm/BUILD 34 | rpm/RPMS 35 | 36 | rpm_dist 37 | 38 | debian/python3-tarantool 39 | debian/*debhelper* 40 | debian/files 41 | debian/*.substvars 42 | 43 | deb_dist 44 | 45 | test/data/*.crt 46 | !test/data/ca.crt 47 | !test/data/invalidhost.crt 48 | !test/data/localhost.crt 49 | test/data/*.csr 50 | test/data/*.ext 51 | test/data/*.key 52 | !test/data/localhost.key 53 | !test/data/localhost.enc.key 54 | test/data/*.pem 55 | test/data/*.srl 56 | 57 | .rocks 58 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/.gitmodules -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | 3 | # Good variable names which should always be accepted, separated by a comma 4 | good-names=i,j,k,ex,Run,_,ok,t,tz 5 | 6 | [FORMAT] 7 | # Allow links in docstings, allow tables 8 | ignore-long-lines=^(?:\s*(# )?(?:\.\.\s.+?:)?\s*?)|(\s\+.+\+)|(\s\|.+\|)$ 9 | 10 | [MESSAGES CONTROL] 11 | # Ignore unknown options to support per-entity ignores for newest warnings/errors 12 | # which are not supported for older versions. 13 | disable=unknown-option-value 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-20.04" 5 | tools: 6 | python: "3.10" 7 | jobs: 8 | post_checkout: 9 | - git fetch --unshallow 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | - requirements: docs/requirements.txt 16 | 17 | sphinx: 18 | configuration: docs/source/conf.py 19 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | In the order of a first contribution. 2 | 3 | Konstantin Cherkasoff 4 | Roman Tsisyk 5 | Vlad Emelyanov 6 | Dmitry E. Oboukhov 7 | Eugine Blikh 8 | Sokolov Yura 9 | Konstantin Osipov 10 | Andrey Drozdov 11 | Alexander 12 | Vladimir Rudnyh 13 | Dmitry Shveenkov 14 | Nikolay Meleshenko 15 | Alexander Abashkin 16 | Nick Zavaritsky 17 | Mikhail Kaplenko 18 | Vladimir Marunov 19 | Zajcev Evgeny 20 | Konstantin Nazarov 21 | Ilya Markov 22 | Dmitry Zimnukhov 23 | Alexey Gadzhiev 24 | Konstantin Belyavskiy 25 | Michael Filonenko 26 | Alexander Turenko 27 | Sergei Voronezhskii 28 | Alexander V. Tikhonov 29 | Oleg Babin 30 | Denis Ignatenko 31 | Ilya Konyukhov 32 | Vladislav Shpilevoy 33 | Artem Morozov 34 | Sergey Bronnikov 35 | Yaroslav Lobankov 36 | Georgy Moiseev 37 | Oleg Jukovec 38 | Ilya Grishnov 39 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installing tarantool-python 2 | =========================== 3 | 4 | Here is the simplest (and recommended) way to install tarantool-python. 5 | 6 | Using `pip`:: 7 | 8 | $ pip install tarantool 9 | 10 | Using `easy_install`:: 11 | 12 | $ easy_install tarantool 13 | 14 | 15 | You can also download the source tarball and install the package using distutils script:: 16 | 17 | # make install 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2022 tarantool-python AUTHORS: please see the AUTHORS file. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include README.txt 3 | include setup.py 4 | recursive-include tarantool/ *.py 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: 3 | pip3 install --editable . 4 | 5 | 6 | PYTHON_FILES=tarantool test setup.py docs/source/conf.py 7 | TEXT_FILES=README.rst CHANGELOG.md docs/source/*.rst 8 | .PHONY: lint 9 | lint: 10 | python3 -m pylint --recursive=y $(PYTHON_FILES) 11 | python3 -m flake8 $(PYTHON_FILES) 12 | codespell $(PYTHON_FILES) $(TEXT_FILES) 13 | 14 | 15 | .PHONY: test 16 | test: lint 17 | python3 setup.py test 18 | 19 | .PHONY: test-pure-install 20 | test-pure-install: 21 | TEST_PURE_INSTALL=true python3 -m unittest discover -v 22 | 23 | .PHONY: testdata 24 | testdata: 25 | cd ./test/data/; ./generate.sh 26 | 27 | .PHONY: coverage 28 | coverage: 29 | python3 -m coverage run -p --source=. setup.py test 30 | 31 | .PHONY: cov-html 32 | cov-html: 33 | python3 -m coverage html -i 34 | 35 | .PHONY: cov-report 36 | cov-report: 37 | python3 -m coverage report 38 | 39 | 40 | .PHONY: docs 41 | docs: 42 | python3 setup.py build_sphinx 43 | 44 | 45 | .PHONY: pip-sdist 46 | pip-sdist: 47 | python3 setup.py sdist --dist-dir=pip_dist 48 | 49 | .PHONY: pip-bdist 50 | pip-bdist: 51 | python3 setup.py bdist_wheel --dist-dir=pip_dist 52 | 53 | .PHONY: pip-dist 54 | pip-dist: pip-sdist pip-bdist 55 | 56 | .PHONY: pip-dist-check 57 | pip-dist-check: 58 | twine check pip_dist/* 59 | 60 | 61 | .PHONY: rpm-dist 62 | rpm-dist: 63 | python3 setup.py sdist --dist-dir=rpm/SOURCES 64 | rpmbuild -ba --define "_topdir `pwd`/rpm" rpm/SPECS/python-tarantool.spec 65 | mkdir -p rpm_dist 66 | mv rpm/SRPMS/*.rpm -t rpm_dist 67 | mv rpm/RPMS/noarch/*.rpm -t rpm_dist 68 | 69 | .PHONY: rpm-dist-check 70 | rpm-dist-check: 71 | rpm -K --nosignature rpm_dist/*.rpm 72 | 73 | 74 | .PHONY: deb-changelog-entry 75 | deb-changelog-entry: 76 | DEBEMAIL=admin@tarantool.org dch --distribution unstable -b \ 77 | --package "python3-tarantool" \ 78 | --newversion $$(python3 setup.py --version) \ 79 | "Nightly build" 80 | 81 | .PHONY: deb-dist 82 | deb-dist: 83 | dpkg-source -b . 84 | dpkg-buildpackage -rfakeroot -us -uc 85 | mkdir -p deb_dist 86 | find .. -maxdepth 1 -type f -regex '.*/python3-tarantool_.*\.deb' \ 87 | -or -regex '.*/python3-tarantool_.*\.buildinfo' \ 88 | -or -regex '.*/python3-tarantool_.*\.changes' \ 89 | -or -regex '.*/python3-tarantool_.*\.dsc' \ 90 | -or -regex '.*/python3-tarantool_.*\.tar\.xz' \ 91 | | xargs -I {} mv {} deb_dist/ 92 | 93 | .PHONY: deb-dist-check 94 | deb-dist-check: 95 | dpkg -I deb_dist/*.deb 96 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python driver for Tarantool 2 | =========================== 3 | 4 | .. image:: https://github.com/tarantool/tarantool-python/actions/workflows/testing.yml/badge.svg?branch=master 5 | :target: https://github.com/tarantool/tarantool-python/actions/workflows/testing.yml 6 | .. image:: https://github.com/tarantool/tarantool-python/actions/workflows/packing.yml/badge.svg?branch=master 7 | :target: https://github.com/tarantool/tarantool-python/actions/workflows/packing.yml 8 | 9 | This package is a pure-python client library for `Tarantool`_. 10 | 11 | `Documentation`_ | `Downloads`_ | `PyPI`_ | `GitHub`_ | `Issue tracker`_ 12 | 13 | .. _`Documentation`: http://tarantool-python.readthedocs.org/en/latest/ 14 | .. _`Downloads`: http://pypi.python.org/pypi/tarantool#downloads 15 | .. _`PyPI`: http://pypi.python.org/pypi/tarantool 16 | .. _`GitHub`: https://github.com/tarantool/tarantool-python 17 | .. _`Issue tracker`: https://github.com/tarantool/tarantool-python/issues 18 | 19 | Download and install 20 | -------------------- 21 | 22 | With pip (recommended) 23 | ^^^^^^^^^^^^^^^^^^^^^^ 24 | 25 | The recommended way to install the ``tarantool`` package is using ``pip``. 26 | 27 | .. code-block:: bash 28 | 29 | $ pip3 install tarantool 30 | 31 | With dnf 32 | ^^^^^^^^ 33 | 34 | You can install ``python3-tarantool`` RPM package if you use Fedora (34, 35, 36). 35 | 36 | Add the repository 37 | 38 | .. code-block:: bash 39 | 40 | $ curl -L https://tarantool.io/OtKysgx/release/2/installer.sh | bash 41 | 42 | and then install the package 43 | 44 | .. code-block:: bash 45 | 46 | $ dnf install -y python3-tarantool 47 | 48 | With apt 49 | ^^^^^^^^ 50 | 51 | You can install ``python3-tarantool`` deb package if you use 52 | Debian (10, 11) or Ubuntu (20.04, 22.04). 53 | 54 | Add the repository 55 | 56 | .. code-block:: bash 57 | 58 | $ curl -L https://tarantool.io/OtKysgx/release/2/installer.sh | bash 59 | 60 | and then install the package 61 | 62 | .. code-block:: bash 63 | 64 | $ apt install -y python3-tarantool 65 | 66 | ZIP archive 67 | ^^^^^^^^^^^ 68 | 69 | You can also download zip archive, unpack it and run: 70 | 71 | .. code-block:: bash 72 | 73 | $ make install 74 | 75 | Development version 76 | ^^^^^^^^^^^^^^^^^^^ 77 | 78 | You can also install the development version of the package using ``pip``. 79 | 80 | .. code-block:: bash 81 | 82 | $ pip3 install git+https://github.com/tarantool/tarantool-python.git@master 83 | 84 | -------------------------------------------------------------------------------- 85 | 86 | What is Tarantool? 87 | ------------------ 88 | 89 | `Tarantool`_ is an in-memory computing platform originally designed by 90 | `VK`_ and released under the terms of `BSD license`_. 91 | 92 | Features 93 | -------- 94 | 95 | * ANSI SQL, including views, joins, referential and check constraints 96 | * Lua packages for non-blocking I/O, fibers, and HTTP 97 | * MessagePack data format and MessagePack-based client-server protocol 98 | * Two data engines: 99 | 100 | * memtx – in-memory storage engine with optional persistence 101 | * vinyl – on-disk storage engine to use with larger data sets 102 | 103 | * Secondary key and index iterator support (can be non-unique and composite) 104 | * Multiple index types: HASH, BITSET, TREE, RTREE 105 | * Asynchronous master-master replication 106 | * Authentication and access control 107 | 108 | See More 109 | ^^^^^^^^ 110 | 111 | * `Tarantool homepage`_ 112 | * `Tarantool on GitHub`_ 113 | * `Tarantool documentation`_ 114 | * `Client-server protocol specification`_ 115 | 116 | NOTE 117 | ^^^^ 118 | 119 | This driver is synchronous, so connection mustn't be shared between threads/processes. 120 | 121 | If you're looking for an asynchronous Python driver based on ``asyncio``, 122 | consider using `asynctnt`_ . See also the `feature comparison table`_. 123 | 124 | Run tests 125 | ^^^^^^^^^ 126 | 127 | On Linux: 128 | 129 | .. code-block:: bash 130 | 131 | $ make test 132 | 133 | On Windows: 134 | 135 | * Setup a Linux machine with Tarantool installed. 136 | This machine will be referred to as ``remote`` in this instruction. 137 | * (On ``remote``) Copy ``test/suites/lib/tarantool_python_ci.lua`` to 138 | ``/etc/tarantool/instances.available``. 139 | * (On ``remote``) Run ``tarantoolctl start tarantool_python_ci``. 140 | * Set the following environment variables: 141 | * ``REMOTE_TARANTOOL_HOST=...``, 142 | * ``REMOTE_TARANTOOL_CONSOLE_PORT=3302``. 143 | * Run ``make test``. 144 | 145 | Build docs 146 | ^^^^^^^^^^ 147 | 148 | To build documentation, first you must install its build requirements: 149 | 150 | .. code-block:: bash 151 | 152 | $ pip3 install -r docs/requirements.txt 153 | 154 | Then run 155 | 156 | .. code-block:: bash 157 | 158 | $ make docs 159 | 160 | You may host local documentation server with 161 | 162 | .. code-block:: bash 163 | 164 | $ python3 -m http.server --directory build/sphinx/html 165 | 166 | Open ``localhost:8000`` in your browser to read the docs. 167 | 168 | .. _`Tarantool`: 169 | .. _`Tarantool Database`: 170 | .. _`Tarantool homepage`: https://tarantool.io 171 | .. _`Tarantool on GitHub`: https://github.com/tarantool/tarantool 172 | .. _`Tarantool documentation`: https://www.tarantool.io/en/doc/latest/ 173 | .. _`VK`: https://vk.company 174 | .. _`Client-server protocol specification`: https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/ 175 | .. _`BSD`: 176 | .. _`BSD license`: 177 | .. _`BSD-2-Clause`: https://opensource.org/licenses/BSD-2-Clause 178 | .. _`asynctnt`: https://github.com/igorcoding/asynctnt 179 | .. _`feature comparison table`: https://www.tarantool.io/en/doc/latest/book/connectors/#python-feature-comparison 180 | 181 | License 182 | ^^^^^^^ 183 | 184 | BSD-2-Clause. See the ``LICENSE`` file. 185 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | See README.rst 2 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python3-tarantool 2 | Maintainer: tarantool-python AUTHORS 3 | Section: python 4 | Priority: optional 5 | # See https://github.com/astraw/stdeb/issues/175 for dependencies 6 | Build-Depends: python3, python3-dev, python3-pip, python3-setuptools, 7 | python3-wheel, python3-stdeb, dh-python, 8 | debhelper (>= 10) 9 | Standards-Version: 3.9.1 10 | Homepage: https://github.com/tarantool/tarantool-python 11 | 12 | Package: python3-tarantool 13 | Replaces: tarantool-python (<< 0.9.1~) 14 | Breaks: tarantool-python (<< 0.9.1~) 15 | Architecture: all 16 | Depends: ${misc:Depends}, ${python3:Depends} 17 | Description: Python client library for Tarantool. 18 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.10.0 at 4 | # Wed, 02 Nov 2022 17:29:57 +0300 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=python_distutils 8 | 9 | override_dh_auto_clean: 10 | python3 setup.py clean -a 11 | find . -name \*.pyc -exec rm {} \; 12 | 13 | override_dh_auto_build: 14 | python3 setup.py build --force 15 | 16 | override_dh_auto_install: 17 | python3 setup.py install --force --root=debian/python3-tarantool --no-compile -O0 --install-layout=deb --prefix=/usr 18 | 19 | override_dh_python2: 20 | dh_python2 --no-guessing-versions 21 | 22 | # Force `xz` compression for older system with dpkg version < 1.15.6 23 | override_dh_builddeb: 24 | dh_builddeb -- -Zxz 25 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore="\.egg-info$" 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==5.2.1 2 | sphinx-paramlinks==0.5.4 3 | sphinx_favicon==1.0.1 4 | -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/generate-png.sh: -------------------------------------------------------------------------------- 1 | for SIZE in 16 32 96 128 196 2 | do 3 | convert -background none -resize ${SIZE}x${SIZE} icon.svg icon-${SIZE}x${SIZE}.png 4 | done 5 | 6 | for SIZE in 57 60 72 76 114 120 144 152 7 | do 8 | convert -background none -resize ${SIZE}x${SIZE} icon.svg apple-touch-icon-${SIZE}x${SIZE}.png 9 | done 10 | -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/icon-128x128.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/icon-16x16.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/icon-196x196.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/icon-32x32.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/docs/source/_static/favicon/icon-96x96.png -------------------------------------------------------------------------------- /docs/source/_static/favicon/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 28 | 30 | 34 | 38 | 39 | 49 | 51 | 55 | 59 | 60 | 61 | 79 | 83 | 87 | 91 | 95 | 99 | 100 | -------------------------------------------------------------------------------- /docs/source/api/module-tarantool.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool` 2 | ========================== 3 | 4 | .. automodule:: tarantool 5 | :exclude-members: Connection, MeshConnection, 6 | Error, DatabaseError, NetworkError, NetworkWarning, 7 | SchemaError 8 | 9 | .. autoclass:: tarantool.Connection 10 | :exclude-members: Error, DatabaseError, InterfaceError, 11 | ConfigurationError, SchemaError, NetworkError, 12 | Warning, DataError, OperationalError, IntegrityError, 13 | InternalError, ProgrammingError, NotSupportedError 14 | 15 | .. autoattribute:: Error 16 | :noindex: 17 | 18 | .. autoattribute:: DatabaseError 19 | :noindex: 20 | 21 | .. autoattribute:: InterfaceError 22 | :noindex: 23 | 24 | .. autoattribute:: ConfigurationError 25 | :noindex: 26 | 27 | .. autoattribute:: SchemaError 28 | :noindex: 29 | 30 | .. autoattribute:: NetworkError 31 | :noindex: 32 | 33 | .. autoattribute:: Warning 34 | :noindex: 35 | 36 | .. autoattribute:: DataError 37 | :noindex: 38 | 39 | .. autoattribute:: OperationalError 40 | :noindex: 41 | 42 | .. autoattribute:: IntegrityError 43 | :noindex: 44 | 45 | .. autoattribute:: InternalError 46 | :noindex: 47 | 48 | .. autoattribute:: ProgrammingError 49 | :noindex: 50 | 51 | .. autoattribute:: NotSupportedError 52 | :noindex: 53 | 54 | .. autoclass:: tarantool.MeshConnection 55 | :exclude-members: Error, DatabaseError, InterfaceError, 56 | ConfigurationError, SchemaError, NetworkError, 57 | Warning, DataError, OperationalError, IntegrityError, 58 | InternalError, ProgrammingError, NotSupportedError 59 | 60 | .. autoattribute:: Error 61 | :noindex: 62 | 63 | .. autoattribute:: DatabaseError 64 | :noindex: 65 | 66 | .. autoattribute:: InterfaceError 67 | :noindex: 68 | 69 | .. autoattribute:: ConfigurationError 70 | :noindex: 71 | 72 | .. autoattribute:: SchemaError 73 | :noindex: 74 | 75 | .. autoattribute:: NetworkError 76 | :noindex: 77 | 78 | .. autoattribute:: Warning 79 | :noindex: 80 | 81 | .. autoattribute:: DataError 82 | :noindex: 83 | 84 | .. autoattribute:: OperationalError 85 | :noindex: 86 | 87 | .. autoattribute:: IntegrityError 88 | :noindex: 89 | 90 | .. autoattribute:: InternalError 91 | :noindex: 92 | 93 | .. autoattribute:: ProgrammingError 94 | :noindex: 95 | 96 | .. autoattribute:: NotSupportedError 97 | :noindex: 98 | 99 | .. autoexception:: Error 100 | :noindex: 101 | 102 | .. autoexception:: DatabaseError 103 | :noindex: 104 | 105 | .. autoexception:: DatabaseError 106 | :noindex: 107 | 108 | .. autoexception:: NetworkError 109 | :noindex: 110 | 111 | .. autoexception:: NetworkWarning 112 | :noindex: 113 | 114 | .. autoexception:: SchemaError 115 | :noindex: 116 | -------------------------------------------------------------------------------- /docs/source/api/submodule-connection-pool.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.connection_pool` 2 | ========================================== 3 | 4 | .. automodule:: tarantool.connection_pool 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-connection.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.connection` 2 | ===================================== 3 | 4 | .. automodule:: tarantool.connection 5 | :exclude-members: Connection 6 | 7 | .. autoclass:: tarantool.connection.Connection 8 | :exclude-members: Error, DatabaseError, InterfaceError, 9 | ConfigurationError, SchemaError, NetworkError, 10 | Warning, DataError, OperationalError, IntegrityError, 11 | InternalError, ProgrammingError, NotSupportedError 12 | 13 | .. autoattribute:: Error 14 | :noindex: 15 | 16 | .. autoattribute:: DatabaseError 17 | :noindex: 18 | 19 | .. autoattribute:: InterfaceError 20 | :noindex: 21 | 22 | .. autoattribute:: ConfigurationError 23 | :noindex: 24 | 25 | .. autoattribute:: SchemaError 26 | :noindex: 27 | 28 | .. autoattribute:: NetworkError 29 | :noindex: 30 | 31 | .. autoattribute:: Warning 32 | :noindex: 33 | 34 | .. autoattribute:: DataError 35 | :noindex: 36 | 37 | .. autoattribute:: OperationalError 38 | :noindex: 39 | 40 | .. autoattribute:: IntegrityError 41 | :noindex: 42 | 43 | .. autoattribute:: InternalError 44 | :noindex: 45 | 46 | .. autoattribute:: ProgrammingError 47 | :noindex: 48 | 49 | .. autoattribute:: NotSupportedError 50 | :noindex: 51 | -------------------------------------------------------------------------------- /docs/source/api/submodule-crud.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.crud` 2 | ================================ 3 | 4 | .. automodule:: tarantool.crud 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-dbapi.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.dbapi` 2 | ================================ 3 | 4 | .. automodule:: tarantool.dbapi 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-error.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.error` 2 | ================================ 3 | 4 | .. automodule:: tarantool.error 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-mesh-connection.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.mesh_connection` 2 | ========================================== 3 | 4 | .. automodule:: tarantool.mesh_connection 5 | :exclude-members: MeshConnection 6 | 7 | .. autoclass:: tarantool.mesh_connection.MeshConnection 8 | :exclude-members: Error, DatabaseError, InterfaceError, 9 | ConfigurationError, SchemaError, NetworkError, 10 | Warning, DataError, OperationalError, IntegrityError, 11 | InternalError, ProgrammingError, NotSupportedError 12 | 13 | .. autoattribute:: Error 14 | :noindex: 15 | 16 | .. autoattribute:: DatabaseError 17 | :noindex: 18 | 19 | .. autoattribute:: InterfaceError 20 | :noindex: 21 | 22 | .. autoattribute:: ConfigurationError 23 | :noindex: 24 | 25 | .. autoattribute:: SchemaError 26 | :noindex: 27 | 28 | .. autoattribute:: NetworkError 29 | :noindex: 30 | 31 | .. autoattribute:: Warning 32 | :noindex: 33 | 34 | .. autoattribute:: DataError 35 | :noindex: 36 | 37 | .. autoattribute:: OperationalError 38 | :noindex: 39 | 40 | .. autoattribute:: IntegrityError 41 | :noindex: 42 | 43 | .. autoattribute:: InternalError 44 | :noindex: 45 | 46 | .. autoattribute:: ProgrammingError 47 | :noindex: 48 | 49 | .. autoattribute:: NotSupportedError 50 | :noindex: 51 | -------------------------------------------------------------------------------- /docs/source/api/submodule-msgpack-ext-types.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.msgpack_ext.types` 2 | ============================================ 3 | 4 | .. currentmodule:: tarantool.msgpack_ext.types 5 | 6 | module :py:mod:`tarantool.msgpack_ext.types.datetime` 7 | ----------------------------------------------------- 8 | 9 | .. automodule:: tarantool.msgpack_ext.types.datetime 10 | :special-members: __add__, __sub__, __eq__ 11 | 12 | 13 | .. currentmodule:: tarantool.msgpack_ext.types 14 | 15 | module :py:mod:`tarantool.msgpack_ext.types.interval` 16 | ----------------------------------------------------- 17 | 18 | .. automodule:: tarantool.msgpack_ext.types.interval 19 | :special-members: __add__, __sub__, __eq__ 20 | -------------------------------------------------------------------------------- /docs/source/api/submodule-msgpack-ext.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.msgpack_ext` 2 | ====================================== 3 | 4 | .. currentmodule:: tarantool.msgpack_ext 5 | 6 | module :py:mod:`tarantool.msgpack_ext.datetime` 7 | ----------------------------------------------- 8 | 9 | .. automodule:: tarantool.msgpack_ext.datetime 10 | 11 | 12 | .. currentmodule:: tarantool.msgpack_ext 13 | 14 | module :py:mod:`tarantool.msgpack_ext.decimal` 15 | ---------------------------------------------- 16 | 17 | .. automodule:: tarantool.msgpack_ext.decimal 18 | 19 | 20 | .. currentmodule:: tarantool.msgpack_ext 21 | 22 | module :py:mod:`tarantool.msgpack_ext.interval` 23 | ----------------------------------------------- 24 | 25 | .. automodule:: tarantool.msgpack_ext.interval 26 | 27 | 28 | .. currentmodule:: tarantool.msgpack_ext 29 | 30 | module :py:mod:`tarantool.msgpack_ext.packer` 31 | --------------------------------------------- 32 | 33 | .. automodule:: tarantool.msgpack_ext.packer 34 | 35 | 36 | .. currentmodule:: tarantool.msgpack_ext 37 | 38 | module :py:mod:`tarantool.msgpack_ext.unpacker` 39 | ----------------------------------------------- 40 | 41 | .. automodule:: tarantool.msgpack_ext.unpacker 42 | 43 | 44 | .. currentmodule:: tarantool.msgpack_ext 45 | 46 | module :py:mod:`tarantool.msgpack_ext.uuid` 47 | ------------------------------------------- 48 | 49 | .. automodule:: tarantool.msgpack_ext.uuid 50 | -------------------------------------------------------------------------------- /docs/source/api/submodule-request.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.request` 2 | ================================== 3 | 4 | .. automodule:: tarantool.request 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-response.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.response` 2 | =================================== 3 | 4 | .. automodule:: tarantool.response 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-schema.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.schema` 2 | ================================= 3 | 4 | .. automodule:: tarantool.schema 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-space.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.space` 2 | ================================ 3 | 4 | .. automodule:: tarantool.space 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-types.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.types` 2 | ================================ 3 | 4 | .. automodule:: tarantool.types 5 | -------------------------------------------------------------------------------- /docs/source/api/submodule-utils.rst: -------------------------------------------------------------------------------- 1 | module :py:mod:`tarantool.utils` 2 | ================================ 3 | 4 | .. automodule:: tarantool.utils 5 | -------------------------------------------------------------------------------- /docs/source/dev-guide.rst: -------------------------------------------------------------------------------- 1 | .. encoding: utf-8 2 | 3 | Developer's guide 4 | ================= 5 | 6 | Tarantool database basic concepts 7 | --------------------------------- 8 | 9 | To understand, what is "space", "tuple" and what basic operations are, 10 | refer to `Tarantool data model documentation`_. 11 | 12 | Field types 13 | ----------- 14 | 15 | Tarantool uses `MessagePack`_ as a format for receiving requests and sending 16 | responses. Refer to `Lua versus MessagePack`_ to see how types are encoded 17 | and decoded. 18 | 19 | While working with Tarantool from Python with this connector, 20 | each request data is encoded to MessagePack and each response data 21 | is decoded from MessagePack with the `Python MessagePack`_ module. See its 22 | documentation to explore how basic types are encoded and decoded. 23 | 24 | There are several cases when you may tune up the behavior. 25 | Use :class:`tarantool.Connection` parameters to set Python MessagePack 26 | module options. 27 | 28 | Use :paramref:`~tarantool.Connection.params.encoding` to tune 29 | behavior for string encoding. 30 | 31 | ``encoding='utf-8'`` (default): 32 | 33 | +--------------+----+----------------------------------+----+--------------+ 34 | | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | 35 | +==============+====+==================================+====+==============+ 36 | | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`str` | 37 | +--------------+----+----------------------------------+----+--------------+ 38 | | :obj:`bytes` | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | 39 | +--------------+----+----------------------------------+----+--------------+ 40 | 41 | ``encoding=None`` (work with non-UTF8 strings): 42 | 43 | +--------------+----+----------------------------------+----+--------------+ 44 | | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | 45 | +==============+====+==================================+====+==============+ 46 | | :obj:`bytes` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | 47 | +--------------+----+----------------------------------+----+--------------+ 48 | | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | 49 | +--------------+----+----------------------------------+----+--------------+ 50 | | | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | 51 | +--------------+----+----------------------------------+----+--------------+ 52 | 53 | Use :paramref:`~tarantool.Connection.params.use_list` to tune 54 | behavior for `mp_array`_ (Lua ``table``) decoding. 55 | 56 | ``use_list='True'`` (default): 57 | 58 | +--------------+----+-----------------------------+----+--------------+ 59 | | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | 60 | +==============+====+=============================+====+==============+ 61 | | :obj:`list` | -> | `mp_array`_ (``table``) | -> | :obj:`list` | 62 | +--------------+----+-----------------------------+----+--------------+ 63 | | :obj:`tuple` | -> | `mp_array`_ (``table``) | -> | :obj:`list` | 64 | +--------------+----+-----------------------------+----+--------------+ 65 | 66 | ``use_list='False'``: 67 | 68 | +--------------+----+-----------------------------+----+--------------+ 69 | | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | 70 | +==============+====+=============================+====+==============+ 71 | | :obj:`list` | -> | `mp_array`_ (``table``) | -> | :obj:`tuple` | 72 | +--------------+----+-----------------------------+----+--------------+ 73 | | :obj:`tuple` | -> | `mp_array`_ (``table``) | -> | :obj:`tuple` | 74 | +--------------+----+-----------------------------+----+--------------+ 75 | 76 | Tarantool implements several `extension types`_. In Python, 77 | they are represented with in-built and custom types: 78 | 79 | +-----------------------------+----+-------------+----+-----------------------------+ 80 | | Python | -> | Tarantool | -> | Python | 81 | +=============================+====+=============+====+=============================+ 82 | | :obj:`decimal.Decimal` | -> | `DECIMAL`_ | -> | :obj:`decimal.Decimal` | 83 | +-----------------------------+----+-------------+----+-----------------------------+ 84 | | :obj:`uuid.UUID` | -> | `UUID`_ | -> | :obj:`uuid.UUID` | 85 | +-----------------------------+----+-------------+----+-----------------------------+ 86 | | :class:`tarantool.BoxError` | -> | `ERROR`_ | -> | :class:`tarantool.BoxError` | 87 | +-----------------------------+----+-------------+----+-----------------------------+ 88 | | :class:`tarantool.Datetime` | -> | `DATETIME`_ | -> | :class:`tarantool.Datetime` | 89 | +-----------------------------+----+-------------+----+-----------------------------+ 90 | | :class:`tarantool.Interval` | -> | `INTERVAL`_ | -> | :class:`tarantool.Interval` | 91 | +-----------------------------+----+-------------+----+-----------------------------+ 92 | 93 | Request response 94 | ---------------- 95 | 96 | Server requests (except for :meth:`~tarantool.Connection.ping`) 97 | return :class:`~tarantool.response.Response` instance in case 98 | of success. 99 | 100 | :class:`~tarantool.response.Response` is inherited from 101 | :class:`collections.abc.Sequence`, so you can index response data 102 | and iterate through it as with any other serializable object. 103 | 104 | .. _Tarantool data model documentation: https://www.tarantool.io/en/doc/latest/concepts/data_model/ 105 | .. _MessagePack: https://msgpack.org/ 106 | .. _Lua versus MessagePack: https://www.tarantool.io/en/doc/latest/concepts/data_model/value_store/#lua-versus-msgpack 107 | .. _Python MessagePack: https://pypi.org/project/msgpack/ 108 | .. _mp_str: https://github.com/msgpack/msgpack/blob/master/spec.md#str-format-family 109 | .. _mp_bin: https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family 110 | .. _mp_array: https://github.com/msgpack/msgpack/blob/master/spec.md#array-format-family 111 | .. _extension types: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ 112 | .. _DECIMAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type 113 | .. _UUID: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type 114 | .. _ERROR: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type 115 | .. _DATETIME: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type 116 | .. _INTERVAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type 117 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. encoding: utf-8 2 | 3 | Python client library for Tarantool 4 | =================================== 5 | 6 | :Version: |version| 7 | 8 | `Tarantool`_ is an in-memory computing platform originally designed by 9 | `VK`_ and released under the terms of `BSD license`_. 10 | 11 | Install Tarantool Python connector with ``pip`` (`PyPI`_ page): 12 | 13 | .. code-block:: bash 14 | 15 | $ pip3 install tarantool 16 | 17 | Otherwise, you can install ``python3-tarantool`` RPM package if you use Fedora (34, 35, 36). 18 | 19 | Add the repository 20 | 21 | .. code-block:: bash 22 | 23 | $ curl -L https://tarantool.io/OtKysgx/release/2/installer.sh | bash 24 | 25 | and then install the package 26 | 27 | .. code-block:: bash 28 | 29 | $ dnf install -y python3-tarantool 30 | 31 | Otherwise, you can install ``python3-tarantool`` deb package if you use Debian (10, 11) 32 | or Ubuntu (20.04, 22.04). 33 | 34 | Add the repository 35 | 36 | .. code-block:: bash 37 | 38 | $ curl -L https://tarantool.io/OtKysgx/release/2/installer.sh | bash 39 | 40 | and then install the package 41 | 42 | .. code-block:: bash 43 | 44 | $ apt install -y python3-tarantool 45 | 46 | Source code is available on `GitHub`_. 47 | 48 | Documentation 49 | ------------- 50 | .. toctree:: 51 | :maxdepth: 1 52 | 53 | quick-start 54 | dev-guide 55 | 56 | .. seealso:: `Tarantool documentation`_ 57 | 58 | API Reference 59 | ------------- 60 | .. toctree:: 61 | :maxdepth: 2 62 | 63 | api/module-tarantool.rst 64 | api/submodule-connection.rst 65 | api/submodule-connection-pool.rst 66 | api/submodule-crud.rst 67 | api/submodule-dbapi.rst 68 | api/submodule-error.rst 69 | api/submodule-mesh-connection.rst 70 | api/submodule-msgpack-ext.rst 71 | api/submodule-msgpack-ext-types.rst 72 | api/submodule-request.rst 73 | api/submodule-response.rst 74 | api/submodule-schema.rst 75 | api/submodule-space.rst 76 | api/submodule-types.rst 77 | api/submodule-utils.rst 78 | 79 | .. Indices and tables 80 | .. ================== 81 | .. 82 | .. * :ref:`genindex` 83 | .. * :ref:`modindex` 84 | .. * :ref:`search` 85 | 86 | .. _`Tarantool`: 87 | .. _`Tarantool homepage`: https://tarantool.io 88 | .. _`Tarantool documentation`: https://www.tarantool.io/en/doc/latest/ 89 | .. _`VK`: https://vk.company 90 | .. _`BSD`: 91 | .. _`BSD license`: http://www.gnu.org/licenses/license-list.html#ModifiedBSD 92 | .. _`PyPI`: http://pypi.python.org/pypi/tarantool 93 | .. _`GitHub`: https://github.com/tarantool/tarantool-python 94 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/baztian/dbapi-compliance.git@ea7cb1b4#egg=dbapi-compliance 2 | pyyaml >= 6.0.2 3 | importlib-metadata >= 1.0 ; python_version < '3.8' 4 | pylint == 3.3.0 ; python_version >= '3.9' 5 | pylint == 3.2.7 ; python_version == '3.8' 6 | pylint == 2.17.7 ; python_version == '3.7' 7 | flake8 == 6.1.0 ; python_version >= '3.8' 8 | flake8 == 5.0.4 ; python_version < '3.8' 9 | codespell == 2.3.0 ; python_version >= '3.8' 10 | codespell == 2.2.5 ; python_version < '3.8' 11 | setuptools >= 75.3.2 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | msgpack 2 | pytz 3 | -------------------------------------------------------------------------------- /rpm/SPECS/python-tarantool.spec: -------------------------------------------------------------------------------- 1 | # Based on https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/installing_and_using_dynamic_programming_languages/assembly_packaging-python-3-rpms_installing-and-using-dynamic-programming-languages 2 | # merged with python3 setup.py bdist_rpm --spec-only result. 3 | 4 | %define srcname tarantool 5 | %define version %(python3 setup.py --version) 6 | 7 | Name: python-%{srcname} 8 | Version: %{version} 9 | Release: 1%{?dist} 10 | Summary: Python client library for Tarantool 11 | 12 | License: BSD 13 | Group: Development/Libraries 14 | URL: https://github.com/tarantool/tarantool-python 15 | 16 | BuildArch: noarch 17 | Source: %{srcname}-%{version}.tar.gz 18 | Vendor: tarantool-python AUTHORS 19 | 20 | BuildRequires: python3-setuptools 21 | BuildRequires: python3-wheel 22 | 23 | %global _description %{expand: 24 | Python client library for Tarantool.} 25 | 26 | %description %_description 27 | 28 | 29 | %package -n python3-%{srcname} 30 | 31 | Requires: python3-msgpack 32 | Requires: python3-pytz 33 | 34 | Summary: %{summary} 35 | 36 | Obsoletes: tarantool-python <= 0.9.0 37 | 38 | %description -n python3-%{srcname} %_description 39 | 40 | 41 | %prep 42 | %setup -n %{srcname}-%{version} 43 | 44 | 45 | %build 46 | python3 setup.py build 47 | 48 | 49 | %install 50 | python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES 51 | 52 | 53 | %clean 54 | rm -rf $RPM_BUILD_ROOT 55 | 56 | 57 | %files -n python3-%{srcname} -f INSTALLED_FILES 58 | 59 | 60 | %defattr(-,root,root) 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Package setup commands. 4 | """ 5 | # pylint: disable=bad-option-value,too-many-ancestors 6 | 7 | import codecs 8 | import os 9 | 10 | from setuptools import find_packages, setup 11 | from setuptools.command.build_py import build_py 12 | 13 | # Extra commands for documentation management 14 | cmdclass = {} 15 | command_options = {} 16 | 17 | 18 | class BuildPyCommand(build_py): 19 | """ 20 | Build the package 21 | python setup.py build_py 22 | builds the package with generating correspondent VERSION file 23 | """ 24 | 25 | def run(self): 26 | """ 27 | Run the command. 28 | """ 29 | 30 | # Import here to allow to run commands 31 | # like `python setup.py test` without setuptools_scm. 32 | # pylint: disable=import-outside-toplevel,import-error 33 | from setuptools_scm import get_version 34 | version = get_version() 35 | 36 | package_dir = self.get_package_dir('tarantool') 37 | version_file = os.path.join(package_dir, 'version.py') 38 | with open(version_file, 'w', encoding='utf-8') as file: 39 | file.write(f"__version__ = '{version}'") 40 | 41 | return super().run() 42 | 43 | 44 | cmdclass["build_py"] = BuildPyCommand 45 | 46 | # Build Sphinx documentation (html) 47 | # python setup.py build_sphinx 48 | # generates files into build/sphinx/html 49 | try: 50 | from sphinx.setup_command import BuildDoc 51 | cmdclass["build_sphinx"] = BuildDoc 52 | except ImportError: 53 | pass 54 | 55 | 56 | # Test runner 57 | # python setup.py test 58 | try: 59 | from test.setup_command import Test 60 | cmdclass["test"] = Test 61 | except ImportError: 62 | pass 63 | 64 | 65 | def read(*parts): 66 | """ 67 | Read the file. 68 | """ 69 | 70 | filename = os.path.join(os.path.dirname(__file__), *parts) 71 | with codecs.open(filename, encoding='utf-8') as file: 72 | return file.read() 73 | 74 | 75 | def get_dependencies(filename): 76 | """ 77 | Get package dependencies from the `requirements.txt`. 78 | """ 79 | 80 | root = os.path.dirname(os.path.realpath(__file__)) 81 | requirements = os.path.join(root, filename) 82 | if os.path.isfile(requirements): 83 | with open(requirements, encoding='utf-8') as file: 84 | return file.read().splitlines() 85 | raise RuntimeError("Unable to get dependencies from file " + filename) 86 | 87 | 88 | packages = [item for item in find_packages('.') if item.startswith('tarantool')] 89 | 90 | setup( 91 | name="tarantool", 92 | packages=packages, 93 | package_dir={"tarantool": "tarantool"}, 94 | include_package_data=True, 95 | use_scm_version=True, 96 | platforms=["all"], 97 | author="tarantool-python AUTHORS", 98 | author_email="admin@tarantool.org", 99 | url="https://github.com/tarantool/tarantool-python", 100 | license="BSD", 101 | description="Python client library for Tarantool", 102 | long_description=read('README.rst'), 103 | long_description_content_type='text/x-rst', 104 | classifiers=[ 105 | "Intended Audience :: Developers", 106 | "License :: OSI Approved :: BSD License", 107 | "Operating System :: OS Independent", 108 | "Programming Language :: Python :: 3 :: Only", 109 | "Topic :: Database :: Front-Ends" 110 | ], 111 | cmdclass=cmdclass, 112 | command_options=command_options, 113 | install_requires=get_dependencies('requirements.txt'), 114 | setup_requires=[ 115 | 'setuptools_scm==7.1.0', 116 | ], 117 | python_requires='>=3.7', 118 | ) 119 | -------------------------------------------------------------------------------- /tarantool/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package provides API for interaction with a Tarantool server. 3 | """ 4 | # pylint: disable=too-many-arguments,too-many-positional-arguments 5 | 6 | from tarantool.connection import Connection 7 | from tarantool.mesh_connection import MeshConnection 8 | from tarantool.const import ( 9 | SOCKET_TIMEOUT, 10 | RECONNECT_MAX_ATTEMPTS, 11 | RECONNECT_DELAY, 12 | DEFAULT_TRANSPORT, 13 | DEFAULT_SSL_KEY_FILE, 14 | DEFAULT_SSL_CERT_FILE, 15 | DEFAULT_SSL_CA_FILE, 16 | DEFAULT_SSL_CIPHERS 17 | ) 18 | 19 | from tarantool.error import ( 20 | Error, 21 | DatabaseError, 22 | NetworkError, 23 | NetworkWarning, 24 | ) 25 | 26 | from tarantool.schema import ( 27 | Schema, 28 | SchemaError 29 | ) 30 | 31 | from tarantool.utils import ( 32 | ENCODING_DEFAULT, 33 | ) 34 | 35 | from tarantool.msgpack_ext.types.datetime import ( 36 | Datetime, 37 | ) 38 | 39 | from tarantool.msgpack_ext.types.interval import ( 40 | Adjust as IntervalAdjust, 41 | Interval, 42 | ) 43 | 44 | from tarantool.connection_pool import ConnectionPool, Mode 45 | 46 | from tarantool.types import BoxError 47 | 48 | try: 49 | from tarantool.version import __version__ 50 | except ImportError: 51 | __version__ = '0.0.0-dev' 52 | 53 | 54 | def connect(host="localhost", port=33013, socket_fd=None, user=None, password=None, 55 | encoding=ENCODING_DEFAULT, transport=DEFAULT_TRANSPORT, 56 | ssl_key_file=DEFAULT_SSL_KEY_FILE, 57 | ssl_cert_file=DEFAULT_SSL_CERT_FILE, 58 | ssl_ca_file=DEFAULT_SSL_CA_FILE, 59 | ssl_ciphers=DEFAULT_SSL_CIPHERS): 60 | """ 61 | Create a connection to the Tarantool server. 62 | 63 | :param host: Refer to :paramref:`~tarantool.Connection.params.host`. 64 | 65 | :param port: Refer to :paramref:`~tarantool.Connection.params.port`. 66 | 67 | :param socket_fd: Refer to :paramref:`~tarantool.Connection.params.socket_fd`. 68 | 69 | :param user: Refer to :paramref:`~tarantool.Connection.params.user`. 70 | 71 | :param password: Refer to 72 | :paramref:`~tarantool.Connection.params.password`. 73 | 74 | :param encoding: Refer to 75 | :paramref:`~tarantool.Connection.params.encoding`. 76 | 77 | :param transport: Refer to 78 | :paramref:`~tarantool.Connection.params.transport`. 79 | 80 | :param ssl_key_file: Refer to 81 | :paramref:`~tarantool.Connection.params.ssl_key_file`. 82 | 83 | :param ssl_cert_file: Refer to 84 | :paramref:`~tarantool.Connection.params.ssl_cert_file`. 85 | 86 | :param ssl_ca_file: Refer to 87 | :paramref:`~tarantool.Connection.params.ssl_ca_file`. 88 | 89 | :param ssl_ciphers: Refer to 90 | :paramref:`~tarantool.Connection.params.ssl_ciphers`. 91 | 92 | :rtype: :class:`~tarantool.Connection` 93 | 94 | :raise: :class:`~tarantool.Connection` exceptions 95 | """ 96 | 97 | return Connection(host, port, 98 | socket_fd=socket_fd, 99 | user=user, 100 | password=password, 101 | socket_timeout=SOCKET_TIMEOUT, 102 | reconnect_max_attempts=RECONNECT_MAX_ATTEMPTS, 103 | reconnect_delay=RECONNECT_DELAY, 104 | connect_now=True, 105 | encoding=encoding, 106 | transport=transport, 107 | ssl_key_file=ssl_key_file, 108 | ssl_cert_file=ssl_cert_file, 109 | ssl_ca_file=ssl_ca_file, 110 | ssl_ciphers=ssl_ciphers) 111 | 112 | 113 | def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None, 114 | password=None, encoding=ENCODING_DEFAULT): 115 | """ 116 | Create a connection to a cluster of Tarantool servers. 117 | 118 | :param addrs: Refer to 119 | :paramref:`~tarantool.MeshConnection.params.addrs`. 120 | 121 | :param user: Refer to 122 | :paramref:`~tarantool.MeshConnection.params.user`. 123 | 124 | :param password: Refer to 125 | :paramref:`~tarantool.MeshConnection.params.password`. 126 | 127 | :param encoding: Refer to 128 | :paramref:`~tarantool.MeshConnection.params.encoding`. 129 | 130 | :rtype: :class:`~tarantool.MeshConnection` 131 | 132 | :raise: :class:`~tarantool.MeshConnection` exceptions 133 | """ 134 | 135 | return MeshConnection(addrs=addrs, 136 | user=user, 137 | password=password, 138 | socket_timeout=SOCKET_TIMEOUT, 139 | reconnect_max_attempts=RECONNECT_MAX_ATTEMPTS, 140 | reconnect_delay=RECONNECT_DELAY, 141 | connect_now=True, 142 | encoding=encoding) 143 | 144 | 145 | __all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema', 146 | 'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning', 147 | 'SchemaError', 'dbapi', 'Datetime', 'Interval', 'IntervalAdjust', 148 | 'ConnectionPool', 'Mode', 'BoxError'] 149 | -------------------------------------------------------------------------------- /tarantool/const.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module a set of constants for the package. 3 | """ 4 | 5 | IPROTO_REQUEST_TYPE = 0x00 6 | IPROTO_SYNC = 0x01 7 | # replication keys (header) 8 | IPROTO_SERVER_ID = 0x02 9 | IPROTO_LSN = 0x03 10 | IPROTO_TIMESTAMP = 0x04 11 | IPROTO_SCHEMA_ID = 0X05 12 | # 13 | IPROTO_SPACE_ID = 0x10 14 | IPROTO_INDEX_ID = 0x11 15 | IPROTO_LIMIT = 0x12 16 | IPROTO_OFFSET = 0x13 17 | IPROTO_ITERATOR = 0x14 18 | IPROTO_INDEX_BASE = 0x15 19 | # 20 | IPROTO_KEY = 0x20 21 | IPROTO_TUPLE = 0x21 22 | IPROTO_FUNCTION_NAME = 0x22 23 | IPROTO_USER_NAME = 0x23 24 | # 25 | IPROTO_SERVER_UUID = 0x24 26 | IPROTO_CLUSTER_UUID = 0x25 27 | IPROTO_VCLOCK = 0x26 28 | IPROTO_EXPR = 0x27 29 | IPROTO_OPS = 0x28 30 | # 31 | IPROTO_DATA = 0x30 32 | IPROTO_ERROR_24 = 0x31 33 | # 34 | IPROTO_METADATA = 0x32 35 | IPROTO_SQL_TEXT = 0x40 36 | IPROTO_SQL_BIND = 0x41 37 | IPROTO_SQL_INFO = 0x42 38 | IPROTO_SQL_INFO_ROW_COUNT = 0x00 39 | IPROTO_SQL_INFO_AUTOINCREMENT_IDS = 0x01 40 | # 41 | IPROTO_ERROR = 0x52 42 | # 43 | IPROTO_VERSION = 0x54 44 | IPROTO_FEATURES = 0x55 45 | IPROTO_AUTH_TYPE = 0x5b 46 | IPROTO_CHUNK = 0x80 47 | 48 | IPROTO_GREETING_SIZE = 128 49 | IPROTO_BODY_MAX_LEN = 2147483648 50 | 51 | REQUEST_TYPE_OK = 0x00 52 | REQUEST_TYPE_SELECT = 0x01 53 | REQUEST_TYPE_INSERT = 0x02 54 | REQUEST_TYPE_REPLACE = 0x03 55 | REQUEST_TYPE_UPDATE = 0x04 56 | REQUEST_TYPE_DELETE = 0x05 57 | REQUEST_TYPE_CALL16 = 0x06 58 | REQUEST_TYPE_AUTHENTICATE = 0x07 59 | REQUEST_TYPE_EVAL = 0x08 60 | REQUEST_TYPE_UPSERT = 0x09 61 | REQUEST_TYPE_CALL = 0x0a 62 | REQUEST_TYPE_EXECUTE = 0x0b 63 | REQUEST_TYPE_PING = 0x40 64 | REQUEST_TYPE_JOIN = 0x41 65 | REQUEST_TYPE_SUBSCRIBE = 0x42 66 | REQUEST_TYPE_ID = 0x49 67 | REQUEST_TYPE_ERROR = 1 << 15 68 | 69 | SPACE_SCHEMA = 272 70 | SPACE_SPACE = 280 71 | SPACE_INDEX = 288 72 | SPACE_FUNC = 296 73 | SPACE_VSPACE = 281 74 | SPACE_VINDEX = 289 75 | SPACE_VFUNC = 297 76 | SPACE_USER = 304 77 | SPACE_PRIV = 312 78 | SPACE_CLUSTER = 320 79 | 80 | INDEX_SPACE_PRIMARY = 0 81 | INDEX_SPACE_NAME = 2 82 | INDEX_INDEX_PRIMARY = 0 83 | INDEX_INDEX_NAME = 2 84 | 85 | ITERATOR_EQ = 0 86 | ITERATOR_REQ = 1 87 | ITERATOR_ALL = 2 88 | ITERATOR_LT = 3 89 | ITERATOR_LE = 4 90 | ITERATOR_GE = 5 91 | ITERATOR_GT = 6 92 | ITERATOR_BITSET_ALL_SET = 7 93 | ITERATOR_BITSET_ANY_SET = 8 94 | ITERATOR_BITSET_ALL_NOT_SET = 9 95 | ITERATOR_OVERLAPS = 10 96 | ITERATOR_NEIGHBOR = 11 97 | 98 | IPROTO_FEATURE_STREAMS = 0 99 | IPROTO_FEATURE_TRANSACTIONS = 1 100 | IPROTO_FEATURE_ERROR_EXTENSION = 2 101 | IPROTO_FEATURE_WATCHERS = 3 102 | IPROTO_FEATURE_PAGINATION = 4 103 | IPROTO_FEATURE_SPACE_AND_INDEX_NAMES = 5 104 | IPROTO_FEATURE_WATCH_ONCE = 6 105 | 106 | # Default value for host. 107 | DEFAULT_HOST = None 108 | # Default value for port. 109 | DEFAULT_PORT = None 110 | # Default value for socket_fd. 111 | DEFAULT_SOCKET_FD = None 112 | # Default value for connection timeout (seconds) 113 | CONNECTION_TIMEOUT = None 114 | # Default value for socket timeout (seconds) 115 | SOCKET_TIMEOUT = None 116 | # Default maximum number of attempts to reconnect 117 | RECONNECT_MAX_ATTEMPTS = 10 118 | # Default delay between attempts to reconnect (seconds) 119 | RECONNECT_DELAY = 0.1 120 | # Default value for transport 121 | DEFAULT_TRANSPORT = "" 122 | # Value for SSL transport 123 | SSL_TRANSPORT = "ssl" 124 | # Default value for a path to SSL key file 125 | DEFAULT_SSL_KEY_FILE = None 126 | # Default value for a path to SSL certificate file 127 | DEFAULT_SSL_CERT_FILE = None 128 | # Default value for a path to SSL certificatea uthorities file 129 | DEFAULT_SSL_CA_FILE = None 130 | # Default value for list of SSL ciphers 131 | DEFAULT_SSL_CIPHERS = None 132 | # Default value for SSL key file password 133 | DEFAULT_SSL_PASSWORD = None 134 | # Default value for a path to file with SSL key file password 135 | DEFAULT_SSL_PASSWORD_FILE = None 136 | # Default cluster nodes list refresh interval (seconds) 137 | CLUSTER_DISCOVERY_DELAY = 60 138 | # Default cluster nodes state refresh interval (seconds) 139 | POOL_REFRESH_DELAY = 1 140 | # Default maximum number of attempts to reconnect for pool instance 141 | POOL_INSTANCE_RECONNECT_MAX_ATTEMPTS = 0 142 | # Default delay between attempts to reconnect (seconds) 143 | POOL_INSTANCE_RECONNECT_DELAY = 0 144 | 145 | # Tarantool master 970ea48 protocol version is 6 146 | CONNECTOR_IPROTO_VERSION = 6 147 | # List of connector-supported features 148 | CONNECTOR_FEATURES = [IPROTO_FEATURE_ERROR_EXTENSION] 149 | 150 | # Authenticate with CHAP-SHA1 (Tarantool CE and EE) 151 | AUTH_TYPE_CHAP_SHA1 = "chap-sha1" 152 | # Authenticate with PAP-SHA256 (Tarantool EE) 153 | AUTH_TYPE_PAP_SHA256 = "pap-sha256" 154 | # List of supported auth types. 155 | AUTH_TYPES = [AUTH_TYPE_CHAP_SHA1, AUTH_TYPE_PAP_SHA256] 156 | -------------------------------------------------------------------------------- /tarantool/crud.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides API for interaction with the `crud`_ module. 3 | 4 | .. _crud: https://github.com/tarantool/crud/ 5 | """ 6 | 7 | from tarantool.error import DatabaseError, ER_NO_SUCH_PROC, ER_ACCESS_DENIED 8 | 9 | 10 | class CrudResponse(): 11 | """ 12 | Contains response fields from the `crud`_ module that correspond 13 | to the Lua implementation. 14 | 15 | .. _crud: https://github.com/tarantool/crud/ 16 | """ 17 | # pylint: disable=too-few-public-methods 18 | 19 | def __init__(self, response): 20 | """ 21 | Sets response fields as in Lua implementation. 22 | 23 | :param response: The response object of the crud module call. 24 | :type response: :class:`~tarantool.response.Response` 25 | """ 26 | 27 | if isinstance(response, dict): 28 | for response_field_name in response.keys(): 29 | if isinstance(response_field_name, bytes): 30 | setattr(self, response_field_name.decode(), response[response_field_name]) 31 | else: 32 | setattr(self, response_field_name, response[response_field_name]) 33 | else: 34 | raise RuntimeError('Unable to decode response to object due to unknown type') 35 | 36 | 37 | class CrudResult(CrudResponse): # pylint: disable=too-few-public-methods 38 | """ 39 | Contains result's fields from result variable 40 | of crud module operation. 41 | """ 42 | 43 | 44 | class CrudError(CrudResponse): # pylint: disable=too-few-public-methods 45 | """ 46 | Contains error's fields from error variable 47 | of crud module operation. 48 | """ 49 | 50 | 51 | def call_crud(conn, *args): 52 | """ 53 | Calls the crud via connection.call with try/except block. 54 | 55 | :param conn: The connection object for the crud module call. 56 | :type conn: :class:`~tarantool.connection.Connection` 57 | 58 | :param args: The method name and args for the crud method. 59 | :type args: :obj:`tuple` 60 | 61 | :raise: :exc:`~tarantool.error.DatabaseError` 62 | 63 | :meta private: 64 | """ 65 | 66 | try: 67 | crud_resp = conn.call(*args) 68 | except DatabaseError as exc: 69 | if exc.code in (ER_NO_SUCH_PROC, ER_ACCESS_DENIED): 70 | exc_msg = ". Ensure that you're calling crud.router and user has sufficient grants" 71 | raise DatabaseError(exc.code, exc.message + exc_msg, extra_info=exc.extra_info) from exc 72 | raise exc 73 | 74 | return crud_resp 75 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/tarantool/msgpack_ext/__init__.py -------------------------------------------------------------------------------- /tarantool/msgpack_ext/datetime.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool `datetime`_ extension type support module. 3 | 4 | The datetime MessagePack representation looks like this: 5 | 6 | .. code-block:: text 7 | 8 | +---------+----------------+==========+-----------------+ 9 | | MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; | 10 | | = d7/d8 | = 4 | | tzindex; | 11 | +---------+----------------+==========+-----------------+ 12 | 13 | MessagePack data contains: 14 | 15 | * Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the 16 | little-endian order. 17 | * The optional fields (8 bytes), if any of them have a non-zero value. 18 | The fields include nsec (4 bytes), tzoffset (2 bytes), and 19 | tzindex (2 bytes) packed in the little-endian order. 20 | 21 | ``seconds`` is seconds since Epoch, where the epoch is the point where 22 | the time starts, and is platform dependent. For Unix, the epoch is 23 | January 1, 1970, 00:00:00 (UTC). Tarantool uses a ``double`` type, see a 24 | structure definition in src/lib/core/datetime.h and reasons in 25 | `datetime RFC`_. 26 | 27 | ``nsec`` is nanoseconds, fractional part of seconds. Tarantool uses 28 | ``int32_t``, see a definition in src/lib/core/datetime.h. 29 | 30 | ``tzoffset`` is timezone offset in minutes from UTC. Tarantool uses 31 | ``int16_t`` type, see a structure definition in src/lib/core/datetime.h. 32 | 33 | ``tzindex`` is Olson timezone id. Tarantool uses ``int16_t`` type, see 34 | a structure definition in src/lib/core/datetime.h. If both 35 | ``tzoffset`` and ``tzindex`` are specified, ``tzindex`` has the 36 | preference and the ``tzoffset`` value is ignored. 37 | 38 | .. _datetime RFC: https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c 39 | """ 40 | 41 | from tarantool.msgpack_ext.types.datetime import ( 42 | NSEC_IN_SEC, 43 | Datetime, 44 | ) 45 | import tarantool.msgpack_ext.types.timezones as tt_timezones 46 | 47 | from tarantool.error import MsgpackError 48 | 49 | EXT_ID = 4 50 | """ 51 | `datetime`_ type id. 52 | """ 53 | 54 | BYTEORDER = 'little' 55 | 56 | SECONDS_SIZE_BYTES = 8 57 | NSEC_SIZE_BYTES = 4 58 | TZOFFSET_SIZE_BYTES = 2 59 | TZINDEX_SIZE_BYTES = 2 60 | 61 | 62 | def get_int_as_bytes(data, size): 63 | """ 64 | Get binary representation of integer value. 65 | 66 | :param data: Integer value. 67 | :type data: :obj:`int` 68 | 69 | :param size: Integer size, in bytes. 70 | :type size: :obj:`int` 71 | 72 | :return: Encoded integer. 73 | :rtype: :obj:`bytes` 74 | 75 | :meta private: 76 | """ 77 | 78 | return data.to_bytes(size, byteorder=BYTEORDER, signed=True) 79 | 80 | 81 | def encode(obj, _): 82 | """ 83 | Encode a datetime object. 84 | 85 | :param obj: Datetime to encode. 86 | :type: :obj: :class:`tarantool.Datetime` 87 | 88 | :return: Encoded datetime. 89 | :rtype: :obj:`bytes` 90 | 91 | :raise: :exc:`tarantool.Datetime.msgpack_encode` exceptions 92 | """ 93 | 94 | seconds = obj.value // NSEC_IN_SEC 95 | nsec = obj.nsec 96 | tzoffset = obj.tzoffset 97 | 98 | timezone = obj.tz 99 | if timezone != '': 100 | tzindex = tt_timezones.timezoneToIndex[timezone] 101 | else: 102 | tzindex = 0 103 | 104 | buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES) 105 | 106 | if (nsec != 0) or (tzoffset != 0) or (tzindex != 0): 107 | buf = buf + get_int_as_bytes(nsec, NSEC_SIZE_BYTES) 108 | buf = buf + get_int_as_bytes(tzoffset, TZOFFSET_SIZE_BYTES) 109 | buf = buf + get_int_as_bytes(tzindex, TZINDEX_SIZE_BYTES) 110 | 111 | return buf 112 | 113 | 114 | def get_bytes_as_int(data, cursor, size): 115 | """ 116 | Get integer value from binary data. 117 | 118 | :param data: MessagePack binary data. 119 | :type data: :obj:`bytes` 120 | 121 | :param cursor: Index after last parsed byte. 122 | :type cursor: :obj:`int` 123 | 124 | :param size: Integer size, in bytes. 125 | :type size: :obj:`int` 126 | 127 | :return: First value: parsed integer, second value: new cursor 128 | position. 129 | :rtype: first value: :obj:`int`, second value: :obj:`int` 130 | 131 | :meta private: 132 | """ 133 | 134 | part = data[cursor:cursor + size] 135 | return int.from_bytes(part, BYTEORDER, signed=True), cursor + size 136 | 137 | 138 | def decode(data, _): 139 | """ 140 | Decode a datetime object. 141 | 142 | :param obj: Datetime to decode. 143 | :type obj: :obj:`bytes` 144 | 145 | :return: Decoded datetime. 146 | :rtype: :class:`tarantool.Datetime` 147 | 148 | :raise: :exc:`~tarantool.error.MsgpackError`, 149 | :exc:`tarantool.Datetime` exceptions 150 | """ 151 | 152 | cursor = 0 153 | seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES) 154 | 155 | data_len = len(data) 156 | if data_len == (SECONDS_SIZE_BYTES + NSEC_SIZE_BYTES 157 | + TZOFFSET_SIZE_BYTES + TZINDEX_SIZE_BYTES): 158 | nsec, cursor = get_bytes_as_int(data, cursor, NSEC_SIZE_BYTES) 159 | tzoffset, cursor = get_bytes_as_int(data, cursor, TZOFFSET_SIZE_BYTES) 160 | tzindex, cursor = get_bytes_as_int(data, cursor, TZINDEX_SIZE_BYTES) 161 | elif data_len == SECONDS_SIZE_BYTES: 162 | nsec = 0 163 | tzoffset = 0 164 | tzindex = 0 165 | else: 166 | raise MsgpackError(f'Unexpected datetime payload length {data_len}') 167 | 168 | if tzindex != 0: 169 | if tzindex not in tt_timezones.indexToTimezone: 170 | raise MsgpackError(f'Failed to decode datetime with unknown tzindex "{tzindex}"') 171 | tz = tt_timezones.indexToTimezone[tzindex] 172 | return Datetime(timestamp=seconds, nsec=nsec, tz=tz, 173 | timestamp_since_utc_epoch=True) 174 | if tzoffset != 0: 175 | return Datetime(timestamp=seconds, nsec=nsec, tzoffset=tzoffset, 176 | timestamp_since_utc_epoch=True) 177 | 178 | return Datetime(timestamp=seconds, nsec=nsec, 179 | timestamp_since_utc_epoch=True) 180 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool `error`_ extension type support module. 3 | 4 | Refer to :mod:`~tarantool.msgpack_ext.types.error`. 5 | 6 | .. _error: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-error-type 7 | """ 8 | 9 | from tarantool.types import ( 10 | encode_box_error, 11 | decode_box_error, 12 | ) 13 | 14 | EXT_ID = 3 15 | """ 16 | `error`_ type id. 17 | """ 18 | 19 | 20 | def encode(obj, packer): 21 | """ 22 | Encode an error object. 23 | 24 | :param obj: Error to encode. 25 | :type obj: :class:`tarantool.BoxError` 26 | 27 | :param packer: msgpack packer to encode error dictionary. 28 | :type packer: :class:`msgpack.Packer` 29 | 30 | :return: Encoded error. 31 | :rtype: :obj:`bytes` 32 | """ 33 | 34 | err_map = encode_box_error(obj) 35 | return packer.pack(err_map) 36 | 37 | 38 | def decode(data, unpacker): 39 | """ 40 | Decode an error object. 41 | 42 | :param obj: Error to decode. 43 | :type obj: :obj:`bytes` 44 | 45 | :param unpacker: msgpack unpacker to decode error dictionary. 46 | :type unpacker: :class:`msgpack.Unpacker` 47 | 48 | :return: Decoded error. 49 | :rtype: :class:`tarantool.BoxError` 50 | """ 51 | 52 | unpacker.feed(data) 53 | err_map = unpacker.unpack() 54 | return decode_box_error(err_map) 55 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/interval.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool `datetime.interval`_ extension type support module. 3 | 4 | The interval MessagePack representation looks like this: 5 | 6 | .. code-block:: text 7 | 8 | +--------+-------------------------+-------------+----------------+ 9 | | MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval | 10 | +--------+-------------------------+-------------+----------------+ 11 | 12 | Packed interval consists of: 13 | 14 | * Packed number of non-zero fields. 15 | * Packed non-null fields. 16 | 17 | Each packed field has the following structure: 18 | 19 | .. code-block:: text 20 | 21 | +----------+=====================+ 22 | | field ID | field value | 23 | +----------+=====================+ 24 | 25 | The number of defined (non-null) fields can be zero. In this case, 26 | the packed interval will be encoded as integer 0. 27 | 28 | List of the field IDs: 29 | 30 | * 0 – year 31 | * 1 – month 32 | * 2 – week 33 | * 3 – day 34 | * 4 – hour 35 | * 5 – minute 36 | * 6 – second 37 | * 7 – nanosecond 38 | * 8 – adjust 39 | 40 | .. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type 41 | """ 42 | 43 | import msgpack 44 | 45 | from tarantool.error import MsgpackError 46 | 47 | from tarantool.msgpack_ext.types.interval import Interval, Adjust, id_map 48 | 49 | EXT_ID = 6 50 | """ 51 | `datetime.interval`_ type id. 52 | """ 53 | 54 | 55 | def encode(obj, _): 56 | """ 57 | Encode an interval object. 58 | 59 | :param obj: Interval to encode. 60 | :type: :obj: :class:`tarantool.Interval` 61 | 62 | :return: Encoded interval. 63 | :rtype: :obj:`bytes` 64 | """ 65 | 66 | buf = bytes() 67 | 68 | count = 0 69 | for field_id, field_name in id_map.items(): 70 | value = getattr(obj, field_name) 71 | 72 | if field_name == 'adjust': 73 | value = value.value 74 | 75 | if value != 0: 76 | buf = buf + msgpack.packb(field_id) + msgpack.packb(value) 77 | count = count + 1 78 | 79 | buf = msgpack.packb(count) + buf 80 | 81 | return buf 82 | 83 | 84 | def decode(data, unpacker): 85 | """ 86 | Decode an interval object. 87 | 88 | :param obj: Interval to decode. 89 | :type obj: :obj:`bytes` 90 | 91 | :param unpacker: msgpack unpacker to decode fields. 92 | :type unpacker: :class:`msgpack.Unpacker` 93 | 94 | :return: Decoded interval. 95 | :rtype: :class:`tarantool.Interval` 96 | 97 | :raise: :exc:`MsgpackError` 98 | """ 99 | 100 | # If MessagePack data does not contain a field value, it is zero. 101 | # If built not from MessagePack data, set argument values later. 102 | kwargs = { 103 | 'year': 0, 104 | 'month': 0, 105 | 'week': 0, 106 | 'day': 0, 107 | 'hour': 0, 108 | 'minute': 0, 109 | 'sec': 0, 110 | 'nsec': 0, 111 | 'adjust': Adjust(0), 112 | } 113 | 114 | if len(data) != 0: 115 | # Unpacker object is the only way to parse 116 | # a sequence of values in Python msgpack module. 117 | unpacker.feed(data) 118 | field_count = unpacker.unpack() 119 | for _ in range(field_count): 120 | field_id = unpacker.unpack() 121 | value = unpacker.unpack() 122 | 123 | if field_id not in id_map: 124 | raise MsgpackError(f'Unknown interval field id {field_id}') 125 | 126 | field_name = id_map[field_id] 127 | 128 | if field_name == 'adjust': 129 | try: 130 | value = Adjust(value) 131 | except ValueError as exc: 132 | raise MsgpackError(exc) from exc 133 | 134 | kwargs[id_map[field_id]] = value 135 | 136 | return Interval(**kwargs) 137 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/packer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool `extension`_ types encoding support. 3 | 4 | .. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ 5 | """ 6 | # pylint: disable=duplicate-code 7 | 8 | from decimal import Decimal 9 | from uuid import UUID 10 | from msgpack import ExtType 11 | 12 | from tarantool.types import BoxError 13 | from tarantool.msgpack_ext.types.datetime import Datetime 14 | from tarantool.msgpack_ext.types.interval import Interval 15 | 16 | import tarantool.msgpack_ext.decimal as ext_decimal 17 | import tarantool.msgpack_ext.uuid as ext_uuid 18 | import tarantool.msgpack_ext.error as ext_error 19 | import tarantool.msgpack_ext.datetime as ext_datetime 20 | import tarantool.msgpack_ext.interval as ext_interval 21 | 22 | encoders = [ 23 | {'type': Decimal, 'ext': ext_decimal}, 24 | {'type': UUID, 'ext': ext_uuid}, 25 | {'type': BoxError, 'ext': ext_error}, 26 | {'type': Datetime, 'ext': ext_datetime}, 27 | {'type': Interval, 'ext': ext_interval}, 28 | ] 29 | 30 | 31 | def default(obj, packer=None): 32 | """ 33 | :class:`msgpack.Packer` encoder. 34 | 35 | :param obj: Object to encode. 36 | :type obj: :class:`decimal.Decimal` or :class:`uuid.UUID` or 37 | or :class:`tarantool.BoxError` or :class:`tarantool.Datetime` 38 | or :class:`tarantool.Interval` 39 | 40 | :param packer: msgpack packer to work with common types 41 | (like dictionary in extended error payload) 42 | :type packer: :class:`msgpack.Packer`, optional 43 | 44 | :return: Encoded value. 45 | :rtype: :class:`msgpack.ExtType` 46 | 47 | :raise: :exc:`~TypeError` 48 | """ 49 | 50 | for encoder in encoders: 51 | if isinstance(obj, encoder['type']): 52 | return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer)) 53 | raise TypeError(f"Unknown type: {repr(obj)}") 54 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/tarantool/msgpack_ext/types/__init__.py -------------------------------------------------------------------------------- /tarantool/msgpack_ext/types/interval.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool `datetime.interval`_ extension type implementation module. 3 | """ 4 | 5 | from enum import Enum 6 | 7 | id_map = { 8 | 0: 'year', 9 | 1: 'month', 10 | 2: 'week', 11 | 3: 'day', 12 | 4: 'hour', 13 | 5: 'minute', 14 | 6: 'sec', 15 | 7: 'nsec', 16 | 8: 'adjust', 17 | } 18 | 19 | # https://github.com/tarantool/tarantool/blob/ff57f990f359f6d7866c1947174d8ba0e97b1ea6/src/lua/datetime.lua#L112-L146 20 | SECS_PER_DAY = 86400 21 | 22 | MIN_DATE_YEAR = -5879610 23 | MIN_DATE_MONTH = 6 24 | MIN_DATE_DAY = 22 25 | MAX_DATE_YEAR = 5879611 26 | MAX_DATE_MONTH = 7 27 | MAX_DATE_DAY = 11 28 | 29 | AVERAGE_DAYS_YEAR = 365.25 30 | AVERAGE_WEEK_YEAR = AVERAGE_DAYS_YEAR / 7 31 | INT_MAX = 2147483647 32 | MAX_YEAR_RANGE = MAX_DATE_YEAR - MIN_DATE_YEAR 33 | MAX_MONTH_RANGE = MAX_YEAR_RANGE * 12 34 | MAX_WEEK_RANGE = MAX_YEAR_RANGE * AVERAGE_WEEK_YEAR 35 | MAX_DAY_RANGE = MAX_YEAR_RANGE * AVERAGE_DAYS_YEAR 36 | MAX_HOUR_RANGE = MAX_DAY_RANGE * 24 37 | MAX_MIN_RANGE = MAX_HOUR_RANGE * 60 38 | MAX_SEC_RANGE = MAX_DAY_RANGE * SECS_PER_DAY 39 | MAX_NSEC_RANGE = INT_MAX 40 | 41 | max_val = { 42 | 'year': MAX_YEAR_RANGE, 43 | 'month': MAX_MONTH_RANGE, 44 | 'week': MAX_WEEK_RANGE, 45 | 'day': MAX_DAY_RANGE, 46 | 'hour': MAX_HOUR_RANGE, 47 | 'minute': MAX_MIN_RANGE, 48 | 'sec': MAX_SEC_RANGE, 49 | 'nsec': MAX_NSEC_RANGE, 50 | } 51 | 52 | 53 | def verify_range(intv): 54 | """ 55 | Check allowed values. Approach is the same as in tarantool/tarantool. 56 | 57 | :param intv: Raw interval to verify. 58 | :type intv: :class:`~tarantool.Interval` 59 | 60 | :raise: :exc:`ValueError` 61 | 62 | :meta private: 63 | """ 64 | 65 | for field_name, range_max in max_val.items(): 66 | val = getattr(intv, field_name) 67 | # Tarantool implementation has a bug 68 | # https://github.com/tarantool/tarantool/issues/8878 69 | if (val > range_max) or (val < -range_max): 70 | raise ValueError(f"value {val} of {field_name} is out of " 71 | f"allowed range [{-range_max}, {range_max}]") 72 | 73 | 74 | # https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.h#L34 75 | class Adjust(Enum): 76 | """ 77 | Interval adjustment mode for year and month arithmetic. Refer to 78 | :meth:`~tarantool.Datetime.__add__`. 79 | """ 80 | 81 | EXCESS = 0 82 | """ 83 | Overflow mode. 84 | """ 85 | 86 | NONE = 1 87 | """ 88 | Only truncation toward the end of month is performed. 89 | """ 90 | 91 | LAST = 2 92 | """ 93 | Mode when day snaps to the end of month, if it happens. 94 | """ 95 | 96 | 97 | class Interval(): 98 | """ 99 | Class representing Tarantool `datetime.interval`_ info. 100 | 101 | You can create :class:`~tarantool.Interval` objects either 102 | from MessagePack data or by using the same API as in Tarantool: 103 | 104 | .. code-block:: python 105 | 106 | di = tarantool.Interval(year=-1, month=2, week=-3, 107 | day=4, hour=5, minute=-6, sec=7, 108 | nsec=308543321, 109 | adjust=tarantool.IntervalAdjust.NONE) 110 | 111 | Its attributes (same as in init API) are exposed, so you can 112 | use them if needed. 113 | 114 | .. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type 115 | """ 116 | # pylint: disable=too-many-instance-attributes 117 | 118 | def __init__(self, *, year=0, month=0, week=0, 119 | day=0, hour=0, minute=0, sec=0, 120 | nsec=0, adjust=Adjust.NONE): 121 | """ 122 | :param year: Interval year value. 123 | :type year: :obj:`int`, optional 124 | 125 | :param month: Interval month value. 126 | :type month: :obj:`int`, optional 127 | 128 | :param week: Interval week value. 129 | :type week: :obj:`int`, optional 130 | 131 | :param day: Interval day value. 132 | :type day: :obj:`int`, optional 133 | 134 | :param hour: Interval hour value. 135 | :type hour: :obj:`int`, optional 136 | 137 | :param minute: Interval minute value. 138 | :type minute: :obj:`int`, optional 139 | 140 | :param sec: Interval seconds value. 141 | :type sec: :obj:`int`, optional 142 | 143 | :param nsec: Interval nanoseconds value. 144 | :type nsec: :obj:`int`, optional 145 | 146 | :param adjust: Interval adjustment rule. Refer to 147 | :meth:`~tarantool.Datetime.__add__`. 148 | :type adjust: :class:`~tarantool.IntervalAdjust`, optional 149 | 150 | :raise: :exc:`ValueError` 151 | """ 152 | # pylint: disable=too-many-arguments 153 | 154 | self.year = year 155 | self.month = month 156 | self.week = week 157 | self.day = day 158 | self.hour = hour 159 | self.minute = minute 160 | self.sec = sec 161 | self.nsec = nsec 162 | self.adjust = adjust 163 | 164 | verify_range(self) 165 | 166 | def __add__(self, other): 167 | """ 168 | Valid operations: 169 | 170 | * :class:`~tarantool.Interval` + :class:`~tarantool.Interval` 171 | = :class:`~tarantool.Interval` 172 | 173 | Adjust of the first operand is used in result. 174 | 175 | :param other: Second operand. 176 | :type other: :class:`~tarantool.Interval` 177 | 178 | :rtype: :class:`~tarantool.Interval` 179 | 180 | :raise: :exc:`TypeError` 181 | """ 182 | 183 | if not isinstance(other, Interval): 184 | raise TypeError("unsupported operand type(s) for +: " 185 | f"'{type(self)}' and '{type(other)}'") 186 | 187 | # Tarantool saves adjust of the first argument 188 | # 189 | # Tarantool 2.10.1-0-g482d91c66 190 | # 191 | # tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'} 192 | # --- 193 | # ... 194 | # 195 | # tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'} 196 | # --- 197 | # ... 198 | # 199 | # tarantool> (dt1 + dt2).adjust 200 | # --- 201 | # - 'cdata: 2' 202 | # ... 203 | 204 | return Interval( 205 | year=self.year + other.year, 206 | month=self.month + other.month, 207 | week=self.week + other.week, 208 | day=self.day + other.day, 209 | hour=self.hour + other.hour, 210 | minute=self.minute + other.minute, 211 | sec=self.sec + other.sec, 212 | nsec=self.nsec + other.nsec, 213 | adjust=self.adjust, 214 | ) 215 | 216 | def __sub__(self, other): 217 | """ 218 | Valid operations: 219 | 220 | * :class:`~tarantool.Interval` - :class:`~tarantool.Interval` 221 | = :class:`~tarantool.Interval` 222 | 223 | Adjust of the first operand is used in result. 224 | 225 | :param other: Second operand. 226 | :type other: :class:`~tarantool.Interval` 227 | 228 | :rtype: :class:`~tarantool.Interval` 229 | 230 | :raise: :exc:`TypeError` 231 | """ 232 | 233 | if not isinstance(other, Interval): 234 | raise TypeError("unsupported operand type(s) for -: " 235 | f"'{type(self)}' and '{type(other)}'") 236 | 237 | # Tarantool saves adjust of the first argument 238 | # 239 | # Tarantool 2.10.1-0-g482d91c66 240 | # 241 | # tarantool> dt1 = datetime.interval.new{year = 2, adjust='last'} 242 | # --- 243 | # ... 244 | # 245 | # tarantool> dt2 = datetime.interval.new{year = 1, adjust='excess'} 246 | # --- 247 | # ... 248 | # 249 | # tarantool> (dt1 - dt2).adjust 250 | # --- 251 | # - 'cdata: 2' 252 | # ... 253 | 254 | return Interval( 255 | year=self.year - other.year, 256 | month=self.month - other.month, 257 | week=self.week - other.week, 258 | day=self.day - other.day, 259 | hour=self.hour - other.hour, 260 | minute=self.minute - other.minute, 261 | sec=self.sec - other.sec, 262 | nsec=self.nsec - other.nsec, 263 | adjust=self.adjust, 264 | ) 265 | 266 | def __eq__(self, other): 267 | """ 268 | Compare equality of each field, no casts. 269 | 270 | :param other: Second operand. 271 | :type other: :class:`~tarantool.Interval` 272 | 273 | :rtype: :obj:`bool` 274 | """ 275 | 276 | if not isinstance(other, Interval): 277 | return False 278 | 279 | # Tarantool interval compare is naive too 280 | # 281 | # Tarantool 2.10.1-0-g482d91c66 282 | # 283 | # tarantool> datetime.interval.new{hour=1} == datetime.interval.new{min=60} 284 | # --- 285 | # - false 286 | # ... 287 | 288 | for _, field_name in id_map.items(): 289 | if getattr(self, field_name) != getattr(other, field_name): 290 | return False 291 | 292 | return True 293 | 294 | def __repr__(self): 295 | return f'tarantool.Interval(year={self.year}, month={self.month}, week={self.week}, ' + \ 296 | f'day={self.day}, hour={self.hour}, minute={self.minute}, sec={self.sec}, ' + \ 297 | f'nsec={self.nsec}, adjust={self.adjust})' 298 | 299 | __str__ = __repr__ 300 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/types/timezones/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tarantool timezones module. 3 | """ 4 | 5 | from tarantool.msgpack_ext.types.timezones.timezones import ( 6 | TZ_AMBIGUOUS, 7 | indexToTimezone, 8 | timezoneToIndex, 9 | timezoneAbbrevInfo, 10 | ) 11 | 12 | __all__ = ['TZ_AMBIGUOUS', 'indexToTimezone', 'timezoneToIndex', 13 | 'timezoneAbbrevInfo'] 14 | -------------------------------------------------------------------------------- /tarantool/msgpack_ext/types/timezones/gen-timezones.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | 4 | SRC_COMMIT="9ee45289e01232b8df1413efea11db170ae3b3b4" 5 | SRC_FILE=timezones.h 6 | DST_FILE=timezones.py 7 | 8 | [ -e ${SRC_FILE} ] && rm ${SRC_FILE} 9 | wget -O ${SRC_FILE} \ 10 | https://raw.githubusercontent.com/tarantool/tarantool/${SRC_COMMIT}/src/lib/tzcode/timezones.h 11 | 12 | # We don't need aliases in indexToTimezone because Tarantool always replace it: 13 | # 14 | # tarantool> T = date.parse '2022-01-01T00:00 Pacific/Enderbury' 15 | # --- 16 | # ... 17 | # tarantool> T 18 | # --- 19 | # - 2022-01-01T00:00:00 Pacific/Kanton 20 | # ... 21 | # 22 | # So we can do the same and don't worry, be happy. 23 | 24 | cat < ${DST_FILE} 25 | """ 26 | Tarantool timezone info. Automatically generated by 27 | \`\`gen-timezones.sh\`\`. 28 | """ 29 | # pylint: disable=too-many-lines 30 | 31 | TZ_UTC = 0x01 32 | TZ_RFC = 0x02 33 | TZ_MILITARY = 0x04 34 | TZ_AMBIGUOUS = 0x08 35 | TZ_NYI = 0x10 36 | TZ_OLSON = 0x20 37 | TZ_ALIAS = 0x40 38 | TZ_DST = 0x80 39 | 40 | indexToTimezone = { 41 | EOF 42 | 43 | grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ 44 | | awk '{printf(" %s: %s,\n", $1, $3)}' >> ${DST_FILE} 45 | grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ 46 | | awk '{printf(" %s: %s,\n", $1, $2)}' >> ${DST_FILE} 47 | 48 | cat <> ${DST_FILE} 49 | } 50 | 51 | timezoneToIndex = { 52 | EOF 53 | 54 | grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ 55 | | awk '{printf(" %s: %s,\n", $3, $1)}' >> ${DST_FILE} 56 | grep ZONE_UNIQUE ${SRC_FILE} | sed "s/ZONE_UNIQUE( *//g" | sed "s/[),]//g" \ 57 | | awk '{printf(" %s: %s,\n", $2, $1)}' >> ${DST_FILE} 58 | grep ZONE_ALIAS ${SRC_FILE} | sed "s/ZONE_ALIAS( *//g" | sed "s/[),]//g" \ 59 | | awk '{printf(" %s: %s,\n", $2, $1)}' >> ${DST_FILE} 60 | 61 | cat <> ${DST_FILE} 62 | } 63 | 64 | timezoneAbbrevInfo = { 65 | EOF 66 | 67 | grep ZONE_ABBREV ${SRC_FILE} | sed "s/ZONE_ABBREV( *//g" | sed "s/[),]//g" \ 68 | | awk '{printf(" %s: {\"offset\": %d, \"category\": %s},\n", $3, $2, $4)}' | sed "s/|/ | /g" >> ${DST_FILE} 69 | echo "}" >> ${DST_FILE} 70 | 71 | rm timezones.h 72 | 73 | python < 0 and tail[0] == '(': 130 | (protocol, _, tail) = tail[1:].partition(') ') 131 | # Extract protocol name - a string between (parentheses) 132 | result.protocol = protocol 133 | if result.protocol != "Binary": 134 | return result 135 | # Parse UUID for binary protocol 136 | (uuid_buf, _, tail) = tail.partition(' ') 137 | if result.version_id >= version_id(1, 6, 7): 138 | result.uuid = uuid.UUID(uuid_buf.strip()) 139 | elif result.version_id < version_id(1, 6, 7): 140 | # Tarantool < 1.6.7 doesn't add "(Binary)" to greeting 141 | result.protocol = "Binary" 142 | elif len(tail.strip()) != 0: 143 | raise ValueError("x") # Unsupported greeting 144 | result.salt = base64_decode(greeting_buf[64:])[:20] 145 | return result 146 | except ValueError as exc: 147 | print('exx', exc) 148 | raise ValueError("Invalid greeting: " + str(greeting_buf)) from exc 149 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/test/__init__.py -------------------------------------------------------------------------------- /test/data/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDLzCCAhegAwIBAgIUWsIywvK+tkdt1ew8Hyl+q8AimxswDQYJKoZIhvcNAQEL 3 | BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y 4 | MjEyMjMxNTM1MjhaFw00NTA1MjgxNTM1MjhaMCcxCzAJBgNVBAYTAlVTMRgwFgYD 5 | VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 6 | AoIBAQDHwdnavXTN4Km9bwnRBV+2GW4Z2eFQ5fw2/Ln0BRUp+Wqzc6Cu9sfxH4DV 7 | 6+A0H1swcc0kctdPGZvApk6b4A98/Pry09JEMbCzLlHaVGeNKQr+g7Vrb2/v9337 8 | ofRbhjA+CsD3bBQio5U3ANTvcB2KnUnqA/PmohIpLGyipOpwz2Dr6N4UABZ/wd0o 9 | mYGeRQx7BCgDzK3b+eVmQkWlHuEkAQv7qAVzKGIkw3lUq2Hikq65b+1DXWsENHSC 10 | GRXNRklu/lnsZPfNIcqu0z0OzkFNZ9VWCSQLRmPBzTv5ATSVmZZiiHJSR89q41MK 11 | T2cw9layXRAmtzDX5VUlPvF5GQUtAgMBAAGjUzBRMB0GA1UdDgQWBBSLeibGvWpo 12 | u0/ecImiZxrGfOJgFTAfBgNVHSMEGDAWgBSLeibGvWpou0/ecImiZxrGfOJgFTAP 13 | BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC9pXVMMKiaS3LLA+Vl 14 | oNsLKGtkbLbFYJSvjnNVlcVv64jdEQNeBWdYA4V4FDkkOc8PZ5NNqLiMMq6YEcyz 15 | zPJkvtkIVrGh67TKdcbqyoLXyWcnqne0IN+CDhZuspvY5w8BX18q5rM0vtdp/si0 16 | z5BM0o6hzUKU8smCOSjDQr7PtbQaPJT0JFLUmNl64TTymg/Wim7i72E5V7wSX1Zp 17 | VkaWDRjRG6H/lIX/88ppEl6aOAEdezgsu68fNjgJBlUK1qkJVvLg/3ddm/od1kwf 18 | 5j3289P1myOvTJoWbaUUVI2GON+kPEAniyi08iJTgHUOHJUEUrbb0STmcGF+rCLT 19 | vPdc 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /test/data/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/test/data/empty -------------------------------------------------------------------------------- /test/data/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xeuo pipefail 3 | # An example how-to re-generate testing certificates (because usually 4 | # TLS certificates have expiration dates and some day they will expire). 5 | # 6 | # The instruction is valid for: 7 | # 8 | # $ openssl version 9 | # OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) 10 | 11 | cat < domains_localhost.ext 12 | authorityKeyIdentifier=keyid,issuer 13 | basicConstraints=CA:FALSE 14 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 15 | subjectAltName = @alt_names 16 | [alt_names] 17 | DNS.1 = localhost 18 | IP.1 = 127.0.0.1 19 | EOF 20 | 21 | cat < domains_invalidhost.ext 22 | authorityKeyIdentifier=keyid,issuer 23 | basicConstraints=CA:FALSE 24 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 25 | subjectAltName = @alt_names 26 | [alt_names] 27 | DNS.1 = invalidhostname 28 | EOF 29 | 30 | openssl req -x509 -nodes -new -sha256 -days 8192 -newkey rsa:2048 -keyout ca.key -out ca.pem -subj "/C=US/CN=Example-Root-CA" 31 | openssl x509 -outform pem -in ca.pem -out ca.crt 32 | 33 | openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost" 34 | openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_localhost.ext -out localhost.crt 35 | openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_invalidhost.ext -out invalidhost.crt 36 | 37 | password=mysslpassword 38 | 39 | # Tarantool tries every line from the password file. 40 | cat < passwords 41 | unusedpassword 42 | $password 43 | EOF 44 | 45 | cat < invalidpasswords 46 | unusedpassword1 47 | EOF 48 | 49 | openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key 50 | -------------------------------------------------------------------------------- /test/data/invalidhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDczCCAlugAwIBAgIUOT0BKqbGYnCaZSKoTdJjaDJyh1owDQYJKoZIhvcNAQEL 3 | BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y 4 | MjEyMjMxNTM1MjhaFw00NTA1MjgxNTM1MjhaMGcxCzAJBgNVBAYTAlVTMRIwEAYD 5 | VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt 6 | cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG 7 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhuQAPWqsyEFS64fuNJ8WNXRzd1ixxyEPcsj 8 | BQLcvATP8H46K2EEZcLLR4re3ZeSWCR2tG7GOlJXtpivEkPhi/jxbxluIHeghPat 9 | 4Q71juZ5y2k+nv1bkP2JT0bjJyflnUY2lno4oCOWswvu0pTTWsJ0/Fenxw0NFD/D 10 | 0WjFo1n+J6LOWxAKIcksVyHK2/duLzPqSPSOc6/MxheteTH8bzD7H5/wbNyiPv0L 11 | qb6IAYzbrTEjAw4wb7yyzat928IqYaltRFYx6T7DyxM/IDNI7kZ+hfI41G8aDER8 12 | 9mm6qNIiS/mLXlnkTHVG4SpKx61ALYz87xNYeF/Wyyajf5TkoQIDAQABo1cwVTAf 13 | BgNVHSMEGDAWgBSLeibGvWpou0/ecImiZxrGfOJgFTAJBgNVHRMEAjAAMAsGA1Ud 14 | DwQEAwIE8DAaBgNVHREEEzARgg9pbnZhbGlkaG9zdG5hbWUwDQYJKoZIhvcNAQEL 15 | BQADggEBACLz1JfEphvK2+axxKZbLCaKjEUr2WVAj2MH9fJ8SFINOk7Rd5wILkJP 16 | NV9meItqMfdmclxORH5h/069mHHzzTofoXBdZNbrwSEFWhAACexC+tqvUPvu2yKj 17 | hddhfQW66WrWUQPrlJh9UETt6AoCXN4G75Mkmm1I8o3wzydhWzXpVlqu8gEe45gH 18 | 7J/vU+Kzf1+B3HkfNEYbGDKC+MPsQcFH+lM8U42Jqs4UZeynH2iqwjeNh7DvoYV4 19 | 4MjEGG3aqtj3XAxnqqNEnd2ChnsiDAnCMFnTJn4EU35iBDKZM0iECHhIuyewsBhI 20 | vRb9+qM6LNIWDQ0zC7i6Xz2EHeI1EEA= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/data/invalidpasswords: -------------------------------------------------------------------------------- 1 | unusedpassword1 2 | -------------------------------------------------------------------------------- /test/data/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDczCCAlugAwIBAgIUOT0BKqbGYnCaZSKoTdJjaDJyh1kwDQYJKoZIhvcNAQEL 3 | BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y 4 | MjEyMjMxNTM1MjhaFw00NTA1MjgxNTM1MjhaMGcxCzAJBgNVBAYTAlVTMRIwEAYD 5 | VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt 6 | cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG 7 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwhuQAPWqsyEFS64fuNJ8WNXRzd1ixxyEPcsj 8 | BQLcvATP8H46K2EEZcLLR4re3ZeSWCR2tG7GOlJXtpivEkPhi/jxbxluIHeghPat 9 | 4Q71juZ5y2k+nv1bkP2JT0bjJyflnUY2lno4oCOWswvu0pTTWsJ0/Fenxw0NFD/D 10 | 0WjFo1n+J6LOWxAKIcksVyHK2/duLzPqSPSOc6/MxheteTH8bzD7H5/wbNyiPv0L 11 | qb6IAYzbrTEjAw4wb7yyzat928IqYaltRFYx6T7DyxM/IDNI7kZ+hfI41G8aDER8 12 | 9mm6qNIiS/mLXlnkTHVG4SpKx61ALYz87xNYeF/Wyyajf5TkoQIDAQABo1cwVTAf 13 | BgNVHSMEGDAWgBSLeibGvWpou0/ecImiZxrGfOJgFTAJBgNVHRMEAjAAMAsGA1Ud 14 | DwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEL 15 | BQADggEBAC0cUnap+JQFItZV7moPgcQJuiQ/nRWYifx+cQ2clMP3OypHbxIlYaIM 16 | Te1+IJ+4u4x/7QecqwbFMqnMl6OaitkEV9LuU4lrnYWd30m9TPoPT1pgV759rG2d 17 | lS/sYDQCY5RRpCDg6lUsrCBIMVXG2MT/99rIokFE9kGo8zQaQDBhSl4fIRMDMmA9 18 | gMgwDoBW/DPDvNc7/VhQY4aUSDy/iudqSfAa24CCOI8hC5eLnnZjSfEqaA50fK2w 19 | TPMlnL2q+QlJjFWhiPyumkGxkUsBRjGMviaXRW8oR8TF+Bub6aTk4qIkoeo8pu38 20 | BEWjCLicBv68/xEnmetubUX/CgdNts8= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/data/localhost.enc.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,A68D3D9FDF7B5ACD2968FB1066D868D5 4 | 5 | aTdzl4pXl+My3e0NGDmoXiDYaD/pXuUzTd3s9QnUWV/NI5rvAxyHZi7UjU1eEX/V 6 | 6C9jk2oMEUg9ceqtO83q0lpMpYuF9zNS1bvCZxjrDfHzc1/zUCNWw97Zbxb6pehR 7 | nCuu4zKIUhxD/K5yS6GmxQMIaELXm9/g8n+H9IrOENcPgH7Bma7cIBaf5gAgDID6 8 | +Te8oQuu8ZmF2MZt5p1zAyT29Ba2ia/l6nSaJytElaEe9DF6kjL0PHb4+NODBKtI 9 | ouwBQaVzdXvHNbpEDmUWgl2UslEESKZBC4ERTlzOpgEAQkIOjGC78cFXdyZfRddu 10 | vjt/uDyvFisV1CQjulZO3PIgXDiJQEQVIMrTvmHZvrj9jWtbxHDdePozTH/TK1ni 11 | Y898rJiN2/NYg1tD51mzAjg1c/HLgEVee3mS5l5h+OS4qGyQfCq2eLHET6SWijb9 12 | lARaVqIMKxXFsYe2uYF8Fxjeu5KnhuK2qmx4iZFReA1UKHIUwsA3z5lTZb+bcTV5 13 | Qj6S/Oq0y+p8eUilPtvdMtzI0ZsmlTq0eY8W3B+8bpXHmQ+3PE7DfpicyA2Yi+Gq 14 | 3v7iLvMlEasCeaavmRa9+tNDNaEu7qD8Cjt1HKpp+FnsCoZr2Ft5Mn+r2VM5WAyj 15 | t+0nGQwA13eaL5Ama1gWBYV8TrU1sntK5Fn+qNMB8nGrFxjHiLCPXRdB9cN67MdI 16 | aBAtToxBYd8Ap/MS0kYiNTWIdbJNSt8Ns4NRUmyCEaBZ518sPRbEZAf3v6c5zL0D 17 | 7nGGCKHxhmLZfpvW7UikiuDLSt2nsU/uLJ9sc4XeMP+ZvOG1HGoKnoIT26kk+T2V 18 | qSTkmpznJsF5vm+DI7UBuRA61DIm9Eai+zvleF500Slu54nTtXXNO/4FgdVFaygX 19 | caECfgL4eeIGiHYHCYH6mgnnGyZ1klj1d8mOYEUnFxmX8ZCgZD6+HyOOC5QmW7t5 20 | ggubqRPCcA0QnLn6KOZpK44dKc2tU6Wk57z68Vtano2KXJAmccvJNnhh/A1UAXCV 21 | 2kxSE66zyfA84adseWFT6eZR/OhQa/AY/zBGN5u1TBdp+LpWOK52FHfVLEaUW6rk 22 | 3bQ7qQglz0SR8ccmyG8uXpJSgSeOblgqb6EobSnhmuWU6hHChm1lTk1AAH589Gjf 23 | 7NuCV16XAEA0MSl/IK+nfynjL+8PffUi0RM3tRNIGdkvz0mVC/RMce3AHWArJ4G1 24 | XF3BXWtVEq3dtR+N6jTB/pZp2yEm+IqZKJ/2D9UetG//LkBpdGj+ypjB+GVTFskf 25 | DltMhBBCGrqUsN8zu2+DrZfuXpnT+z+pN/votQqPFaQ4qOxsE+vToErGela9jWDt 26 | w+5U6d0VQdZbFOq1ptY1+4yxzmxAbIgQDPIxdW0urEwIAedPyfLqGeEzwhf6rDii 27 | ZzRXevbYhwRMRJef57i9h2TUDvcXq3VSTTek6X+YNDBWcsl+nFcG3o8HFHnsb5t6 28 | CpQ6VMI3469T+gl+6PZ15F/eE+mI9F9Ov/3+pJWII8p/qidu/Y469LrH3qI5PIIj 29 | 1VNHMWtDE5I5qR1FsqaZJk7uHlgbVKj5z0Sw6x/iOzksxi1P67hAr0ErY8rrUvsj 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /test/data/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCG5AA9aqzIQVL 3 | rh+40nxY1dHN3WLHHIQ9yyMFAty8BM/wfjorYQRlwstHit7dl5JYJHa0bsY6Ule2 4 | mK8SQ+GL+PFvGW4gd6CE9q3hDvWO5nnLaT6e/VuQ/YlPRuMnJ+WdRjaWejigI5az 5 | C+7SlNNawnT8V6fHDQ0UP8PRaMWjWf4nos5bEAohySxXIcrb924vM+pI9I5zr8zG 6 | F615MfxvMPsfn/Bs3KI+/QupvogBjNutMSMDDjBvvLLNq33bwiphqW1EVjHpPsPL 7 | Ez8gM0juRn6F8jjUbxoMRHz2abqo0iJL+YteWeRMdUbhKkrHrUAtjPzvE1h4X9bL 8 | JqN/lOShAgMBAAECggEAUI+f9MYBUtchm4dpIww5D6WurgJ0PK9ZM0xb/HhzpJVa 9 | uDWrbtWVPabbN5YraUy/MFXx7nELVKlYPjCgeLQzqCRqD/I+Arujo4u+HupWgDin 10 | 1ASaOOJuXn7bvktte5LTebLqdQnE9rHOmgEZ3cxm7ARJCXJX8AU6qGzOmNsjK+gz 11 | Lf3Yqf2Gmct1Bp+JBea9YB3dAWyph10lExIisxfh7BCwaqaNeA8n8Wv17IGL9B5j 12 | OLC4HAPQJRYvfouFl6eB5TZqLagg+KW3qiBuxafEiuPL7pJiNJnTc1ILbMSwi2x9 13 | kWTe7RbhsTSoVn5lzHn5Oq9ZmJluSmFTZdwCLGuwAQKBgQD+UsKGygV7y8ihtvJm 14 | jN7T1CzloqqAuHkn3OvWt40nRsBs38FX+rnOavLSljbB+n4GGR3o2wBGaNQN5iOn 15 | RM1RcBSIHBdgZvACtIauybNwmzpRvO57I+ehqC3YDOl+3wKo+fPpN9aKWOEL9KJp 16 | G4dNxdkhVLL2yBtD7M72LD4eKQKBgQDDYywdBuTAts+chrFaUaiI0qOoMfG0sh2H 17 | i0lzUag/vL6X7lXvf7iDG4hYxU4ExQDCsDKxTXBI0FYUJQR8gg1NrjDCe8zWBPSM 18 | iMTzXh4q+OrUqzbId1fPAY1RzG+xsqXTsXjUlDy8qUd9rh+1mS0fFgDEo4ispUdO 19 | IfZSSnlxuQKBgHf/AH2skE3K5w02TsUILpLwB4cJ6zz0zWV7nWMgE9+2SFCWeplS 20 | WZ0FZTDrY0a/M/sYmr4lpsmR6IvuTGA93EpSgb1+06DOsOv11Z5e2OWGuEucw0Ei 21 | vcXOnmLUJM+R1aV42hbuG7IHIZgMgxzoujx932cUmaRK4mJ4N2Z7lYuRAoGAZspP 22 | tN2hjrkeM+ywdSGsln6qVpwf2r4xxtNCSwbUiuOTKX7beuooeeEMNBdo2h2CLupf 23 | YOOqhMQF5Qcseww4T3uqb0aOFrH4rc5uPtJu8JCPil6grYoLif35COWShVvE3b/q 24 | H3v1EBPGZpoqWHFDSa1brheSmbFB+Brp6ZUAmxECgYEAkuJwTsn1GQSH9aDNC3ss 25 | AeNwsRf6nzqAkYt+YSOR6A69VPsNbAjgtG1JFns8hL85r4zj0nmDsTbLXYXw3Meb 26 | 96rm8CR1nizHfzfyBcF+aF6TeuDKNHDcotyD9+dgwSQz4MHZ0GK76Ic4rz/wqup8 27 | ZVvgfzBA6cMoCWnIfYheMho= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/data/passwords: -------------------------------------------------------------------------------- 1 | unusedpassword 2 | mysslpassword 3 | -------------------------------------------------------------------------------- /test/setup_command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This module describes class implementing `python setup.py test`. 4 | """ 5 | 6 | import unittest 7 | 8 | import setuptools 9 | 10 | try: 11 | from setuptools.errors import BaseError 12 | except (ModuleNotFoundError, ImportError): 13 | # pylint: disable=deprecated-module 14 | from distutils.errors import DistutilsError as BaseError 15 | 16 | 17 | class Test(setuptools.Command): 18 | """ 19 | Class implementing `python setup.py test`. 20 | """ 21 | # pylint: disable=bad-option-value,no-self-use 22 | 23 | user_options = [] 24 | description = 'Run tests' 25 | 26 | def initialize_options(self): 27 | """ 28 | Do nothing. setuptools requires to override this abstract 29 | method. 30 | """ 31 | 32 | def finalize_options(self): 33 | """ 34 | Do nothing. setuptools requires to override this abstract 35 | method. 36 | """ 37 | 38 | def run(self): 39 | """ 40 | Find all tests in test/tarantool/ and run them 41 | """ 42 | 43 | tests = unittest.defaultTestLoader.discover('test', pattern='suites') 44 | test_runner = unittest.TextTestRunner(verbosity=2) 45 | result = test_runner.run(tests) 46 | if not result.wasSuccessful(): 47 | raise BaseError('There are failed tests') 48 | -------------------------------------------------------------------------------- /test/suites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module tests entrypoint. 3 | """ 4 | 5 | import os 6 | import unittest 7 | 8 | from .test_schema import TestSuiteSchemaUnicodeConnection 9 | from .test_schema import TestSuiteSchemaBinaryConnection 10 | from .test_dml import TestSuiteRequest 11 | from .test_protocol import TestSuiteProtocol 12 | from .test_reconnect import TestSuiteReconnect 13 | from .test_mesh import TestSuiteMesh 14 | from .test_pool import TestSuitePool 15 | from .test_execute import TestSuiteExecute 16 | from .test_dbapi import TestSuiteDBAPI 17 | from .test_encoding import TestSuiteEncoding 18 | from .test_socket_fd import TestSuiteSocketFD 19 | from .test_ssl import TestSuiteSsl 20 | from .test_decimal import TestSuiteDecimal 21 | from .test_uuid import TestSuiteUUID 22 | from .test_datetime import TestSuiteDatetime 23 | from .test_interval import TestSuiteInterval 24 | from .test_package import TestSuitePackage 25 | from .test_error_ext import TestSuiteErrorExt 26 | from .test_push import TestSuitePush 27 | from .test_connection import TestSuiteConnection 28 | from .test_crud import TestSuiteCrud 29 | 30 | test_cases = (TestSuiteSchemaUnicodeConnection, 31 | TestSuiteSchemaBinaryConnection, 32 | TestSuiteRequest, TestSuiteProtocol, TestSuiteReconnect, 33 | TestSuiteMesh, TestSuiteExecute, TestSuiteDBAPI, 34 | TestSuiteEncoding, TestSuitePool, TestSuiteSsl, 35 | TestSuiteDecimal, TestSuiteUUID, TestSuiteDatetime, 36 | TestSuiteInterval, TestSuitePackage, TestSuiteErrorExt, 37 | TestSuitePush, TestSuiteConnection, TestSuiteCrud, TestSuiteSocketFD) 38 | 39 | 40 | def load_tests(loader, tests, pattern): 41 | """ 42 | Add suites to test run. 43 | """ 44 | # pylint: disable=unused-argument 45 | 46 | suite = unittest.TestSuite() 47 | for testc in test_cases: 48 | suite.addTests(loader.loadTestsFromTestCase(testc)) 49 | return suite 50 | 51 | 52 | __tmp = os.getcwd() 53 | os.chdir(os.path.abspath(os.path.dirname(__file__))) 54 | 55 | os.chdir(__tmp) 56 | 57 | # Workaround to disable unittest output truncating 58 | __import__('sys').modules['unittest.util']._MAX_LENGTH = 99999 # pylint: disable=protected-access 59 | -------------------------------------------------------------------------------- /test/suites/box.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local is_compat, compat = pcall(require, "compat") 4 | local os = require('os') 5 | 6 | local admin_listen = os.getenv("ADMIN") 7 | local primary_listen = os.getenv("LISTEN") 8 | local auth_type = os.getenv("AUTH_TYPE") 9 | 10 | local SQL_SEQ_SCAN_DEFAULT = os.getenv("SQL_SEQ_SCAN_DEFAULT") 11 | if is_compat and SQL_SEQ_SCAN_DEFAULT then 12 | compat.sql_seq_scan_default = SQL_SEQ_SCAN_DEFAULT 13 | end 14 | 15 | require('console').listen(admin_listen) 16 | box.cfg{ 17 | listen = primary_listen, 18 | memtx_memory = 0.1 * 1024^3, -- 0.1 GiB 19 | pid_file = "box.pid", 20 | auth_type = (auth_type:len() > 0) and auth_type or nil, 21 | } 22 | 23 | rawset(_G, 'ready', true) 24 | -------------------------------------------------------------------------------- /test/suites/crud_mock_server.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local admin_listen = os.getenv("ADMIN") 4 | local primary_listen = os.getenv("LISTEN") 5 | 6 | require('console').listen(admin_listen) 7 | box.cfg{ 8 | listen = primary_listen, 9 | memtx_memory = 0.1 * 1024^3, -- 0.1 GiB 10 | pid_file = "box.pid", 11 | } 12 | 13 | box.schema.user.grant('guest', 'execute', 'universe', nil, {if_not_exists = true}) 14 | 15 | local function mock_replace() 16 | error('Unexpected connection error') 17 | end 18 | 19 | rawset(_G, 'crud', {replace = mock_replace}) 20 | 21 | rawset(_G, 'ready', true) 22 | -------------------------------------------------------------------------------- /test/suites/crud_server.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local function replicaset_uuid() 4 | if box.info().replicaset ~= nil and box.info().replicaset.uuid ~= nil then 5 | return box.info().replicaset.uuid 6 | end 7 | 8 | return box.info().cluster.uuid 9 | end 10 | 11 | local function configure_crud_instance(primary_listen, crud, vshard) 12 | box.schema.create_space( 13 | 'tester', { 14 | format = { 15 | {name = 'id', type = 'unsigned'}, 16 | {name = 'bucket_id', type = 'unsigned'}, 17 | {name = 'name', type = 'string'}, 18 | } 19 | }) 20 | box.space.tester:create_index('primary_index', { 21 | parts = { 22 | {field = 1, type = 'unsigned'}, 23 | }, 24 | }) 25 | box.space.tester:create_index('bucket_id', { 26 | parts = { 27 | {field = 2, type = 'unsigned'}, 28 | }, 29 | unique = false, 30 | }) 31 | 32 | -- Setup vshard. 33 | _G.vshard = vshard 34 | box.once('guest', function() 35 | box.schema.user.grant('guest', 'super') 36 | end) 37 | local uri = 'guest@0.0.0.0:' .. primary_listen 38 | local cfg = { 39 | bucket_count = 300, 40 | sharding = { 41 | [replicaset_uuid()] = { 42 | replicas = { 43 | [box.info().uuid] = { 44 | uri = uri, 45 | name = 'storage', 46 | master = true, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | vshard.storage.cfg(cfg, box.info().uuid) 53 | vshard.router.cfg(cfg) 54 | vshard.router.bootstrap() 55 | 56 | -- Initialize crud. 57 | crud.init_storage() 58 | crud.init_router() 59 | crud.cfg{stats = true} 60 | end 61 | 62 | local crud_imported, crud = pcall(require, 'crud') 63 | local vshard_imported, vshard = pcall(require, 'vshard') 64 | 65 | local admin_listen = os.getenv("ADMIN") 66 | local primary_listen = os.getenv("LISTEN") 67 | 68 | require('console').listen(admin_listen) 69 | box.cfg{ 70 | listen = primary_listen, 71 | memtx_memory = 0.1 * 1024^3, -- 0.1 GiB 72 | pid_file = "box.pid", 73 | } 74 | 75 | box.schema.user.grant( 76 | 'guest', 77 | 'read,write,execute', 78 | 'universe' 79 | ) 80 | 81 | if crud_imported == false or vshard_imported == false then 82 | -- Set flag for unittest. 83 | _G['ROCKS_IMPORT_FAIL'] = true 84 | local fail_msg = 'The crud/vshard modules are not detected, ' .. 85 | 'installation via rocks install is required ' .. 86 | 'for CRUD testing purposes. You can use ' .. 87 | ' or ' .. 88 | ' to install modules' 89 | -- The print output will be captured in the logs. 90 | print(fail_msg) 91 | else 92 | configure_crud_instance(primary_listen, crud, vshard) 93 | end 94 | 95 | rawset(_G, 'ready', true) 96 | -------------------------------------------------------------------------------- /test/suites/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarantool/tarantool-python/28310fc4e927c77d3be75e98360bf3b09f72482a/test/suites/lib/__init__.py -------------------------------------------------------------------------------- /test/suites/lib/remote_tarantool_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides helpers to work with remote Tarantool server 3 | (used on Windows). 4 | """ 5 | 6 | import sys 7 | import os 8 | import random 9 | import string 10 | import time 11 | 12 | from .tarantool_admin import TarantoolAdmin 13 | 14 | 15 | # a time during which try to acquire a lock 16 | AWAIT_TIME = 60 # seconds 17 | 18 | # on which port bind a socket for binary protocol 19 | BINARY_PORT = 3301 20 | 21 | 22 | def get_random_string(): 23 | """ 24 | :type: :obj:`str` 25 | """ 26 | return ''.join(random.choice(string.ascii_lowercase) for _ in range(16)) 27 | 28 | 29 | class RemoteTarantoolServer(): 30 | """ 31 | Class to work with remote Tarantool server. 32 | """ 33 | 34 | def __init__(self): 35 | self.host = os.environ['REMOTE_TARANTOOL_HOST'] 36 | 37 | self.args = {} 38 | self.args['primary'] = BINARY_PORT 39 | self.args['admin'] = os.environ['REMOTE_TARANTOOL_CONSOLE_PORT'] 40 | 41 | assert self.args['primary'] != self.args['admin'] 42 | 43 | # a name to using for a lock 44 | self.whoami = get_random_string() 45 | 46 | self.admin = TarantoolAdmin(self.host, self.args['admin']) 47 | self.lock_is_acquired = False 48 | 49 | # emulate stopped server 50 | self.acquire_lock() 51 | self.admin.execute('box.cfg{listen = box.NULL}') 52 | 53 | def acquire_lock(self): 54 | """ 55 | Acquire lock on the remote server so no concurrent tests would run. 56 | """ 57 | 58 | deadline = time.time() + AWAIT_TIME 59 | while True: 60 | res = self.admin.execute(f'return acquire_lock("{ self.whoami}")') 61 | ok = res[0] 62 | err = res[1] if not ok else None 63 | if ok: 64 | break 65 | if time.time() > deadline: 66 | raise RuntimeError(f'can not acquire "{self.whoami}" lock: {str(err)}') 67 | print(f'waiting to acquire "{self.whoami}" lock', 68 | file=sys.stderr) 69 | time.sleep(1) 70 | self.lock_is_acquired = True 71 | 72 | def touch_lock(self): 73 | """ 74 | Refresh locked state on the remote server so no concurrent 75 | tests would run. 76 | """ 77 | 78 | assert self.lock_is_acquired 79 | res = self.admin.execute(f'return touch_lock("{self.whoami}")') 80 | ok = res[0] 81 | err = res[1] if not ok else None 82 | if not ok: 83 | raise RuntimeError(f'can not update "{self.whoami}" lock: {str(err)}') 84 | 85 | def release_lock(self): 86 | """ 87 | Release loack so another test suite can run on the remote 88 | server. 89 | """ 90 | 91 | res = self.admin.execute(f'return release_lock("{self.whoami}")') 92 | ok = res[0] 93 | err = res[1] if not ok else None 94 | if not ok: 95 | raise RuntimeError(f'can not release "{self.whoami}" lock: {str(err)}') 96 | self.lock_is_acquired = False 97 | 98 | def start(self): 99 | """ 100 | Initialize the work with the remote server. 101 | """ 102 | 103 | if not self.lock_is_acquired: 104 | self.acquire_lock() 105 | self.admin.execute(f'box.cfg{{listen = "0.0.0.0:{self.args["primary"]}"}}') 106 | 107 | def stop(self): 108 | """ 109 | Finish the work with the remote server. 110 | """ 111 | 112 | self.admin.execute('box.cfg{listen = box.NULL}') 113 | self.release_lock() 114 | 115 | def is_started(self): 116 | """ 117 | Check if we still work with the remote server. 118 | """ 119 | 120 | return self.lock_is_acquired 121 | 122 | def clean(self): 123 | """ 124 | Do nothing. 125 | """ 126 | 127 | def __del__(self): 128 | self.admin.disconnect() 129 | -------------------------------------------------------------------------------- /test/suites/lib/skip.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides helpers to skip specific tests. 3 | """ 4 | 5 | import functools 6 | import sys 7 | 8 | import pkg_resources 9 | 10 | 11 | def fetch_tarantool_version(self): 12 | """ 13 | Helper to fetch current Tarantool version. 14 | """ 15 | if not hasattr(self, 'tnt_version') or self.tnt_version is None: 16 | srv = None 17 | 18 | if hasattr(self, 'servers') and self.servers is not None: 19 | srv = self.servers[0] 20 | 21 | if hasattr(self, 'srv') and self.srv is not None: 22 | srv = self.srv 23 | 24 | assert srv is not None 25 | 26 | try: 27 | self.tnt_version = srv.admin.tnt_version 28 | except AttributeError: 29 | self.__class__.tnt_version = srv.admin.tnt_version 30 | 31 | 32 | def skip_or_run_test_tarantool_impl(self, required_tt_version, msg): 33 | """ 34 | Helper to skip or run tests depending on the Tarantool 35 | version. 36 | 37 | Also, it can be used with the 'setUp' method for skipping 38 | the whole test suite. 39 | """ 40 | fetch_tarantool_version(self) 41 | 42 | support_version = pkg_resources.parse_version(required_tt_version) 43 | 44 | if self.tnt_version < support_version: 45 | self.skipTest(f'Tarantool {self.tnt_version} {msg}') 46 | 47 | 48 | def skip_or_run_test_tarantool(func, required_tt_version, msg): 49 | """ 50 | Decorator to skip or run tests depending on the tarantool 51 | version. 52 | 53 | Also, it can be used with the 'setUp' method for skipping 54 | the whole test suite. 55 | """ 56 | 57 | @functools.wraps(func) 58 | def wrapper(self, *args, **kwargs): 59 | if func.__name__ == 'setUp': 60 | func(self, *args, **kwargs) 61 | 62 | skip_or_run_test_tarantool_impl(self, required_tt_version, msg) 63 | 64 | if func.__name__ != 'setUp': 65 | func(self, *args, **kwargs) 66 | 67 | return wrapper 68 | 69 | 70 | def skip_or_run_test_tarantool_call(self, required_tt_version, msg): 71 | """ 72 | Function to skip or run tests depending on the tarantool 73 | version. Useful in cases when in is inconvenient to work 74 | with decorators. 75 | 76 | Also, it can be used with the 'setUp' method for skipping 77 | the whole test suite. 78 | """ 79 | 80 | skip_or_run_test_tarantool_impl(self, required_tt_version, msg) 81 | 82 | 83 | def skip_or_run_test_pcall_require(func, required_tt_module, msg): 84 | """ 85 | Decorator to skip or run tests depending on tarantool 86 | module require success or fail. 87 | 88 | Also, it can be used with the 'setUp' method for skipping 89 | the whole test suite. 90 | """ 91 | 92 | @functools.wraps(func) 93 | def wrapper(self, *args, **kwargs): 94 | if func.__name__ == 'setUp': 95 | func(self, *args, **kwargs) 96 | 97 | srv = None 98 | 99 | if hasattr(self, 'servers') and self.servers: 100 | srv = self.servers[0] 101 | 102 | if hasattr(self, 'srv') and self.srv: 103 | srv = self.srv 104 | 105 | assert srv is not None 106 | 107 | resp = srv.admin(f"pcall(require, '{required_tt_module}')") 108 | if not resp[0]: 109 | self.skipTest(f'Tarantool {msg}') 110 | 111 | if func.__name__ != 'setUp': 112 | func(self, *args, **kwargs) 113 | 114 | return wrapper 115 | 116 | 117 | def skip_or_run_test_python(func, required_python_version, msg): 118 | """ 119 | Decorator to skip or run tests depending on the Python version. 120 | 121 | Also, it can be used with the 'setUp' method for skipping 122 | the whole test suite. 123 | """ 124 | 125 | @functools.wraps(func) 126 | def wrapper(self, *args, **kwargs): 127 | if func.__name__ == 'setUp': 128 | func(self, *args, **kwargs) 129 | 130 | ver = sys.version_info 131 | python_version_str = f'{ver.major}.{ver.minor}' 132 | python_version = pkg_resources.parse_version(python_version_str) 133 | support_version = pkg_resources.parse_version(required_python_version) 134 | if python_version < support_version: 135 | self.skipTest(f'Python {python_version} connector {msg}') 136 | 137 | if func.__name__ != 'setUp': 138 | func(self, *args, **kwargs) 139 | 140 | return wrapper 141 | 142 | 143 | def skip_or_run_sql_test(func): 144 | """ 145 | Decorator to skip or run SQL-related tests depending on the 146 | tarantool version. 147 | 148 | Tarantool supports SQL-related stuff only since 2.0.0 version. 149 | So this decorator should wrap every SQL-related test to skip it if 150 | the tarantool version < 2.0.0 is used for testing. 151 | """ 152 | 153 | return skip_or_run_test_tarantool(func, '2.0.0', 'does not support SQL') 154 | 155 | 156 | def skip_or_run_varbinary_test(func): 157 | """ 158 | Decorator to skip or run VARBINARY-related tests depending on 159 | the tarantool version. 160 | 161 | Tarantool supports VARBINARY type only since 2.2.1 version. 162 | See https://github.com/tarantool/tarantool/issues/4201 163 | """ 164 | 165 | return skip_or_run_test_tarantool(func, '2.2.1', 166 | 'does not support VARBINARY type') 167 | 168 | 169 | def skip_or_run_decimal_test(func): 170 | """ 171 | Decorator to skip or run decimal-related tests depending on 172 | the tarantool version. 173 | 174 | Tarantool supports decimal type only since 2.2.1 version. 175 | See https://github.com/tarantool/tarantool/issues/692 176 | """ 177 | 178 | return skip_or_run_test_pcall_require(func, 'decimal', 179 | 'does not support decimal type') 180 | 181 | 182 | def skip_or_run_uuid_test(func): 183 | """ 184 | Decorator to skip or run UUID-related tests depending on 185 | the tarantool version. 186 | 187 | Tarantool supports UUID type only since 2.4.1 version. 188 | See https://github.com/tarantool/tarantool/issues/4268 189 | """ 190 | 191 | return skip_or_run_test_tarantool(func, '2.4.1', 192 | 'does not support UUID type') 193 | 194 | 195 | def skip_or_run_datetime_test(func): 196 | """ 197 | Decorator to skip or run datetime-related tests depending on 198 | the tarantool version. 199 | 200 | Tarantool supports datetime type only since 2.10.0 version. 201 | See https://github.com/tarantool/tarantool/issues/5941 202 | """ 203 | 204 | return skip_or_run_test_pcall_require(func, 'datetime', 205 | 'does not support datetime type') 206 | 207 | 208 | def skip_or_run_datetime_2_11_test(func): 209 | """ 210 | Decorator to skip or run tests related to datetime module with 211 | fixes introduced in 2.11 release. 212 | 213 | See https://github.com/tarantool/tarantool/issues/7698 and 214 | https://github.com/tarantool/tarantool/issues/7700 215 | """ 216 | 217 | return skip_or_run_test_tarantool(func, '2.11.0', 218 | 'does not provide required datetime fixes') 219 | 220 | 221 | def skip_or_run_error_extra_info_test(func): 222 | """ 223 | Decorator to skip or run tests related to extra error info 224 | provided over iproto depending on the tarantool version. 225 | 226 | Tarantool provides extra error info only since 2.4.1 version. 227 | See https://github.com/tarantool/tarantool/issues/4398 228 | """ 229 | 230 | return skip_or_run_test_tarantool(func, '2.4.1', 231 | 'does not provide extra error info') 232 | 233 | 234 | def skip_or_run_error_ext_type_test(func): 235 | """ 236 | Decorator to skip or run tests related to error extension 237 | type depending on the tarantool version. 238 | 239 | Tarantool supports error extension type only since 2.4.1 version, 240 | yet encoding was introduced only in 2.10.0. 241 | See https://github.com/tarantool/tarantool/issues/4398, 242 | https://github.com/tarantool/tarantool/issues/6433 243 | """ 244 | 245 | return skip_or_run_test_tarantool(func, '2.10.0', 246 | 'does not support error extension type') 247 | 248 | 249 | def skip_or_run_ssl_password_test_call(self): 250 | """ 251 | Function to skip or run tests related to SSL password 252 | and SSL password files support. Supported only in Tarantool EE. 253 | Do not check Enterprise prefix since TNT_SSL_TEST already assumes 254 | it. 255 | 256 | Tarantool EE supports SSL passwords and password files only in 257 | current master since commit e1f47dd4 (after 2.11.0-entrypoint). 258 | See https://github.com/tarantool/tarantool-ee/issues/22 259 | """ 260 | 261 | return skip_or_run_test_tarantool_call(self, '2.11.0', 262 | 'does not support SSL passwords') 263 | 264 | 265 | def skip_or_run_auth_type_test_call(self): 266 | """ 267 | Function to skip or run tests related to configuring 268 | authentication method. 269 | 270 | Tarantool supports auth_type only in current master since 271 | commit 2574ff1a (after 2.11.0-entrypoint). 272 | See https://github.com/tarantool/tarantool/issues/7988 273 | https://github.com/tarantool/tarantool/issues/7989 274 | https://github.com/tarantool/tarantool-ee/issues/295 275 | https://github.com/tarantool/tarantool-ee/issues/322 276 | """ 277 | 278 | return skip_or_run_test_tarantool_call(self, '2.11.0', 279 | 'does not support auth type') 280 | 281 | 282 | def skip_or_run_constraints_test(func): 283 | """ 284 | Decorator to skip or run tests related to spaces with 285 | schema constraints. 286 | 287 | Tarantool supports schema constraints only since 2.10.0 version. 288 | See https://github.com/tarantool/tarantool/issues/6436 289 | """ 290 | 291 | return skip_or_run_test_tarantool(func, '2.10.0', 292 | 'does not support schema constraints') 293 | 294 | 295 | def skip_or_run_iproto_basic_features_test(func): 296 | """ 297 | Decorator to skip or run tests related to iproto ID requests, 298 | protocol version and features. 299 | 300 | Tarantool supports iproto ID requests only since 2.10.0 version. 301 | Protocol version is 3 for Tarantool 2.10.0, 302 | IPROTO_FEATURE_STREAMS, IPROTO_FEATURE_TRANSACTIONS 303 | and IPROTO_FEATURE_ERROR_EXTENSION are supported in Tarantool 2.10.0. 304 | See https://github.com/tarantool/tarantool/issues/6253 305 | """ 306 | 307 | return skip_or_run_test_tarantool(func, '2.10.0', 308 | 'does not support iproto ID and iproto basic features') 309 | 310 | 311 | def skip_or_run_box_session_new_tests(func): 312 | """ 313 | Decorator to skip or run tests that use box.session.new. 314 | 315 | Tarantool supports box.session.new only in current master since 316 | commit 324872a. 317 | See https://github.com/tarantool/tarantool/issues/8801. 318 | """ 319 | return skip_or_run_test_tarantool(func, '3.0.0', 'does not support box.session.new') 320 | -------------------------------------------------------------------------------- /test/suites/lib/tarantool_admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides helpers to setup running Tarantool server. 3 | """ 4 | 5 | import socket 6 | import re 7 | 8 | import pkg_resources 9 | import yaml 10 | 11 | 12 | class TarantoolAdmin(): 13 | """ 14 | Class to setup running Tarantool server. 15 | """ 16 | 17 | def __init__(self, host, port): 18 | self.host = host 19 | self.port = port 20 | self.is_connected = False 21 | self.socket = None 22 | self._tnt_version = None 23 | 24 | def connect(self): 25 | """ 26 | Connect to running Tarantool server. 27 | """ 28 | 29 | self.socket = socket.create_connection((self.host, self.port)) 30 | self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) 31 | self.is_connected = True 32 | self.socket.recv(256) # skip greeting 33 | 34 | def disconnect(self): 35 | """ 36 | Disconnect from the Tarantool server. 37 | """ 38 | 39 | if self.is_connected: 40 | self.socket.close() 41 | self.socket = None 42 | self.is_connected = False 43 | 44 | def reconnect(self): 45 | """ 46 | Reconnect to the running Tarantool server. 47 | """ 48 | 49 | self.disconnect() 50 | self.connect() 51 | 52 | def __enter__(self): 53 | self.connect() 54 | return self 55 | 56 | def __exit__(self, exception_type, exception_value, exception_traceback): 57 | self.disconnect() 58 | 59 | def __call__(self, command): 60 | return self.execute(command) 61 | 62 | def execute(self, command): 63 | """ 64 | Evaluate some Lua code on the Tarantool server. 65 | """ 66 | 67 | if not command: 68 | return None 69 | 70 | if not self.is_connected: 71 | self.connect() 72 | 73 | cmd = (command.replace('\n', ' ') + '\n').encode() 74 | try: 75 | self.socket.sendall(cmd) 76 | except socket.error: 77 | # reconnect and try again 78 | self.reconnect() 79 | self.socket.sendall(cmd) 80 | 81 | bufsiz = 4096 82 | res = "" 83 | 84 | while True: 85 | buf = self.socket.recv(bufsiz) 86 | if not buf: 87 | break 88 | res = res + buf.decode() 89 | if (res.rfind("\n...\n") >= 0 or res.rfind("\r\n...\r\n") >= 0): 90 | break 91 | 92 | return yaml.safe_load(res) 93 | 94 | @property 95 | def tnt_version(self): 96 | """ 97 | Connected Tarantool server version. 98 | """ 99 | 100 | if self._tnt_version is not None: 101 | return self._tnt_version 102 | 103 | raw_version = re.match( 104 | r'[\d.]+', self.execute('box.info.version')[0] 105 | ).group() 106 | 107 | self._tnt_version = pkg_resources.parse_version(raw_version) 108 | 109 | return self._tnt_version 110 | -------------------------------------------------------------------------------- /test/suites/lib/tarantool_python_ci.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local console = require('console') 4 | local clock = require('clock') 5 | local log = require('log') 6 | 7 | local CONSOLE_PORT = 3302 8 | 9 | box.cfg({}) 10 | console.listen(CONSOLE_PORT) 11 | 12 | -- forward declarations 13 | local clean 14 | local init 15 | 16 | -- {{{ locking 17 | 18 | local LOCK_LIFETIME = 30 -- seconds 19 | 20 | local locked_by = nil 21 | local locked_at = 0 -- unix time, seconds 22 | 23 | local function clean_lock() 24 | locked_by = nil 25 | locked_at = 0 26 | clean() 27 | init() 28 | end 29 | 30 | local function set_lock(who) 31 | locked_by = who 32 | locked_at = clock.monotonic() 33 | end 34 | 35 | local function clean_dead_lock() 36 | if locked_by ~= nil and clock.monotonic() - locked_at > LOCK_LIFETIME then 37 | log.info(('removed dead "%s" lock'):format(tostring(locked_by))) 38 | clean_lock() 39 | end 40 | end 41 | 42 | local function is_locked_by(who) 43 | return locked_by == who 44 | end 45 | 46 | local function is_locked() 47 | return locked_by ~= nil 48 | end 49 | 50 | local function acquire_lock(who) 51 | assert(type(who) == 'string') 52 | clean_dead_lock() 53 | if is_locked_by(who) then 54 | -- update lock time 55 | set_lock(who) 56 | log.info(('updated "%s" lock'):format(who)) 57 | return true 58 | end 59 | if is_locked() then 60 | local err = 'locked by ' .. tostring(locked_by) 61 | log.info(('can not update "%s" lock: %s'):format(who, err)) 62 | return false, err 63 | end 64 | set_lock(who) 65 | log.info(('set "%s" lock'):format(who)) 66 | return true 67 | end 68 | 69 | local function touch_lock(who) 70 | assert(type(who) == 'string') 71 | clean_dead_lock() 72 | if is_locked_by(who) then 73 | -- update lock time 74 | set_lock(who) 75 | log.info(('updated "%s" lock'):format(who)) 76 | return true 77 | end 78 | if is_locked() then 79 | local err = 'locked by ' .. tostring(locked_by) 80 | log.info(('can not update "%s" lock: %s'):format(who, err)) 81 | return false, err 82 | end 83 | local err = 'is not locked' 84 | log.info(('can not update "%s" lock: %s'):format(who, err)) 85 | return false, err 86 | end 87 | 88 | local function release_lock(who) 89 | assert(type(who) == 'string') 90 | if is_locked_by(who) then 91 | clean_lock() 92 | log.info(('released "%s" lock'):format(who)) 93 | return true 94 | end 95 | clean_dead_lock() 96 | if is_locked() then 97 | local err = 'locked by ' .. tostring(locked_by) 98 | log.info(('can not release "%s" lock: %s'):format(who, err)) 99 | return false, err 100 | end 101 | local err = 'is not locked' 102 | log.info(('can not release "%s" lock: %s'):format(who, err)) 103 | return false, err 104 | end 105 | 106 | -- }}} 107 | 108 | -- {{{ init 109 | 110 | init = function() 111 | _G.acquire_lock = acquire_lock 112 | _G.touch_lock = touch_lock 113 | _G.release_lock = release_lock 114 | end 115 | 116 | -- }}} 117 | 118 | -- {{{ clean 119 | 120 | -- Copy of cleanup_cluster() from test_run.lua. 121 | local function cleanup_cluster() 122 | local cluster = box.space._cluster:select() 123 | for _, tuple in pairs(cluster) do 124 | if tuple[1] ~= box.info.id then 125 | box.space._cluster:delete(tuple[1]) 126 | end 127 | end 128 | end 129 | 130 | -- Copy of clean() from pretest_clean.lua from test-run. 131 | clean = function() 132 | local _SPACE_NAME = 3 133 | 134 | box.space._space:pairs():map(function(tuple) 135 | local name = tuple[_SPACE_NAME] 136 | return name 137 | end):filter(function(name) 138 | -- skip internal spaces 139 | local first_char = string.sub(name, 1, 1) 140 | return first_char ~= '_' 141 | end):each(function(name) 142 | box.space[name]:drop() 143 | end) 144 | 145 | local _USER_TYPE = 4 146 | local _USER_NAME = 3 147 | 148 | local allowed_users = { 149 | guest = true, 150 | admin = true, 151 | } 152 | box.space._user:pairs():filter(function(tuple) 153 | local tuple_type = tuple[_USER_TYPE] 154 | return tuple_type == 'user' 155 | end):map(function(tuple) 156 | local name = tuple[_USER_NAME] 157 | return name 158 | end):filter(function(name) 159 | return not allowed_users[name] 160 | end):each(function(name) 161 | box.schema.user.drop(name) 162 | end) 163 | 164 | local allowed_roles = { 165 | public = true, 166 | replication = true, 167 | super = true, 168 | } 169 | box.space._user:pairs():filter(function(tuple) 170 | local tuple_type = tuple[_USER_TYPE] 171 | return tuple_type == 'role' 172 | end):map(function(tuple) 173 | local name = tuple[_USER_NAME] 174 | return name 175 | end):filter(function(name) 176 | return not allowed_roles[name] 177 | end):each(function(name) 178 | box.schema.role.drop(name) 179 | end) 180 | 181 | local _FUNC_NAME = 3 182 | local _FUNC_LANGUAGE = 5 183 | local allowed_funcs = { 184 | ['box.schema.user.info'] = true, 185 | } 186 | local allowed_langs = { 187 | ['SQL_BUILTIN'] = true, 188 | } 189 | box.space._func:pairs():map(function(tuple) 190 | local name = tuple[_FUNC_NAME] 191 | local lang = tuple[_FUNC_LANGUAGE] 192 | return { name = name, lang = lang } 193 | end):filter(function(prop) 194 | return not allowed_funcs[prop.name] 195 | end):filter(function(prop) 196 | return not allowed_langs[prop.lang] 197 | end):each(function(prop) 198 | box.schema.func.drop(prop.name) 199 | end) 200 | 201 | local sql_builtin_func_count = box.space._func:pairs():map(function(tuple) 202 | local lang = tuple[_FUNC_LANGUAGE] 203 | if lang == 'SQL_BUILTIN' then 204 | return 1 205 | end 206 | return 0 207 | end):sum() 208 | 209 | cleanup_cluster() 210 | 211 | local cleanup_list = function(list, allowed) 212 | for k, _ in pairs(list) do 213 | if not allowed[k] then 214 | list[k] = nil 215 | end 216 | end 217 | end 218 | 219 | local allowed_globals = { 220 | -- modules 221 | bit = true, 222 | coroutine = true, 223 | debug = true, 224 | io = true, 225 | jit = true, 226 | math = true, 227 | os = true, 228 | package = true, 229 | string = true, 230 | table = true, 231 | utf8 = true, 232 | -- variables 233 | _G = true, 234 | _VERSION = true, 235 | arg = true, 236 | -- functions 237 | assert = true, 238 | collectgarbage = true, 239 | dofile = true, 240 | error = true, 241 | gcinfo = true, 242 | getfenv = true, 243 | getmetatable = true, 244 | ipairs = true, 245 | load = true, 246 | loadfile = true, 247 | loadstring = true, 248 | module = true, 249 | next = true, 250 | pairs = true, 251 | pcall = true, 252 | print = true, 253 | rawequal = true, 254 | rawget = true, 255 | rawset = true, 256 | require = true, 257 | select = true, 258 | setfenv = true, 259 | setmetatable = true, 260 | tonumber = true, 261 | tonumber64 = true, 262 | tostring = true, 263 | type = true, 264 | unpack = true, 265 | xpcall = true, 266 | -- tarantool 267 | _TARANTOOL = true, 268 | box = true, 269 | dostring = true, 270 | help = true, 271 | newproxy = true, 272 | role_check_grant_revoke_of_sys_priv = true, 273 | tutorial = true, 274 | update_format = true, 275 | } 276 | cleanup_list(_G, allowed_globals) 277 | 278 | local allowed_packages = { 279 | ['_G'] = true, 280 | bit = true, 281 | box = true, 282 | ['box.backup'] = true, 283 | ['box.internal'] = true, 284 | ['box.internal.sequence'] = true, 285 | ['box.internal.session'] = true, 286 | ['box.internal.space'] = true, 287 | buffer = true, 288 | clock = true, 289 | console = true, 290 | coroutine = true, 291 | crypto = true, 292 | csv = true, 293 | debug = true, 294 | digest = true, 295 | errno = true, 296 | ffi = true, 297 | fiber = true, 298 | fio = true, 299 | fun = true, 300 | help = true, 301 | ['help.en_US'] = true, 302 | ['http.client'] = true, 303 | iconv = true, 304 | ['internal.argparse'] = true, 305 | ['internal.trigger'] = true, 306 | io = true, 307 | jit = true, 308 | ['jit.bc'] = true, 309 | ['jit.bcsave'] = true, 310 | ['jit.dis_x64'] = true, 311 | ['jit.dis_x86'] = true, 312 | ['jit.dump'] = true, 313 | ['jit.opt'] = true, 314 | ['jit.p'] = true, 315 | ['jit.profile'] = true, 316 | ['jit.util'] = true, 317 | ['jit.v'] = true, 318 | ['jit.vmdef'] = true, 319 | ['jit.zone'] = true, 320 | json = true, 321 | log = true, 322 | math = true, 323 | msgpack = true, 324 | msgpackffi = true, 325 | ['net.box'] = true, 326 | ['net.box.lib'] = true, 327 | os = true, 328 | package = true, 329 | pickle = true, 330 | pwd = true, 331 | socket = true, 332 | strict = true, 333 | string = true, 334 | table = true, 335 | ['table.clear'] = true, 336 | ['table.new'] = true, 337 | tap = true, 338 | tarantool = true, 339 | title = true, 340 | uri = true, 341 | utf8 = true, 342 | uuid = true, 343 | xlog = true, 344 | yaml = true, 345 | } 346 | cleanup_list(package.loaded, allowed_packages) 347 | 348 | local user_count = box.space._user:count() 349 | assert(user_count == 4 or user_count == 5, 350 | 'box.space._user:count() should be 4 (1.10) or 5 (2.0)') 351 | assert(box.space._func:count() == 1 + sql_builtin_func_count, 352 | 'box.space._func:count() should be 1 (1.10 and >= 2.10)' .. 353 | ' or 1 + count of SQL_BUILTIN functions (>= 2.2.1, < 2.10)') 354 | assert(box.space._cluster:count() == 1, 355 | 'box.space._cluster:count() should be only one') 356 | 357 | box.cfg({listen = box.NULL}) 358 | end 359 | 360 | -- }}} 361 | 362 | clean() 363 | init() 364 | -------------------------------------------------------------------------------- /test/suites/sidecar.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-module-docstring 2 | import os 3 | 4 | import tarantool 5 | 6 | socket_fd = int(os.environ["SOCKET_FD"]) 7 | 8 | conn = tarantool.connect(None, None, socket_fd=socket_fd) 9 | 10 | # Check user. 11 | assert conn.eval("return box.session.user()").data[0] == "test" 12 | 13 | # Check db operations. 14 | conn.insert("test", [1]) 15 | conn.insert("test", [2]) 16 | assert conn.select("test").data == [[1], [2]] 17 | -------------------------------------------------------------------------------- /test/suites/test_connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests basic connection behavior. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | import decimal 9 | 10 | import pkg_resources 11 | import msgpack 12 | 13 | import tarantool 14 | import tarantool.msgpack_ext.decimal as ext_decimal 15 | 16 | from .lib.skip import skip_or_run_decimal_test, skip_or_run_varbinary_test 17 | from .lib.tarantool_server import TarantoolServer 18 | from .utils import assert_admin_success 19 | 20 | 21 | class TestSuiteConnection(unittest.TestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | print(' CONNECTION '.center(70, '='), file=sys.stderr) 25 | print('-' * 70, file=sys.stderr) 26 | cls.srv = TarantoolServer() 27 | cls.srv.script = 'test/suites/box.lua' 28 | cls.srv.start() 29 | 30 | cls.adm = cls.srv.admin 31 | resp = cls.adm(""" 32 | box.schema.user.create('test', {password = 'test', if_not_exists = true}) 33 | box.schema.user.grant('test', 'read,write,execute', 'universe', 34 | nil, {if_not_exists = true}) 35 | 36 | return true 37 | """) 38 | assert_admin_success(resp) 39 | 40 | if cls.srv.admin.tnt_version >= pkg_resources.parse_version('2.2.1'): 41 | resp = cls.adm(""" 42 | box.schema.create_space('space_varbin', {if_not_exists = true}) 43 | 44 | box.space['space_varbin']:format({ 45 | { 46 | 'id', 47 | type = 'number', 48 | is_nullable = false 49 | }, 50 | { 51 | 'varbin', 52 | type = 'varbinary', 53 | is_nullable = false, 54 | } 55 | }) 56 | 57 | box.space['space_varbin']:create_index('id', { 58 | type = 'tree', 59 | parts = {1, 'number'}, 60 | unique = true, 61 | if_not_exists = true}) 62 | 63 | box.space['space_varbin']:create_index('varbin', { 64 | type = 'tree', 65 | parts = {2, 'varbinary'}, 66 | unique = true, 67 | if_not_exists = true}) 68 | 69 | return true 70 | """) 71 | assert_admin_success(resp) 72 | 73 | cls.con = None 74 | 75 | def setUp(self): 76 | # prevent a remote tarantool from clean our session 77 | if self.srv.is_started(): 78 | self.srv.touch_lock() 79 | 80 | @skip_or_run_decimal_test 81 | def test_custom_packer(self): 82 | def my_ext_type_encoder(obj): 83 | if isinstance(obj, decimal.Decimal): 84 | obj = obj + 1 85 | return msgpack.ExtType(ext_decimal.EXT_ID, ext_decimal.encode(obj, None)) 86 | raise TypeError(f"Unknown type: {repr(obj)}") 87 | 88 | def my_packer_factory(_): 89 | return msgpack.Packer(default=my_ext_type_encoder) 90 | 91 | self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 92 | user='test', password='test', 93 | packer_factory=my_packer_factory) 94 | 95 | resp = self.con.eval("return ...", (decimal.Decimal('27756'),)) 96 | self.assertSequenceEqual(resp, [decimal.Decimal('27757')]) 97 | 98 | def test_custom_packer_supersedes_encoding(self): 99 | def my_packer_factory(_): 100 | return msgpack.Packer(use_bin_type=False) 101 | 102 | self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 103 | user='test', password='test', 104 | encoding='utf-8', 105 | packer_factory=my_packer_factory) 106 | 107 | # bytes -> mp_str (string) for encoding=None 108 | # bytes -> mp_bin (varbinary) for encoding='utf-8' 109 | resp = self.con.eval("return type(...)", (bytes(bytearray.fromhex('DEADBEAF0103')),)) 110 | self.assertSequenceEqual(resp, ['string']) 111 | 112 | @skip_or_run_decimal_test 113 | def test_custom_unpacker(self): 114 | def my_ext_type_decoder(code, data): 115 | if code == ext_decimal.EXT_ID: 116 | return ext_decimal.decode(data, None) - 1 117 | raise NotImplementedError(f"Unknown msgpack extension type code {code}") 118 | 119 | def my_unpacker_factory(_): 120 | if msgpack.version >= (1, 0, 0): 121 | return msgpack.Unpacker(ext_hook=my_ext_type_decoder, strict_map_key=False) 122 | return msgpack.Unpacker(ext_hook=my_ext_type_decoder) 123 | 124 | self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 125 | user='test', password='test', 126 | unpacker_factory=my_unpacker_factory) 127 | 128 | resp = self.con.eval("return require('decimal').new('27756')") 129 | self.assertSequenceEqual(resp, [decimal.Decimal('27755')]) 130 | 131 | @skip_or_run_varbinary_test 132 | def test_custom_unpacker_supersedes_encoding(self): 133 | def my_unpacker_factory(_): 134 | if msgpack.version >= (0, 5, 2): 135 | if msgpack.version >= (1, 0, 0): 136 | return msgpack.Unpacker(raw=True, strict_map_key=False) 137 | 138 | return msgpack.Unpacker(raw=True) 139 | return msgpack.Unpacker(encoding=None) 140 | 141 | self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 142 | user='test', password='test', 143 | encoding='utf-8', 144 | unpacker_factory=my_unpacker_factory) 145 | 146 | data_id = 1 147 | data_hex = 'DEADBEAF' 148 | data = bytes(bytearray.fromhex(data_hex)) 149 | space = 'space_varbin' 150 | 151 | self.con.execute(f""" 152 | INSERT INTO "{space}" VALUES ({data_id}, x'{data_hex}'); 153 | """) 154 | 155 | resp = self.con.execute(f""" 156 | SELECT * FROM "{space}" WHERE "varbin" == x'{data_hex}'; 157 | """) 158 | self.assertSequenceEqual(resp, [[data_id, data]]) 159 | 160 | def test_custom_unpacker_supersedes_use_list(self): 161 | def my_unpacker_factory(_): 162 | if msgpack.version >= (1, 0, 0): 163 | return msgpack.Unpacker(use_list=False, strict_map_key=False) 164 | return msgpack.Unpacker(use_list=False) 165 | 166 | self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 167 | user='test', password='test', 168 | use_list=True, 169 | unpacker_factory=my_unpacker_factory) 170 | 171 | resp = self.con.eval("return {1, 2, 3}") 172 | self.assertIsInstance(resp[0], tuple) 173 | 174 | def tearDown(self): 175 | if self.con: 176 | self.con.close() 177 | 178 | @classmethod 179 | def tearDownClass(cls): 180 | cls.srv.stop() 181 | cls.srv.clean() 182 | -------------------------------------------------------------------------------- /test/suites/test_dbapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests compatibility with DBAPI standards. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,protected-access,fixme 5 | 6 | import sys 7 | import unittest 8 | 9 | import dbapi20 10 | 11 | import tarantool 12 | from tarantool import dbapi 13 | from .lib.tarantool_server import TarantoolServer 14 | from .lib.skip import skip_or_run_sql_test 15 | from .utils import assert_admin_success 16 | 17 | 18 | class TestSuiteDBAPI(dbapi20.DatabaseAPI20Test): 19 | table_prefix = 'dbapi20test_' # If you need to specify a prefix for tables 20 | 21 | ddl0 = f'create table {table_prefix} (id INTEGER PRIMARY KEY AUTOINCREMENT, ' \ 22 | 'name varchar(20))' 23 | ddl1 = f'create table {table_prefix}booze (name varchar(20) primary key)' 24 | ddl2 = f'create table {table_prefix}barflys (name varchar(20) primary key, ' \ 25 | 'drink varchar(30))' 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | print(' DBAPI '.center(70, '='), file=sys.stderr) 30 | print('-' * 70, file=sys.stderr) 31 | # Select scans are not allowed with compat.sql_seq_scan_default = "new", 32 | # but tests create cursors with fullscan. 33 | cls.srv = TarantoolServer(sql_seq_scan_default="old") 34 | cls.srv.script = 'test/suites/box.lua' 35 | cls.srv.start() 36 | cls.con = tarantool.Connection(cls.srv.host, cls.srv.args['primary']) 37 | cls.driver = dbapi 38 | cls.connect_kw_args = { 39 | "host": cls.srv.host, 40 | "port": cls.srv.args['primary'] 41 | } 42 | 43 | # grant full access to guest 44 | resp = cls.srv.admin(""" 45 | box.schema.user.grant('guest', 'create,read,write,execute', 'universe', 46 | nil, {if_not_exists = true}) 47 | return true 48 | """) 49 | assert_admin_success(resp) 50 | 51 | @skip_or_run_sql_test 52 | def setUp(self): 53 | # prevent a remote tarantool from clean our session 54 | if self.srv.is_started(): 55 | self.srv.touch_lock() 56 | self.con.flush_schema() 57 | 58 | @classmethod 59 | def tearDownClass(cls): 60 | cls.con.close() 61 | cls.srv.stop() 62 | cls.srv.clean() 63 | 64 | def help_nextset_setUp(self, cur): 65 | # pylint: disable=unused-argument 66 | pass 67 | 68 | def help_nextset_tearDown(self, cur): 69 | # pylint: disable=unused-argument 70 | pass 71 | 72 | def test_rowcount(self): 73 | con = self._connect() 74 | try: 75 | cur = con.cursor() 76 | self.executeDDL1(cur) 77 | dbapi20._failUnless( 78 | self, cur.rowcount in (-1, 1), 79 | 'cursor.rowcount should be -1 or 1 after executing no-result ' 80 | 'statements' + str(cur.rowcount) 81 | ) 82 | cur.execute(f"{self.insert} into {self.table_prefix}booze values ('Victoria Bitter')") 83 | dbapi20._failUnless( 84 | self, cur.rowcount == 1, 85 | 'cursor.rowcount should == number or rows inserted, or ' 86 | 'set to -1 after executing an insert statement' 87 | ) 88 | cur.execute(f"select name from {self.table_prefix}booze") 89 | dbapi20._failUnless( 90 | self, cur.rowcount == -1, 91 | 'cursor.rowcount should == number of rows returned, or ' 92 | 'set to -1 after executing a select statement' 93 | ) 94 | self.executeDDL2(cur) 95 | dbapi20._failUnless( 96 | self, cur.rowcount in (-1, 1), 97 | 'cursor.rowcount should be -1 or 1 after executing no-result ' 98 | 'statements' 99 | ) 100 | finally: 101 | con.close() 102 | 103 | @unittest.skip('Not implemented') 104 | def test_Binary(self): 105 | pass 106 | 107 | @unittest.skip('Not implemented') 108 | def test_STRING(self): 109 | pass 110 | 111 | @unittest.skip('Not implemented') 112 | def test_BINARY(self): 113 | pass 114 | 115 | @unittest.skip('Not implemented') 116 | def test_NUMBER(self): 117 | pass 118 | 119 | @unittest.skip('Not implemented') 120 | def test_DATETIME(self): 121 | pass 122 | 123 | @unittest.skip('Not implemented') 124 | def test_ROWID(self): 125 | pass 126 | 127 | @unittest.skip('Not implemented') 128 | def test_Date(self): 129 | pass 130 | 131 | @unittest.skip('Not implemented') 132 | def test_Time(self): 133 | pass 134 | 135 | @unittest.skip('Not implemented') 136 | def test_Timestamp(self): 137 | pass 138 | 139 | @unittest.skip('Not implemented as optional.') 140 | def test_nextset(self): 141 | pass 142 | 143 | @unittest.skip('Not implemented') 144 | def test_callproc(self): 145 | pass 146 | 147 | def test_setoutputsize(self): # Do nothing 148 | pass 149 | 150 | @unittest.skip('Not implemented') 151 | def test_description(self): 152 | pass 153 | -------------------------------------------------------------------------------- /test/suites/test_encoding.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests various type encoding cases. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | import pkg_resources 9 | 10 | import tarantool 11 | from tarantool.error import DatabaseError 12 | 13 | from .lib.skip import skip_or_run_varbinary_test, skip_or_run_error_extra_info_test 14 | from .lib.tarantool_server import TarantoolServer 15 | from .utils import assert_admin_success 16 | 17 | 18 | class TestSuiteEncoding(unittest.TestCase): 19 | # pylint: disable=invalid-name 20 | 21 | @classmethod 22 | def setUpClass(cls): 23 | print(' ENCODING '.center(70, '='), file=sys.stderr) 24 | print('-' * 70, file=sys.stderr) 25 | cls.srv = TarantoolServer() 26 | cls.srv.script = 'test/suites/box.lua' 27 | cls.srv.start() 28 | 29 | resp = cls.srv.admin(""" 30 | box.schema.user.create('test', { password = 'test', if_not_exists = true }) 31 | box.schema.user.grant('test', 'execute,read,write', 'universe', 32 | nil, {if_not_exists = true}) 33 | 34 | return true 35 | """) 36 | assert_admin_success(resp) 37 | 38 | args = [cls.srv.host, cls.srv.args['primary']] 39 | kwargs = {'user': 'test', 'password': 'test'} 40 | cls.con_encoding_utf8 = tarantool.Connection(*args, encoding='utf-8', **kwargs) 41 | cls.con_encoding_none = tarantool.Connection(*args, encoding=None, **kwargs) 42 | cls.conns = [cls.con_encoding_utf8, cls.con_encoding_none] 43 | 44 | resp = cls.srv.admin(""" 45 | box.schema.create_space('space_str', {if_not_exists = true}) 46 | box.space['space_str']:create_index('primary', { 47 | type = 'tree', 48 | parts = {1, 'str'}, 49 | unique = true, 50 | if_not_exists = true}) 51 | 52 | return true 53 | """) 54 | assert_admin_success(resp) 55 | 56 | if cls.srv.admin.tnt_version >= pkg_resources.parse_version('2.2.1'): 57 | resp = cls.srv.admin(""" 58 | box.schema.create_space('space_varbin', {if_not_exists = true}) 59 | box.space['space_varbin']:format({ 60 | { 61 | 'id', 62 | type = 'number', 63 | is_nullable = false 64 | }, 65 | { 66 | 'varbin', 67 | type = 'varbinary', 68 | is_nullable = false, 69 | } 70 | }) 71 | box.space['space_varbin']:create_index('id', { 72 | type = 'tree', 73 | parts = {1, 'number'}, 74 | unique = true, 75 | if_not_exists = true}) 76 | box.space['space_varbin']:create_index('varbin', { 77 | type = 'tree', 78 | parts = {2, 'varbinary'}, 79 | unique = true, 80 | if_not_exists = true}) 81 | 82 | return true 83 | """) 84 | assert_admin_success(resp) 85 | 86 | def assertNotRaises(self, func, *args, **kwargs): 87 | try: 88 | func(*args, **kwargs) 89 | except Exception as exc: # pylint: disable=bad-option-value,broad-exception-caught,broad-except 90 | self.fail(f'Function raised Exception: {repr(exc)}') 91 | 92 | def setUp(self): 93 | # prevent a remote tarantool from clean our session 94 | if self.srv.is_started(): 95 | self.srv.touch_lock() 96 | 97 | # encoding = 'utf-8' 98 | # 99 | # Python 3 -> Tarantool -> Python 3 100 | # str -> mp_str (string) -> str 101 | # bytes -> mp_bin (varbinary) -> bytes 102 | def test_01_01_str_encode_for_encoding_utf8_behavior(self): 103 | data = 'test_01_01' 104 | space = 'space_str' 105 | 106 | self.assertNotRaises(self.con_encoding_utf8.insert, space, [data]) 107 | 108 | resp = self.con_encoding_utf8.select(space, [data]) 109 | self.assertSequenceEqual(resp, [[data]]) 110 | 111 | def test_01_02_string_decode_for_encoding_utf8_behavior(self): 112 | data = 'test_01_02' 113 | space = 'space_str' 114 | 115 | self.srv.admin(f"box.space['{space}']:insert{{'{data}'}}") 116 | 117 | resp = self.con_encoding_utf8.eval(f"return box.space['{space}']:get('{data}')") 118 | self.assertSequenceEqual(resp, [[data]]) 119 | 120 | @skip_or_run_varbinary_test 121 | def test_01_03_bytes_encode_for_encoding_utf8_behavior(self): 122 | data_id = 103 123 | data = bytes(bytearray.fromhex('DEADBEAF0103')) 124 | space = 'space_varbin' 125 | 126 | self.assertNotRaises(self.con_encoding_utf8.insert, space, [data_id, data]) 127 | 128 | resp = self.con_encoding_utf8.select(space, [data], index='varbin') 129 | self.assertSequenceEqual(resp, [[data_id, data]]) 130 | 131 | @skip_or_run_varbinary_test 132 | def test_01_04_varbinary_decode_for_encoding_utf8_behavior(self): 133 | data_id = 104 134 | data_hex = 'DEADBEAF0104' 135 | data = bytes(bytearray.fromhex(data_hex)) 136 | space = 'space_varbin' 137 | 138 | self.con_encoding_utf8.execute(f""" 139 | INSERT INTO "{space}" VALUES ({data_id}, x'{data_hex}'); 140 | """) 141 | 142 | resp = self.con_encoding_utf8.execute(f""" 143 | SELECT * FROM "{space}" WHERE "varbin" == x'{data_hex}'; 144 | """) 145 | self.assertSequenceEqual(resp, [[data_id, data]]) 146 | 147 | # encoding = None 148 | # 149 | # Python 3 -> Tarantool -> Python 3 150 | # bytes -> mp_str (string) -> bytes 151 | # str -> mp_str (string) -> bytes 152 | # mp_bin (varbinary) -> bytes 153 | def test_02_01_str_encode_for_encoding_none_behavior(self): 154 | data = 'test_02_01' 155 | space = 'space_str' 156 | 157 | self.assertNotRaises(self.con_encoding_none.insert, space, [data]) 158 | 159 | resp = self.con_encoding_utf8.select(space, [data]) 160 | self.assertSequenceEqual(resp, [[data]]) 161 | 162 | def test_02_02_string_decode_for_encoding_none_behavior(self): 163 | data = 'test_02_02' 164 | data_decoded = b'test_02_02' 165 | space = 'space_str' 166 | 167 | self.srv.admin(f"box.space['{space}']:insert{{'{data}'}}") 168 | 169 | resp = self.con_encoding_none.eval(f"return box.space['{space}']:get('{data}')") 170 | self.assertSequenceEqual(resp, [[data_decoded]]) 171 | 172 | def test_02_03_bytes_encode_for_encoding_none_behavior(self): 173 | data = b'test_02_03' 174 | space = 'space_str' 175 | 176 | self.assertNotRaises(self.con_encoding_none.insert, space, [data]) 177 | 178 | resp = self.con_encoding_none.select(space, [data]) 179 | self.assertSequenceEqual(resp, [[data]]) 180 | 181 | @skip_or_run_varbinary_test 182 | def test_02_04_varbinary_decode_for_encoding_none_behavior(self): 183 | data_id = 204 184 | data_hex = 'DEADBEAF0204' 185 | data = bytes(bytearray.fromhex(data_hex)) 186 | space = 'space_varbin' 187 | 188 | self.con_encoding_none.execute(f""" 189 | INSERT INTO "{space}" VALUES ({data_id}, x'{data_hex}'); 190 | """) 191 | 192 | resp = self.con_encoding_none.execute(f""" 193 | SELECT * FROM "{space}" WHERE "varbin" == x'{data_hex}'; 194 | """) 195 | self.assertSequenceEqual(resp, [[data_id, data]]) 196 | 197 | @skip_or_run_error_extra_info_test 198 | def test_01_05_error_extra_info_decode_for_encoding_utf8_behavior(self): 199 | try: 200 | self.con_encoding_utf8.eval("not a Lua code") 201 | except DatabaseError as exc: 202 | self.assertEqual(exc.extra_info.type, 'LuajitError') 203 | self.assertEqual(exc.extra_info.message, "eval:1: unexpected symbol near 'not'") 204 | else: 205 | self.fail('Expected error') 206 | 207 | @skip_or_run_error_extra_info_test 208 | def test_02_05_error_extra_info_decode_for_encoding_none_behavior(self): 209 | try: 210 | self.con_encoding_none.eval("not a Lua code") 211 | except DatabaseError as exc: 212 | self.assertEqual(exc.extra_info.type, b'LuajitError') 213 | self.assertEqual(exc.extra_info.message, b"eval:1: unexpected symbol near 'not'") 214 | else: 215 | self.fail('Expected error') 216 | 217 | @classmethod 218 | def tearDownClass(cls): 219 | for con in cls.conns: 220 | con.close() 221 | cls.srv.stop() 222 | cls.srv.clean() 223 | -------------------------------------------------------------------------------- /test/suites/test_execute.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests API for running SQL on a server. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | 9 | import tarantool 10 | from .lib.tarantool_server import TarantoolServer 11 | from .lib.skip import skip_or_run_sql_test 12 | from .utils import assert_admin_success 13 | 14 | 15 | class TestSuiteExecute(unittest.TestCase): 16 | ddl = 'create table %s (id INTEGER PRIMARY KEY AUTOINCREMENT, ' \ 17 | 'name varchar(20))' 18 | 19 | dml_params = [ 20 | {'id': None, 'name': 'Michael'}, 21 | {'id': None, 'name': 'Mary'}, 22 | {'id': None, 'name': 'John'}, 23 | {'id': None, 'name': 'Ruth'}, 24 | {'id': None, 'name': 'Rachel'} 25 | ] 26 | 27 | @classmethod 28 | def setUpClass(cls): 29 | print(' EXECUTE '.center(70, '='), file=sys.stderr) 30 | print('-' * 70, file=sys.stderr) 31 | cls.srv = TarantoolServer() 32 | cls.srv.script = 'test/suites/box.lua' 33 | cls.srv.start() 34 | cls.con = tarantool.Connection(cls.srv.host, cls.srv.args['primary']) 35 | 36 | # grant full access to guest 37 | resp = cls.srv.admin(""" 38 | box.schema.user.grant('guest', 'create,read,write,execute', 'universe', 39 | nil, {if_not_exists = true}) 40 | return true 41 | """) 42 | assert_admin_success(resp) 43 | 44 | @skip_or_run_sql_test 45 | def setUp(self): 46 | # prevent a remote tarantool from clean our session 47 | if self.srv.is_started(): 48 | self.srv.touch_lock() 49 | self.con.flush_schema() 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | cls.con.close() 54 | cls.srv.stop() 55 | cls.srv.clean() 56 | 57 | def _populate_data(self, table_name): 58 | query = f"insert into {table_name} values (:id, :name)" 59 | for param in self.dml_params: 60 | self.con.execute(query, param) 61 | 62 | def _create_table(self, table_name): 63 | return self.con.execute(self.ddl % table_name) 64 | 65 | def test_dml_response(self): 66 | table_name = 'foo' 67 | response = self._create_table(table_name) 68 | self.assertEqual(response.autoincrement_ids, None) 69 | self.assertEqual(response.affected_row_count, 1) 70 | self.assertEqual(response.data, None) 71 | 72 | query = f"insert into {table_name} values (:id, :name)" 73 | 74 | for num, param in enumerate(self.dml_params, start=1): 75 | response = self.con.execute(query, param) 76 | self.assertEqual(response.autoincrement_ids[0], num) 77 | self.assertEqual(response.affected_row_count, 1) 78 | self.assertEqual(response.data, None) 79 | 80 | query = f"delete from {table_name} where id in (4, 5)" 81 | response = self.con.execute(query) 82 | self.assertEqual(response.autoincrement_ids, None) 83 | self.assertEqual(response.affected_row_count, 2) 84 | self.assertEqual(response.data, None) 85 | 86 | def test_dql_response(self): 87 | table_name = 'bar' 88 | self._create_table(table_name) 89 | self._populate_data(table_name) 90 | 91 | select_query = f"select name from {table_name} where id in (1, 3, 5)" 92 | response = self.con.execute(select_query) 93 | self.assertEqual(response.autoincrement_ids, None) 94 | self.assertEqual(response.affected_row_count, None) 95 | expected_data = [['Michael'], ['John'], ['Rachel']] 96 | self.assertListEqual(response.data, expected_data) 97 | -------------------------------------------------------------------------------- /test/suites/test_package.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests package features. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring 5 | 6 | import os 7 | import sys 8 | import unittest 9 | 10 | import tarantool 11 | 12 | if sys.version_info >= (3, 8): 13 | from importlib import metadata 14 | else: 15 | import importlib_metadata as metadata 16 | 17 | 18 | def is_test_pure_install(): 19 | env = os.getenv("TEST_PURE_INSTALL") 20 | if env: 21 | env = env.upper() 22 | return env in ("1", "TRUE") 23 | return False 24 | 25 | 26 | class TestSuitePackage(unittest.TestCase): 27 | @classmethod 28 | def setUpClass(cls): 29 | print(' PACKAGE '.center(70, '='), file=sys.stderr) 30 | print('-' * 70, file=sys.stderr) 31 | 32 | def test_version(self): 33 | if is_test_pure_install(): 34 | self.assertEqual(tarantool.__version__, metadata.version('tarantool')) 35 | else: 36 | self.assertEqual( 37 | tarantool.__version__, '0.0.0-dev', 38 | 'Ensure that there is no tarantool/version.py file in your dev build') 39 | -------------------------------------------------------------------------------- /test/suites/test_protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests connection authentication. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,protected-access,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | import uuid 9 | 10 | import pkg_resources 11 | 12 | import tarantool 13 | from tarantool.const import ( 14 | IPROTO_FEATURE_STREAMS, 15 | IPROTO_FEATURE_TRANSACTIONS, 16 | IPROTO_FEATURE_ERROR_EXTENSION, 17 | IPROTO_FEATURE_WATCHERS, 18 | IPROTO_FEATURE_PAGINATION, 19 | IPROTO_FEATURE_SPACE_AND_INDEX_NAMES, 20 | IPROTO_FEATURE_WATCH_ONCE, 21 | ) 22 | from tarantool.error import NetworkError 23 | from tarantool.utils import greeting_decode, version_id 24 | 25 | from .lib.tarantool_server import TarantoolServer 26 | from .lib.skip import skip_or_run_iproto_basic_features_test 27 | 28 | 29 | class TestSuiteProtocol(unittest.TestCase): 30 | @classmethod 31 | def setUpClass(cls): 32 | print(' PROTOCOL '.center(70, '='), file=sys.stderr) 33 | print('-' * 70, file=sys.stderr) 34 | cls.srv = TarantoolServer() 35 | cls.srv.script = 'test/suites/box.lua' 36 | cls.srv.start() 37 | cls.con = tarantool.Connection(cls.srv.host, cls.srv.args['primary']) 38 | cls.adm = cls.srv.admin 39 | 40 | def setUp(self): 41 | # prevent a remote tarantool from clean our session 42 | if self.srv.is_started(): 43 | self.srv.touch_lock() 44 | 45 | def test_00_greeting_1_6(self): 46 | buf = "Tarantool 1.6.6 \n" + \ 47 | "AtQnb9SAIaKazZZy9lJKvK3urtbjCEJndhRVbslSPGc= \n" 48 | greeting = greeting_decode(buf.encode()) 49 | self.assertEqual(greeting.version_id, version_id(1, 6, 6)) 50 | self.assertEqual(greeting.protocol, "Binary") 51 | self.assertIsNone(greeting.uuid) 52 | self.assertIsNotNone(greeting.salt) 53 | 54 | def test_01_greeting_1_6_with_tag(self): 55 | buf = "Tarantool 1.6.6-232-gcf47324 \n" + \ 56 | "AtQnb9SAIaKazZZy9lJKvK3urtbjCEJndhRVbslSPGc= \n" 57 | greeting = greeting_decode(buf.encode()) 58 | self.assertEqual(greeting.version_id, version_id(1, 6, 6)) 59 | self.assertEqual(greeting.protocol, "Binary") 60 | self.assertIsNone(greeting.uuid) 61 | self.assertIsNotNone(greeting.salt) 62 | 63 | def test_02_greeting_1_6_console(self): 64 | buf = "Tarantool 1.6.6-132-g82f5424 (Lua console) \n" + \ 65 | "type 'help' for interactive help \n" 66 | greeting = greeting_decode(buf.encode()) 67 | self.assertEqual(greeting.version_id, version_id(1, 6, 6)) 68 | self.assertEqual(greeting.protocol, "Lua console") 69 | self.assertIsNone(greeting.uuid) 70 | self.assertIsNone(greeting.salt) 71 | 72 | def test_03_greeting_1_6_7(self): 73 | buf = "Tarantool 1.6.7 (Binary) 52dc2837-8001-48fe-bdce-c493c04599ce \n" + \ 74 | "Z+2F+VRlyK1nKT82xQtxqEggMtkTK5RtPYf27JryRas= \n" 75 | greeting = greeting_decode(buf.encode()) 76 | self.assertEqual(greeting.version_id, version_id(1, 6, 7)) 77 | self.assertEqual(greeting.protocol, "Binary") 78 | self.assertEqual(greeting.uuid, 79 | uuid.UUID('52dc2837-8001-48fe-bdce-c493c04599ce')) 80 | self.assertIsNotNone(greeting.salt) 81 | 82 | def test_04_protocol(self): 83 | # First Tarantool protocol version (1) was introduced between 84 | # 2.10.0-beta1 and 2.10.0-beta2. Versions 2 and 3 were also 85 | # introduced between 2.10.0-beta1 and 2.10.0-beta2. Version 4 86 | # was introduced between 2.10.0-beta2 and 2.10.0-rc1 and reverted 87 | # back to version 3 in the same version interval. 88 | # Tarantool 2.10.3 still has version 3. 89 | if self.adm.tnt_version >= pkg_resources.parse_version('2.10.0'): 90 | self.assertTrue(self.con._protocol_version >= 3) 91 | self.assertEqual(self.con._features[IPROTO_FEATURE_ERROR_EXTENSION], True) 92 | else: 93 | self.assertIsNone(self.con._protocol_version) 94 | self.assertEqual(self.con._features[IPROTO_FEATURE_ERROR_EXTENSION], False) 95 | 96 | self.assertEqual(self.con._features[IPROTO_FEATURE_STREAMS], False) 97 | self.assertEqual(self.con._features[IPROTO_FEATURE_TRANSACTIONS], False) 98 | self.assertEqual(self.con._features[IPROTO_FEATURE_WATCHERS], False) 99 | self.assertEqual(self.con._features[IPROTO_FEATURE_PAGINATION], False) 100 | self.assertEqual(self.con._features[IPROTO_FEATURE_SPACE_AND_INDEX_NAMES], False) 101 | self.assertEqual(self.con._features[IPROTO_FEATURE_WATCH_ONCE], False) 102 | 103 | @skip_or_run_iproto_basic_features_test 104 | def test_protocol_requirement(self): 105 | try: 106 | con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 107 | required_protocol_version=3, 108 | required_features=[IPROTO_FEATURE_STREAMS, 109 | IPROTO_FEATURE_TRANSACTIONS, 110 | IPROTO_FEATURE_ERROR_EXTENSION]) 111 | con.close() 112 | except Exception as exc: # pylint: disable=bad-option-value,broad-exception-caught,broad-except 113 | self.fail(f'Connection create have raised Exception: {repr(exc)}') 114 | 115 | def test_protocol_version_requirement_fail(self): 116 | with self.assertRaisesRegex(NetworkError, # ConfigurationError is wrapped in NetworkError 117 | 'protocol version 100500 is required'): 118 | con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 119 | required_protocol_version=100500) 120 | con.close() 121 | 122 | def test_protocol_features_requirement_fail(self): 123 | with self.assertRaisesRegex(NetworkError, # ConfigurationError is wrapped in NetworkError 124 | 'Server missing protocol features with id 100500, 500100'): 125 | con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 126 | required_features=[100500, 500100]) 127 | con.close() 128 | 129 | @classmethod 130 | def tearDownClass(cls): 131 | cls.con.close() 132 | cls.srv.stop() 133 | cls.srv.clean() 134 | -------------------------------------------------------------------------------- /test/suites/test_push.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests work with IPROTO pushed messages. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | import tarantool 9 | from .lib.tarantool_server import TarantoolServer 10 | from .utils import assert_admin_success 11 | 12 | 13 | def create_server(): 14 | srv = TarantoolServer() 15 | srv.script = 'test/suites/box.lua' 16 | srv.start() 17 | resp = srv.admin(""" 18 | box.schema.user.create('test', {password = 'test', if_not_exists = true}) 19 | box.schema.user.grant('test', 'read,write,execute', 'universe', 20 | nil, {if_not_exists = true}) 21 | 22 | function server_function() 23 | x = {0,0} 24 | while x[1] < 3 do 25 | x[1] = x[1] + 1 26 | box.session.push(x) 27 | end 28 | return x 29 | end 30 | 31 | box.schema.create_space( 32 | 'tester', { 33 | format = { 34 | {name = 'id', type = 'unsigned'}, 35 | {name = 'name', type = 'string'}, 36 | } 37 | }) 38 | 39 | box.space.tester:create_index( 40 | 'primary_index', { 41 | parts = { 42 | {field = 1, type = 'unsigned'}, 43 | }, 44 | if_not_exists = true, 45 | }) 46 | 47 | function on_replace_callback() 48 | x = {0,0} 49 | while x[1] < 300 do 50 | x[1] = x[1] + 100 51 | box.session.push(x) 52 | end 53 | end 54 | 55 | box.space.tester:on_replace( 56 | on_replace_callback 57 | ) 58 | 59 | return true 60 | """) 61 | assert_admin_success(resp) 62 | 63 | return srv 64 | 65 | 66 | # Callback for on_push arg (for testing purposes). 67 | def push_callback(data, on_push_ctx): 68 | data[0][1] = data[0][1] + 1 69 | on_push_ctx.append(data) 70 | 71 | 72 | class TestSuitePush(unittest.TestCase): 73 | 74 | @classmethod 75 | def setUpClass(cls): 76 | print(' PUSH '.center(70, '='), file=sys.stderr) 77 | print('-' * 70, file=sys.stderr) 78 | # Create server and extract helpful fields for tests. 79 | cls.srv = create_server() 80 | cls.host = cls.srv.host 81 | cls.port = cls.srv.args['primary'] 82 | 83 | def setUp(self): 84 | # Open connection, connection pool and mesh connection to instance. 85 | self.conn = tarantool.Connection(host=self.host, port=self.port, 86 | user='test', password='test') 87 | self.conn_pool = tarantool.ConnectionPool([{'host': self.host, 'port': self.port}], 88 | user='test', password='test') 89 | self.mesh_conn = tarantool.MeshConnection(host=self.host, port=self.port, 90 | user='test', password='test') 91 | 92 | push_test_cases = { 93 | 'call': { 94 | 'input': { 95 | 'args': ['server_function'], 96 | 'kwargs': { 97 | 'on_push': push_callback, 98 | # on_push_ctx must be set manually when running the test. 99 | 'on_push_ctx': None, 100 | } 101 | }, 102 | 'output': { 103 | 'callback_res': [[[1, 1]], [[2, 1]], [[3, 1]]], 104 | 'resp': [3, 0], 105 | }, 106 | }, 107 | 'eval': { 108 | 'input': { 109 | 'args': ['return server_function()'], 110 | 'kwargs': { 111 | 'on_push': push_callback, 112 | # on_push_ctx must be set manually when running the test. 113 | 'on_push_ctx': None, 114 | } 115 | }, 116 | 'output': { 117 | 'callback_res': [[[1, 1]], [[2, 1]], [[3, 1]]], 118 | 'resp': [3, 0], 119 | }, 120 | }, 121 | 'insert': { 122 | 'input': { 123 | 'args': ['tester', (1, 'Mike')], 124 | 'kwargs': { 125 | 'on_push': push_callback, 126 | # on_push_ctx must be set manually when running the test. 127 | 'on_push_ctx': None, 128 | } 129 | }, 130 | 'output': { 131 | 'callback_res': [[[100, 1]], [[200, 1]], [[300, 1]]], 132 | 'resp': [1, 'Mike'], 133 | }, 134 | }, 135 | 'replace': { 136 | 'input': { 137 | 'args': ['tester', (1, 'Bill')], 138 | 'kwargs': { 139 | 'on_push': push_callback, 140 | # on_push_ctx must be set manually when running the test. 141 | 'on_push_ctx': None, 142 | } 143 | }, 144 | 'output': { 145 | 'callback_res': [[[100, 1]], [[200, 1]], [[300, 1]]], 146 | 'resp': [1, 'Bill'], 147 | }, 148 | }, 149 | 'update': { 150 | 'input': { 151 | 'args': ['tester', 1], 152 | 'kwargs': { 153 | 'op_list': [], 154 | 'on_push': push_callback, 155 | # on_push_ctx must be set manually when running the test. 156 | 'on_push_ctx': None, 157 | } 158 | }, 159 | 'output': { 160 | 'callback_res': [[[100, 1]], [[200, 1]], [[300, 1]]], 161 | 'resp': [1, 'Bill'], 162 | }, 163 | }, 164 | 'upsert': { 165 | 'input': { 166 | 'args': ['tester', (1, 'Bill')], 167 | 'kwargs': { 168 | 'op_list': [], 169 | 'on_push': push_callback, 170 | # on_push_ctx must be set manually when running the test. 171 | 'on_push_ctx': None, 172 | } 173 | }, 174 | 'output': { 175 | 'callback_res': [[[100, 1]], [[200, 1]], [[300, 1]]], 176 | # resp not used in the test output. 177 | 'resp': None, 178 | }, 179 | }, 180 | 'delete': { 181 | 'input': { 182 | 'args': ['tester', 1], 183 | 'kwargs': { 184 | 'on_push': push_callback, 185 | # on_push_ctx must be set manually when running the test. 186 | 'on_push_ctx': None, 187 | } 188 | }, 189 | 'output': { 190 | 'callback_res': [[[100, 1]], [[200, 1]], [[300, 1]]], 191 | 'resp': [1, 'Bill'], 192 | }, 193 | }, 194 | } 195 | 196 | def test_00_00_push_via_connection(self): 197 | for name, case in self.push_test_cases.items(): 198 | with self.subTest(name=name): 199 | callback_res = [] 200 | testing_function = getattr(self.conn, name) 201 | case['input']['kwargs']['on_push_ctx'] = callback_res 202 | resp = testing_function( 203 | *case['input']['args'], 204 | **case['input']['kwargs'] 205 | ) 206 | self.assertEqual(callback_res, case['output']['callback_res']) 207 | if case['output']['resp'] is not None: 208 | self.assertEqual(resp.data[0], case['output']['resp']) 209 | 210 | def test_00_01_push_via_mesh_connection(self): 211 | for name, case in self.push_test_cases.items(): 212 | with self.subTest(name=name): 213 | callback_res = [] 214 | testing_function = getattr(self.mesh_conn, name) 215 | case['input']['kwargs']['on_push_ctx'] = callback_res 216 | resp = testing_function( 217 | *case['input']['args'], 218 | **case['input']['kwargs'] 219 | ) 220 | self.assertEqual(callback_res, case['output']['callback_res']) 221 | if case['output']['resp'] is not None: 222 | self.assertEqual(resp.data[0], case['output']['resp']) 223 | 224 | def test_00_02_push_via_connection_pool(self): 225 | for name, case in self.push_test_cases.items(): 226 | with self.subTest(name=name): 227 | callback_res = [] 228 | testing_function = getattr(self.conn_pool, name) 229 | case['input']['kwargs']['on_push_ctx'] = callback_res 230 | resp = testing_function( 231 | *case['input']['args'], 232 | **case['input']['kwargs'], 233 | mode=tarantool.Mode.RW 234 | ) 235 | self.assertEqual(callback_res, case['output']['callback_res']) 236 | if case['output']['resp'] is not None: 237 | self.assertEqual(resp.data[0], case['output']['resp']) 238 | 239 | def tearDown(self): 240 | # Close connection, connection pool and mesh connection to instance. 241 | self.conn.close() 242 | self.conn_pool.close() 243 | self.mesh_conn.close() 244 | 245 | @classmethod 246 | def tearDownClass(cls): 247 | # Stop instance. 248 | cls.srv.stop() 249 | cls.srv.clean() 250 | -------------------------------------------------------------------------------- /test/suites/test_reconnect.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests basic reconnect behavior. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring 5 | 6 | import sys 7 | import unittest 8 | import warnings 9 | import tarantool 10 | from .lib.tarantool_server import TarantoolServer 11 | 12 | 13 | class TestSuiteReconnect(unittest.TestCase): 14 | @classmethod 15 | def setUpClass(cls): 16 | print(' RECONNECT '.center(70, '='), file=sys.stderr) 17 | print('-' * 70, file=sys.stderr) 18 | cls.srv = TarantoolServer() 19 | cls.srv.script = 'test/suites/box.lua' 20 | 21 | def setUp(self): 22 | # prevent a remote tarantool from clean our session 23 | if self.srv.is_started(): 24 | self.srv.touch_lock() 25 | 26 | def test_01_simple(self): 27 | # Create a connection, but don't connect it. 28 | con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 29 | connect_now=False) 30 | 31 | # Trigger a reconnection due to server unavailability. 32 | with warnings.catch_warnings(): 33 | warnings.simplefilter("ignore") 34 | with self.assertRaises(tarantool.error.NetworkError): 35 | con.ping() 36 | 37 | # Start a server and verify that the reconnection 38 | # succeeds. 39 | self.srv.start() 40 | self.assertIs(con.ping(notime=True), "Success") 41 | 42 | # Close the connection and stop the server. 43 | con.close() 44 | self.srv.stop() 45 | 46 | def test_02_wrong_auth(self): 47 | # Create a connection with wrong credentials, but don't 48 | # connect it. 49 | con = tarantool.Connection(self.srv.host, self.srv.args['primary'], 50 | connect_now=False, user='not_exist') 51 | 52 | # Start a server. 53 | self.srv.start() 54 | 55 | # Trigger a reconnection due to wrong credentials. 56 | with warnings.catch_warnings(): 57 | warnings.simplefilter("ignore") 58 | with self.assertRaises(tarantool.error.DatabaseError): 59 | con.ping() 60 | 61 | # Set right credentials and verify that the reconnection 62 | # succeeds. 63 | con.user = None 64 | self.assertIs(con.ping(notime=True), "Success") 65 | 66 | # Close the connection and stop the server. 67 | con.close() 68 | self.srv.stop() 69 | 70 | def test_03_connect_after_close(self): 71 | # Start a server and connect to it. 72 | self.srv.start() 73 | con = tarantool.Connection(self.srv.host, self.srv.args['primary']) 74 | con.ping() 75 | 76 | # Close the connection and connect again. 77 | con.close() 78 | con.connect() 79 | 80 | # Verify that the connection is alive. 81 | con.ping() 82 | 83 | # Close the connection and stop the server. 84 | con.close() 85 | self.srv.stop() 86 | 87 | @classmethod 88 | def tearDownClass(cls): 89 | cls.srv.clean() 90 | -------------------------------------------------------------------------------- /test/suites/test_socket_fd.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests work with connection over socket fd. 3 | """ 4 | import os.path 5 | # pylint: disable=missing-class-docstring,missing-function-docstring 6 | 7 | import socket 8 | import sys 9 | import unittest 10 | 11 | import tarantool 12 | from .lib.skip import skip_or_run_box_session_new_tests 13 | from .lib.tarantool_server import TarantoolServer, find_port 14 | from .utils import assert_admin_success 15 | 16 | 17 | def find_python(): 18 | for _dir in os.environ["PATH"].split(os.pathsep): 19 | exe = os.path.join(_dir, "python") 20 | if os.access(exe, os.X_OK): 21 | return os.path.abspath(exe) 22 | raise RuntimeError("Can't find python executable in " + os.environ["PATH"]) 23 | 24 | 25 | class TestSuiteSocketFD(unittest.TestCase): 26 | EVAL_USER = "return box.session.user()" 27 | 28 | @classmethod 29 | def setUpClass(cls): 30 | print(' SOCKET FD '.center(70, '='), file=sys.stderr) 31 | print('-' * 70, file=sys.stderr) 32 | 33 | cls.srv = TarantoolServer() 34 | cls.srv.script = 'test/suites/box.lua' 35 | cls.srv.start() 36 | cls.tcp_port = find_port() 37 | 38 | # Start tcp server to test work with blocking sockets. 39 | # pylint: disable=consider-using-f-string 40 | resp = cls.srv.admin(""" 41 | local socket = require('socket') 42 | 43 | box.cfg{} 44 | box.schema.user.create('test', {password = 'test', if_not_exists = true}) 45 | box.schema.user.grant('test', 'read,write,execute,create', 'universe', 46 | nil, {if_not_exists = true}) 47 | box.schema.user.grant('guest', 'execute', 'universe', 48 | nil, {if_not_exists = true}) 49 | 50 | socket.tcp_server('0.0.0.0', %d, function(s) 51 | if not s:nonblock(true) then 52 | s:close() 53 | return 54 | end 55 | box.session.new({ 56 | type = 'binary', 57 | fd = s:fd(), 58 | user = 'test', 59 | }) 60 | s:detach() 61 | end) 62 | 63 | box.schema.create_space('test', { 64 | format = {{type='unsigned', name='id'}}, 65 | if_not_exists = true, 66 | }) 67 | box.space.test:create_index('primary') 68 | 69 | return true 70 | """ % cls.tcp_port) 71 | assert_admin_success(resp) 72 | 73 | @skip_or_run_box_session_new_tests 74 | def setUp(self): 75 | # Prevent a remote tarantool from clean our session. 76 | if self.srv.is_started(): 77 | self.srv.touch_lock() 78 | 79 | def _get_tt_sock(self): 80 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 81 | sock.connect((self.srv.host, self.tcp_port)) 82 | return sock 83 | 84 | def test_01_incorrect_params(self): 85 | cases = { 86 | "host and socket_fd": { 87 | "args": {"host": "123", "socket_fd": 3}, 88 | "msg": "specifying both socket_fd and host/port is not allowed", 89 | }, 90 | "port and socket_fd": { 91 | "args": {"port": 14, "socket_fd": 3}, 92 | "msg": "specifying both socket_fd and host/port is not allowed", 93 | }, 94 | "empty": { 95 | "args": {}, 96 | "msg": r"host/port.* port.* or socket_fd", 97 | }, 98 | "only host": { 99 | "args": {"host": "localhost"}, 100 | "msg": "when specifying host, it is also necessary to specify port", 101 | }, 102 | } 103 | 104 | for name, case in cases.items(): 105 | with self.subTest(msg=name): 106 | with self.assertRaisesRegex(tarantool.Error, case["msg"]): 107 | tarantool.Connection(**case["args"]) 108 | 109 | def test_02_socket_fd_connect(self): 110 | sock = self._get_tt_sock() 111 | conn = tarantool.connect(None, None, socket_fd=sock.fileno()) 112 | sock.detach() 113 | try: 114 | self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["test"]) 115 | finally: 116 | conn.close() 117 | 118 | def test_03_socket_fd_re_auth(self): 119 | sock = self._get_tt_sock() 120 | conn = tarantool.connect(None, None, socket_fd=sock.fileno(), user="guest") 121 | sock.detach() 122 | try: 123 | self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["guest"]) 124 | finally: 125 | conn.close() 126 | 127 | @unittest.skipIf(sys.platform.startswith("win"), 128 | "Skip on Windows since it uses remote server") 129 | def test_04_tarantool_made_socket(self): 130 | python_exe = find_python() 131 | cwd = os.getcwd() 132 | side_script_path = os.path.join(cwd, "test", "suites", "sidecar.py") 133 | 134 | # pylint: disable=consider-using-f-string 135 | ret_code, err = self.srv.admin(""" 136 | local socket = require('socket') 137 | local popen = require('popen') 138 | local os = require('os') 139 | local s1, s2 = socket.socketpair('AF_UNIX', 'SOCK_STREAM', 0) 140 | 141 | --[[ Tell sidecar which fd use to connect. --]] 142 | os.setenv('SOCKET_FD', tostring(s2:fd())) 143 | 144 | --[[ Tell sidecar where find `tarantool` module. --]] 145 | os.setenv('PYTHONPATH', (os.getenv('PYTHONPATH') or '') .. ':' .. '%s') 146 | 147 | box.session.new({ 148 | type = 'binary', 149 | fd = s1:fd(), 150 | user = 'test', 151 | }) 152 | s1:detach() 153 | 154 | local ph, err = popen.new({'%s', '%s'}, { 155 | stdout = popen.opts.PIPE, 156 | stderr = popen.opts.PIPE, 157 | inherit_fds = {s2:fd()}, 158 | }) 159 | 160 | if err ~= nil then 161 | return 1, err 162 | end 163 | 164 | ph:wait() 165 | 166 | local status_code = ph:info().status.exit_code 167 | local stderr = ph:read({stderr=true}):rstrip() 168 | return status_code, stderr 169 | """ % (cwd, python_exe, side_script_path)) 170 | self.assertIsNone(err, err) 171 | self.assertEqual(ret_code, 0) 172 | 173 | def test_05_socket_fd_pool(self): 174 | sock = self._get_tt_sock() 175 | pool = tarantool.ConnectionPool( 176 | addrs=[{'host': None, 'port': None, 'socket_fd': sock.fileno()}] 177 | ) 178 | sock.detach() 179 | try: 180 | self.assertSequenceEqual(pool.eval(self.EVAL_USER, mode=tarantool.Mode.ANY), ["test"]) 181 | finally: 182 | pool.close() 183 | 184 | def test_06_socket_fd_mesh(self): 185 | sock = self._get_tt_sock() 186 | mesh = tarantool.MeshConnection( 187 | host=None, 188 | port=None, 189 | socket_fd=sock.fileno() 190 | ) 191 | sock.detach() 192 | try: 193 | self.assertSequenceEqual(mesh.eval(self.EVAL_USER), ["test"]) 194 | finally: 195 | mesh.close() 196 | 197 | @classmethod 198 | def tearDownClass(cls): 199 | cls.srv.stop() 200 | cls.srv.clean() 201 | -------------------------------------------------------------------------------- /test/suites/test_uuid.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module tests work with UUID type. 3 | """ 4 | # pylint: disable=missing-class-docstring,missing-function-docstring,duplicate-code 5 | 6 | import sys 7 | import unittest 8 | import uuid 9 | 10 | import pkg_resources 11 | import msgpack 12 | 13 | import tarantool 14 | from tarantool.msgpack_ext.packer import default as packer_default 15 | from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook 16 | 17 | from .lib.tarantool_server import TarantoolServer 18 | from .lib.skip import skip_or_run_uuid_test 19 | from .utils import assert_admin_success 20 | 21 | 22 | class TestSuiteUUID(unittest.TestCase): 23 | @classmethod 24 | def setUpClass(cls): 25 | print(' UUID EXT TYPE '.center(70, '='), file=sys.stderr) 26 | print('-' * 70, file=sys.stderr) 27 | cls.srv = TarantoolServer() 28 | cls.srv.script = 'test/suites/box.lua' 29 | cls.srv.start() 30 | 31 | cls.adm = cls.srv.admin 32 | resp = cls.adm(""" 33 | _, uuid = pcall(require, 'uuid') 34 | 35 | box.schema.space.create('test', {if_not_exists = true}) 36 | box.space['test']:create_index('primary', { 37 | type = 'tree', 38 | parts = {1, 'string'}, 39 | unique = true, 40 | if_not_exists = true}) 41 | 42 | box.schema.user.create('test', {password = 'test', if_not_exists = true}) 43 | box.schema.user.grant('test', 'read,write,execute', 'universe', 44 | nil, {if_not_exists = true}) 45 | 46 | return true 47 | """) 48 | assert_admin_success(resp) 49 | 50 | if cls.srv.admin.tnt_version >= pkg_resources.parse_version('2.4.1'): 51 | resp = cls.adm(""" 52 | box.schema.space.create('test_pk', {if_not_exists = true}) 53 | box.space['test_pk']:create_index('primary', { 54 | type = 'tree', 55 | parts = {1, 'uuid'}, 56 | unique = true, 57 | if_not_exists = true}) 58 | return true 59 | """) 60 | assert_admin_success(resp) 61 | 62 | cls.con = tarantool.Connection(cls.srv.host, cls.srv.args['primary'], 63 | user='test', password='test') 64 | 65 | def setUp(self): 66 | # prevent a remote tarantool from clean our session 67 | if self.srv.is_started(): 68 | self.srv.touch_lock() 69 | 70 | resp = self.adm(""" 71 | box.space['test']:truncate() 72 | return true 73 | """) 74 | assert_admin_success(resp) 75 | 76 | cases = { 77 | 'uuid_1': { 78 | 'python': uuid.UUID('ae28d4f6-076c-49dd-8227-7f9fae9592d0'), 79 | 'msgpack': (b'\xae\x28\xd4\xf6\x07\x6c\x49\xdd\x82\x27\x7f\x9f\xae\x95\x92\xd0'), 80 | 'tarantool': "uuid.fromstr('ae28d4f6-076c-49dd-8227-7f9fae9592d0')", 81 | }, 82 | 'uuid_2': { 83 | 'python': uuid.UUID('b3121301-9300-4038-a652-ead943fb9c39'), 84 | 'msgpack': (b'\xb3\x12\x13\x01\x93\x00\x40\x38\xa6\x52\xea\xd9\x43\xfb\x9c\x39'), 85 | 'tarantool': "uuid.fromstr('b3121301-9300-4038-a652-ead943fb9c39')", 86 | }, 87 | 'uuid_3': { 88 | 'python': uuid.UUID('dfa69f02-92e6-44a5-abb5-84b39292ff93'), 89 | 'msgpack': (b'\xdf\xa6\x9f\x02\x92\xe6\x44\xa5\xab\xb5\x84\xb3\x92\x92\xff\x93'), 90 | 'tarantool': "uuid.fromstr('dfa69f02-92e6-44a5-abb5-84b39292ff93')", 91 | }, 92 | 'uuid_4': { 93 | 'python': uuid.UUID('8b69a1ce-094a-4e21-a5dc-4cdae7cd8960'), 94 | 'msgpack': (b'\x8b\x69\xa1\xce\x09\x4a\x4e\x21\xa5\xdc\x4c\xda\xe7\xcd\x89\x60'), 95 | 'tarantool': "uuid.fromstr('8b69a1ce-094a-4e21-a5dc-4cdae7cd8960')", 96 | }, 97 | 'uuid_5': { 98 | 'python': uuid.UUID('25932334-1d42-4686-9299-ec1a7165227c'), 99 | 'msgpack': (b'\x25\x93\x23\x34\x1d\x42\x46\x86\x92\x99\xec\x1a\x71\x65\x22\x7c'), 100 | 'tarantool': "uuid.fromstr('25932334-1d42-4686-9299-ec1a7165227c')", 101 | }, 102 | } 103 | 104 | def test_msgpack_decode(self): 105 | for name, case in self.cases.items(): 106 | with self.subTest(msg=name): 107 | self.assertEqual(unpacker_ext_hook(2, case['msgpack']), 108 | case['python']) 109 | 110 | @skip_or_run_uuid_test 111 | def test_tarantool_decode(self): 112 | for name, case in self.cases.items(): 113 | with self.subTest(msg=name): 114 | self.adm(f"box.space['test']:replace{{'{name}', {case['tarantool']}}}") 115 | 116 | self.assertSequenceEqual(self.con.select('test', name), 117 | [[name, case['python']]]) 118 | 119 | def test_msgpack_encode(self): 120 | for name, case in self.cases.items(): 121 | with self.subTest(msg=name): 122 | self.assertEqual(packer_default(case['python']), 123 | msgpack.ExtType(code=2, data=case['msgpack'])) 124 | 125 | @skip_or_run_uuid_test 126 | def test_tarantool_encode(self): 127 | for name, case in self.cases.items(): 128 | with self.subTest(msg=name): 129 | self.con.insert('test', [name, case['python']]) 130 | 131 | lua_eval = f""" 132 | local tuple = box.space['test']:get('{name}') 133 | assert(tuple ~= nil) 134 | 135 | local id = {case['tarantool']} 136 | if tuple[2] == id then 137 | return true 138 | else 139 | return nil, ('%s is not equal to expected %s'):format( 140 | tostring(tuple[2]), tostring(id)) 141 | end 142 | """ 143 | 144 | self.assertSequenceEqual(self.con.eval(lua_eval), [True]) 145 | 146 | @skip_or_run_uuid_test 147 | def test_primary_key(self): 148 | data = [uuid.UUID('ae28d4f6-076c-49dd-8227-7f9fae9592d0'), 'content'] 149 | 150 | self.assertSequenceEqual(self.con.insert('test_pk', data), [data]) 151 | self.assertSequenceEqual(self.con.select('test_pk', data[0]), [data]) 152 | 153 | @classmethod 154 | def tearDownClass(cls): 155 | cls.con.close() 156 | cls.srv.stop() 157 | cls.srv.clean() 158 | -------------------------------------------------------------------------------- /test/suites/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various test utilities. 3 | """ 4 | 5 | 6 | def assert_admin_success(resp): 7 | """ 8 | Util to assert admin text request response. 9 | It is expected that request last line is `return true`. 10 | If something went wrong on executing, Tarantool throws an error 11 | which would be a part of return values. 12 | """ 13 | 14 | assert isinstance(resp, list), f'got unexpected resp type: {type(resp)}' 15 | assert len(resp) > 0, 'got unexpected empty resp' 16 | assert resp[0] is True, f'got unexpected resp: {resp}' 17 | --------------------------------------------------------------------------------