diff --git a/AUTHORS b/AUTHORS index 1bc49242..f77bd18d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,3 +33,5 @@ Vladislav Shpilevoy Artem Morozov Sergey Bronnikov Yaroslav Lobankov +Georgy Moiseev +Oleg Jukovec diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d415421..0f518c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 type. `tarantool.Datetime` may be encoded to Tarantool datetime objects. - You can create `tarantool.Datetime` objects either from msgpack - data or by using the same API as in Tarantool: + You can create `tarantool.Datetime` objects either from + MessagePack data or by using the same API as in Tarantool: ```python dt1 = tarantool.Datetime(year=2022, month=8, day=31, @@ -73,8 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 type. `tarantool.Interval` may be encoded to Tarantool interval objects. - You can create `tarantool.Interval` objects either from msgpack - data or by using the same API as in Tarantool: + You can create `tarantool.Interval` objects either from + MessagePack data or by using the same API as in Tarantool: ```python di = tarantool.Interval(year=-1, month=2, day=3, @@ -138,6 +138,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump msgpack requirement to 1.0.4 (PR #223). The only reason of this bump is various vulnerability fixes, msgpack>=0.4.0 and msgpack-python==0.4.0 are still supported. +- Change documentation HTML theme (#67). +- Update API documentation strings (#67). +- Update documentation index, quick start and guide pages (#67). ### Fixed diff --git a/INSTALL b/INSTALL index 5d7089ac..9c9975fc 100644 --- a/INSTALL +++ b/INSTALL @@ -14,4 +14,4 @@ Using `easy_install`:: You can also download the source tarball and install the package using distutils script:: - # python setup.py install + # make install diff --git a/Makefile b/Makefile index bfa3516b..1da07273 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -.PHONY: test +.PHONY: install test +install: + python setup.py install test: python setup.py test testdata: diff --git a/README.rst b/README.rst index 500e3a1d..e62407cc 100644 --- a/README.rst +++ b/README.rst @@ -22,31 +22,25 @@ With pip (recommended) The recommended way to install the ``tarantool`` package is using ``pip``. -For Tarantool version < 1.6.0, get the ``0.3.*`` connector version:: +.. code-block:: bash - $ pip install tarantool\<0.4 - -For a later Tarantool version, get the ``0.5.*`` connector version:: - - $ pip install tarantool\>0.4 + $ pip install tarantool ZIP archive ^^^^^^^^^^^ -You can also download zip archive, unpack it and run:: +You can also download zip archive, unpack it and run: - $ python setup.py install +.. code-block:: bash + + $ make install Development version ^^^^^^^^^^^^^^^^^^^ You can also install the development version of the package using ``pip``. -For Tarantool version < 1.6.0, get the ``stable`` branch:: - - $ pip install git+https://github.com/tarantool/tarantool-python.git@stable - -For a later Tarantool version, use the ``master`` branch:: +.. code-block:: bash $ pip install git+https://github.com/tarantool/tarantool-python.git@master @@ -55,17 +49,15 @@ For a later Tarantool version, use the ``master`` branch:: What is Tarantool? ------------------ -`Tarantool`_ is an in-memory NoSQL database with a Lua application server on board. -It combines the network programming power of Node.JS -with data persistency capabilities of Redis. -It's open-source, licensed under `BSD-2-Clause`_. +`Tarantool`_ is an in-memory computing platform originally designed by +`VK`_ and released under the terms of `BSD license`_. Features -------- * ANSI SQL, including views, joins, referential and check constraints * Lua packages for non-blocking I/O, fibers, and HTTP -* MsgPack data format and MsgPack-based client-server protocol +* MessagePack data format and MessagePack-based client-server protocol * Two data engines: * memtx – in-memory storage engine with optional persistence @@ -97,9 +89,9 @@ Run tests On Linux: -.. code-block:: console +.. code-block:: bash - $ python setup.py test + $ make test On Windows: @@ -111,14 +103,40 @@ On Windows: * Set the following environment variables: * ``REMOTE_TARANTOOL_HOST=...``, * ``REMOTE_TARANTOOL_CONSOLE_PORT=3302``. -* Run ``python setup.py test``. +* Run ``make test``. + +Build docs +^^^^^^^^^^ + +To build documentation, first you must install its build requirements: + +.. code-block:: bash + + $ pip install -r requirements-doc.txt + +Then run + +.. code-block:: bash + + $ make docs + +You may host local documentation server with + +.. code-block:: bash + + $ python -m http.server --directory build/sphinx/html + +Open ``localhost:8000`` in your browser to read the docs. .. _`Tarantool`: .. _`Tarantool Database`: .. _`Tarantool homepage`: https://tarantool.io .. _`Tarantool on GitHub`: https://github.com/tarantool/tarantool .. _`Tarantool documentation`: https://www.tarantool.io/en/doc/latest/ +.. _`VK`: https://vk.company .. _`Client-server protocol specification`: https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/ +.. _`BSD`: +.. _`BSD license`: .. _`BSD-2-Clause`: https://opensource.org/licenses/BSD-2-Clause .. _`asynctnt`: https://github.com/igorcoding/asynctnt .. _`feature comparison table`: https://www.tarantool.io/en/doc/latest/book/connectors/#python-feature-comparison diff --git a/doc/_static/favicon/apple-touch-icon-114x114.png b/doc/_static/favicon/apple-touch-icon-114x114.png new file mode 100644 index 00000000..054be521 Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-114x114.png differ diff --git a/doc/_static/favicon/apple-touch-icon-120x120.png b/doc/_static/favicon/apple-touch-icon-120x120.png new file mode 100644 index 00000000..a0e72b27 Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-120x120.png differ diff --git a/doc/_static/favicon/apple-touch-icon-144x144.png b/doc/_static/favicon/apple-touch-icon-144x144.png new file mode 100644 index 00000000..90a616fc Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-144x144.png differ diff --git a/doc/_static/favicon/apple-touch-icon-152x152.png b/doc/_static/favicon/apple-touch-icon-152x152.png new file mode 100644 index 00000000..1b4cda08 Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-152x152.png differ diff --git a/doc/_static/favicon/apple-touch-icon-57x57.png b/doc/_static/favicon/apple-touch-icon-57x57.png new file mode 100644 index 00000000..b2a09aaa Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-57x57.png differ diff --git a/doc/_static/favicon/apple-touch-icon-60x60.png b/doc/_static/favicon/apple-touch-icon-60x60.png new file mode 100644 index 00000000..1588e24f Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-60x60.png differ diff --git a/doc/_static/favicon/apple-touch-icon-72x72.png b/doc/_static/favicon/apple-touch-icon-72x72.png new file mode 100644 index 00000000..26b1f1a4 Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-72x72.png differ diff --git a/doc/_static/favicon/apple-touch-icon-76x76.png b/doc/_static/favicon/apple-touch-icon-76x76.png new file mode 100644 index 00000000..be802bce Binary files /dev/null and b/doc/_static/favicon/apple-touch-icon-76x76.png differ diff --git a/doc/_static/favicon/generate-png.sh b/doc/_static/favicon/generate-png.sh new file mode 100755 index 00000000..02bc0d6f --- /dev/null +++ b/doc/_static/favicon/generate-png.sh @@ -0,0 +1,9 @@ +for SIZE in 16 32 96 128 196 +do + convert -background none -resize ${SIZE}x${SIZE} icon.svg icon-${SIZE}x${SIZE}.png +done + +for SIZE in 57 60 72 76 114 120 144 152 +do + convert -background none -resize ${SIZE}x${SIZE} icon.svg apple-touch-icon-${SIZE}x${SIZE}.png +done diff --git a/doc/_static/favicon/icon-128x128.png b/doc/_static/favicon/icon-128x128.png new file mode 100644 index 00000000..99716a79 Binary files /dev/null and b/doc/_static/favicon/icon-128x128.png differ diff --git a/doc/_static/favicon/icon-16x16.png b/doc/_static/favicon/icon-16x16.png new file mode 100644 index 00000000..6a35da34 Binary files /dev/null and b/doc/_static/favicon/icon-16x16.png differ diff --git a/doc/_static/favicon/icon-196x196.png b/doc/_static/favicon/icon-196x196.png new file mode 100644 index 00000000..0b36131d Binary files /dev/null and b/doc/_static/favicon/icon-196x196.png differ diff --git a/doc/_static/favicon/icon-32x32.png b/doc/_static/favicon/icon-32x32.png new file mode 100644 index 00000000..4690f3f0 Binary files /dev/null and b/doc/_static/favicon/icon-32x32.png differ diff --git a/doc/_static/favicon/icon-96x96.png b/doc/_static/favicon/icon-96x96.png new file mode 100644 index 00000000..abc5d235 Binary files /dev/null and b/doc/_static/favicon/icon-96x96.png differ diff --git a/doc/_static/favicon/icon.svg b/doc/_static/favicon/icon.svg new file mode 100644 index 00000000..0f5acac8 --- /dev/null +++ b/doc/_static/favicon/icon.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_static/tarantool.css b/doc/_static/tarantool.css deleted file mode 100644 index fb94f7ef..00000000 --- a/doc/_static/tarantool.css +++ /dev/null @@ -1,19 +0,0 @@ -@import url("default.css"); - -cite, code, tt { - font-family: 'Consolas', 'Deja Vu Sans Mono', - 'Bitstream Vera Sans Mono', monospace; -} - -dl.attribute, dl.class, dl.method { - margin-top: 2em; - margin-bottom: 2em; -} - -tt { - font-size: 100%; -} - -th { - background-color: #fff; -} diff --git a/doc/api/class-connection.rst b/doc/api/class-connection.rst deleted file mode 100644 index 35ea7765..00000000 --- a/doc/api/class-connection.rst +++ /dev/null @@ -1,10 +0,0 @@ - -.. currentmodule:: tarantool.connection - -class :class:`Connection` -------------------------- - -.. autoclass:: Connection - :members: close, ping, space - - .. automethod:: call(func_name, *args) diff --git a/doc/api/class-mesh-connection.rst b/doc/api/class-mesh-connection.rst deleted file mode 100644 index d1048eba..00000000 --- a/doc/api/class-mesh-connection.rst +++ /dev/null @@ -1,8 +0,0 @@ - -.. currentmodule:: tarantool.mesh_connection - -class :class:`MeshConnection` ------------------------------ - -.. autoclass:: MeshConnection - diff --git a/doc/api/class-response.rst b/doc/api/class-response.rst deleted file mode 100644 index 9739d32d..00000000 --- a/doc/api/class-response.rst +++ /dev/null @@ -1,9 +0,0 @@ - -.. currentmodule:: tarantool.response - -class :class:`Response` -------------------------- - -.. autoclass:: Response - :members: - :undoc-members: diff --git a/doc/api/class-space.rst b/doc/api/class-space.rst deleted file mode 100644 index 947c550e..00000000 --- a/doc/api/class-space.rst +++ /dev/null @@ -1,9 +0,0 @@ - -.. currentmodule:: tarantool.space - -class :class:`Space` --------------------- - -.. autoclass:: tarantool.space.Space - :members: - :undoc-members: diff --git a/doc/api/module-tarantool.rst b/doc/api/module-tarantool.rst index 0fad1230..9134d9f0 100644 --- a/doc/api/module-tarantool.rst +++ b/doc/api/module-tarantool.rst @@ -2,5 +2,114 @@ module :py:mod:`tarantool` ========================== .. automodule:: tarantool - :members: - :undoc-members: + :exclude-members: Connection, MeshConnection, + Error, DatabaseError, NetworkError, NetworkWarning, + SchemaError + + .. autoclass:: tarantool.Connection + :exclude-members: Error, DatabaseError, InterfaceError, + ConfigurationError, SchemaError, NetworkError, + Warning, DataError, OperationalError, IntegrityError, + InternalError, ProgrammingError, NotSupportedError + + .. autoattribute:: Error + :noindex: + + .. autoattribute:: DatabaseError + :noindex: + + .. autoattribute:: InterfaceError + :noindex: + + .. autoattribute:: ConfigurationError + :noindex: + + .. autoattribute:: SchemaError + :noindex: + + .. autoattribute:: NetworkError + :noindex: + + .. autoattribute:: Warning + :noindex: + + .. autoattribute:: DataError + :noindex: + + .. autoattribute:: OperationalError + :noindex: + + .. autoattribute:: IntegrityError + :noindex: + + .. autoattribute:: InternalError + :noindex: + + .. autoattribute:: ProgrammingError + :noindex: + + .. autoattribute:: NotSupportedError + :noindex: + + .. autoclass:: tarantool.MeshConnection + :exclude-members: Error, DatabaseError, InterfaceError, + ConfigurationError, SchemaError, NetworkError, + Warning, DataError, OperationalError, IntegrityError, + InternalError, ProgrammingError, NotSupportedError + + .. autoattribute:: Error + :noindex: + + .. autoattribute:: DatabaseError + :noindex: + + .. autoattribute:: InterfaceError + :noindex: + + .. autoattribute:: ConfigurationError + :noindex: + + .. autoattribute:: SchemaError + :noindex: + + .. autoattribute:: NetworkError + :noindex: + + .. autoattribute:: Warning + :noindex: + + .. autoattribute:: DataError + :noindex: + + .. autoattribute:: OperationalError + :noindex: + + .. autoattribute:: IntegrityError + :noindex: + + .. autoattribute:: InternalError + :noindex: + + .. autoattribute:: ProgrammingError + :noindex: + + .. autoattribute:: NotSupportedError + :noindex: + + .. autoexception:: Error + :noindex: + + .. autoexception:: DatabaseError + :noindex: + + .. autoexception:: DatabaseError + :noindex: + + .. autoexception:: NetworkError + :noindex: + + .. autoexception:: NetworkWarning + :noindex: + + .. autoexception:: SchemaError + :noindex: diff --git a/doc/api/submodule-connection-pool.rst b/doc/api/submodule-connection-pool.rst new file mode 100644 index 00000000..d8d2b3fb --- /dev/null +++ b/doc/api/submodule-connection-pool.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.connection_pool` +========================================== + +.. automodule:: tarantool.connection_pool diff --git a/doc/api/submodule-connection.rst b/doc/api/submodule-connection.rst new file mode 100644 index 00000000..a0211d55 --- /dev/null +++ b/doc/api/submodule-connection.rst @@ -0,0 +1,50 @@ +module :py:mod:`tarantool.connection` +===================================== + +.. automodule:: tarantool.connection + :exclude-members: Connection + + .. autoclass:: tarantool.connection.Connection + :exclude-members: Error, DatabaseError, InterfaceError, + ConfigurationError, SchemaError, NetworkError, + Warning, DataError, OperationalError, IntegrityError, + InternalError, ProgrammingError, NotSupportedError + + .. autoattribute:: Error + :noindex: + + .. autoattribute:: DatabaseError + :noindex: + + .. autoattribute:: InterfaceError + :noindex: + + .. autoattribute:: ConfigurationError + :noindex: + + .. autoattribute:: SchemaError + :noindex: + + .. autoattribute:: NetworkError + :noindex: + + .. autoattribute:: Warning + :noindex: + + .. autoattribute:: DataError + :noindex: + + .. autoattribute:: OperationalError + :noindex: + + .. autoattribute:: IntegrityError + :noindex: + + .. autoattribute:: InternalError + :noindex: + + .. autoattribute:: ProgrammingError + :noindex: + + .. autoattribute:: NotSupportedError + :noindex: diff --git a/doc/api/submodule-dbapi.rst b/doc/api/submodule-dbapi.rst new file mode 100644 index 00000000..9f388d5a --- /dev/null +++ b/doc/api/submodule-dbapi.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.dbapi` +================================ + +.. automodule:: tarantool.dbapi diff --git a/doc/api/submodule-error.rst b/doc/api/submodule-error.rst new file mode 100644 index 00000000..ff12f691 --- /dev/null +++ b/doc/api/submodule-error.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.error` +================================ + +.. automodule:: tarantool.error diff --git a/doc/api/submodule-mesh-connection.rst b/doc/api/submodule-mesh-connection.rst new file mode 100644 index 00000000..2eb59e84 --- /dev/null +++ b/doc/api/submodule-mesh-connection.rst @@ -0,0 +1,50 @@ +module :py:mod:`tarantool.mesh_connection` +========================================== + +.. automodule:: tarantool.mesh_connection + :exclude-members: MeshConnection + + .. autoclass:: tarantool.mesh_connection.MeshConnection + :exclude-members: Error, DatabaseError, InterfaceError, + ConfigurationError, SchemaError, NetworkError, + Warning, DataError, OperationalError, IntegrityError, + InternalError, ProgrammingError, NotSupportedError + + .. autoattribute:: Error + :noindex: + + .. autoattribute:: DatabaseError + :noindex: + + .. autoattribute:: InterfaceError + :noindex: + + .. autoattribute:: ConfigurationError + :noindex: + + .. autoattribute:: SchemaError + :noindex: + + .. autoattribute:: NetworkError + :noindex: + + .. autoattribute:: Warning + :noindex: + + .. autoattribute:: DataError + :noindex: + + .. autoattribute:: OperationalError + :noindex: + + .. autoattribute:: IntegrityError + :noindex: + + .. autoattribute:: InternalError + :noindex: + + .. autoattribute:: ProgrammingError + :noindex: + + .. autoattribute:: NotSupportedError + :noindex: diff --git a/doc/api/submodule-msgpack-ext-types.rst b/doc/api/submodule-msgpack-ext-types.rst new file mode 100644 index 00000000..7c771a9b --- /dev/null +++ b/doc/api/submodule-msgpack-ext-types.rst @@ -0,0 +1,19 @@ +module :py:mod:`tarantool.msgpack_ext.types` +============================================ + +.. currentmodule:: tarantool.msgpack_ext.types + +module :py:mod:`tarantool.msgpack_ext.types.datetime` +----------------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.types.datetime + :special-members: __add__, __sub__, __eq__ + + +.. currentmodule:: tarantool.msgpack_ext.types + +module :py:mod:`tarantool.msgpack_ext.types.interval` +----------------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.types.interval + :special-members: __add__, __sub__, __eq__ diff --git a/doc/api/submodule-msgpack-ext.rst b/doc/api/submodule-msgpack-ext.rst new file mode 100644 index 00000000..173cc84d --- /dev/null +++ b/doc/api/submodule-msgpack-ext.rst @@ -0,0 +1,49 @@ +module :py:mod:`tarantool.msgpack_ext` +====================================== + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.datetime` +----------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.datetime + + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.decimal` +---------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.decimal + + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.interval` +----------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.interval + + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.packer` +--------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.packer + + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.unpacker` +----------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.unpacker + + +.. currentmodule:: tarantool.msgpack_ext + +module :py:mod:`tarantool.msgpack_ext.uuid` +------------------------------------------- + +.. automodule:: tarantool.msgpack_ext.uuid diff --git a/doc/api/submodule-request.rst b/doc/api/submodule-request.rst new file mode 100644 index 00000000..e7b7b507 --- /dev/null +++ b/doc/api/submodule-request.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.request` +================================== + +.. automodule:: tarantool.request diff --git a/doc/api/submodule-response.rst b/doc/api/submodule-response.rst new file mode 100644 index 00000000..328d5a93 --- /dev/null +++ b/doc/api/submodule-response.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.response` +=================================== + +.. automodule:: tarantool.response diff --git a/doc/api/submodule-schema.rst b/doc/api/submodule-schema.rst new file mode 100644 index 00000000..70f579ea --- /dev/null +++ b/doc/api/submodule-schema.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.schema` +================================= + +.. automodule:: tarantool.schema diff --git a/doc/api/submodule-space.rst b/doc/api/submodule-space.rst new file mode 100644 index 00000000..c329bfcd --- /dev/null +++ b/doc/api/submodule-space.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.space` +================================ + +.. automodule:: tarantool.space diff --git a/doc/api/submodule-utils.rst b/doc/api/submodule-utils.rst new file mode 100644 index 00000000..a6351415 --- /dev/null +++ b/doc/api/submodule-utils.rst @@ -0,0 +1,4 @@ +module :py:mod:`tarantool.utils` +================================ + +.. automodule:: tarantool.utils diff --git a/doc/conf.py b/doc/conf.py index 3e96e59e..7bcef61d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -29,7 +29,14 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', + 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx_paramlinks', + 'sphinx-favicon',] + +autodoc_default_options = { + 'members': True, + 'inherited-members': True, +} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -45,7 +52,7 @@ # General information about the project. project = u'Tarantool python client library' -copyright = u'2011, Konstantin Cherkasoff' +copyright = u'2011-2022, tarantool-python AUTHORS' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -93,16 +100,16 @@ # -- Options for HTML output --------------------------------------------------- -html_style = 'tarantool.css' +#html_style = 'style.css' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = {'sidebarwidth': '30%'} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] @@ -123,6 +130,93 @@ # pixels large. #html_favicon = None +# Set up favicons with sphinx-favicon. +favicons = [ + { + "rel": "icon", + "static-file": "favicon/icon.svg", + "type": "image/svg", + }, + { + "rel": "icon", + "sizes": "16x16", + "static-file": "favicon/icon-16x16.png", + "type": "image/png", + }, + { + "rel": "icon", + "sizes": "32x32", + "static-file": "favicon/icon-32x32.png", + "type": "image/png", + }, + { + "rel": "icon", + "sizes": "96x96", + "static-file": "favicon/icon-96x96.png", + "type": "image/png", + }, + { + "rel": "icon", + "sizes": "128x128", + "static-file": "favicon/icon-128x128.png", + "type": "image/png", + }, + { + "rel": "icon", + "sizes": "196x196", + "static-file": "favicon/icon-196x196.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "57x57", + "static-file": "favicon/apple-touch-icon-57x57.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "60x60", + "static-file": "favicon/apple-touch-icon-60x60.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "72x72", + "static-file": "favicon/apple-touch-icon-72x72.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "76x76", + "static-file": "favicon/apple-touch-icon-76x76.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "114x114", + "static-file": "favicon/apple-touch-icon-114x114.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "120x120", + "static-file": "favicon/apple-touch-icon-120x120.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "144x144", + "static-file": "favicon/apple-touch-icon-144x144.png", + "type": "image/png", + }, + { + "rel": "apple-touch-icon", + "sizes": "152x152", + "static-file": "favicon/apple-touch-icon-152x152.png", + "type": "image/png", + }, +] + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". @@ -190,7 +284,7 @@ # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Tarantoolpythonclientlibrary.tex', u'Tarantool python client library Documentation', - u'Konstantin Cherkasoff', 'manual'), + u'tarantool-python AUTHORS', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -220,7 +314,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'tarantoolpythonclientlibrary', u'Tarantool python client library Documentation', - [u'Konstantin Cherkasoff'], 1) + [u'tarantool-python AUTHORS'], 1) ] # If true, show URL addresses after external links. @@ -234,7 +328,7 @@ # dir menu entry, description, category) texinfo_documents = [ ('index', 'Tarantoolpythonclientlibrary', u'Tarantool python client library Documentation', - u'Konstantin Cherkasoff', 'Tarantoolpythonclientlibrary', 'One line description of project.', + u'tarantool-python AUTHORS', 'Tarantoolpythonclientlibrary', 'One line description of project.', 'Miscellaneous'), ] @@ -249,7 +343,12 @@ # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'python':('http://docs.python.org/', None)} +intersphinx_mapping = { + 'python': ('http://docs.python.org/', None), + 'msgpack': ('https://msgpack-python.readthedocs.io/en/latest/', None), + 'pandas': ('https://pandas.pydata.org/docs/', None), + 'pytz': ('https://pytz.sourceforge.net/', None), +} autoclass_content = "both" diff --git a/doc/dev-guide.rst b/doc/dev-guide.rst new file mode 100644 index 00000000..a3f7d704 --- /dev/null +++ b/doc/dev-guide.rst @@ -0,0 +1,113 @@ +.. encoding: utf-8 + +Developer's guide +================= + +Tarantool database basic concepts +--------------------------------- + +To understand, what is "space", "tuple" and what basic operations are, +refer to `Tarantool data model documentation`_. + +Field types +----------- + +Tarantool uses `MessagePack`_ as a format for receiving requests and sending +responses. Refer to `Lua versus MessagePack`_ to see how types are encoded +and decoded. + +While working with Tarantool from Python with this connector, +each request data is encoded to MessagePack and each response data +is decoded from MessagePack with the `Python MessagePack`_ module. See its +documentation to explore how basic types are encoded and decoded. + +There are several cases when you may tune up the behavior. +Use :class:`tarantool.Connection` parameters to set Python MessagePack +module options. + +Use :paramref:`~tarantool.Connection.params.encoding` to tune +behavior for string encoding. + +``encoding='utf-8'`` (default): + + +--------------+----+----------------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+==================================+====+==============+ + | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`str` | + +--------------+----+----------------------------------+----+--------------+ + | :obj:`bytes` | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + +``encoding=None`` (work with non-UTF8 strings): + + +--------------+----+----------------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+==================================+====+==============+ + | :obj:`bytes` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + | | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + +Use :paramref:`~tarantool.Connection.params.use_list` to tune +behavior for `mp_array`_ (Lua ``table``) decoding. + +``use_list='True'`` (default): + + +--------------+----+-----------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+=============================+====+==============+ + | :obj:`list` | -> | `mp_array`_ (``table``) | -> | :obj:`list` | + +--------------+----+-----------------------------+----+--------------+ + | :obj:`tuple` | -> | `mp_array`_ (``table``) | -> | :obj:`list` | + +--------------+----+-----------------------------+----+--------------+ + +``use_list='False'``: + + +--------------+----+-----------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+=============================+====+==============+ + | :obj:`list` | -> | `mp_array`_ (``table``) | -> | :obj:`tuple` | + +--------------+----+-----------------------------+----+--------------+ + | :obj:`tuple` | -> | `mp_array`_ (``table``) | -> | :obj:`tuple` | + +--------------+----+-----------------------------+----+--------------+ + +Tarantool implements several `extension types`_. In Python, +they are represented with in-built and custom types: + + +-----------------------------+----+-------------+----+-----------------------------+ + | Python | -> | Tarantool | -> | Python | + +=============================+====+=============+====+=============================+ + | :obj:`decimal.Decimal` | -> | `DECIMAL`_ | -> | :obj:`decimal.Decimal` | + +-----------------------------+----+-------------+----+-----------------------------+ + | :obj:`uuid.UUID` | -> | `UUID`_ | -> | :obj:`uuid.UUID` | + +-----------------------------+----+-------------+----+-----------------------------+ + | :class:`tarantool.Datetime` | -> | `DATETIME`_ | -> | :class:`tarantool.Datetime` | + +-----------------------------+----+-------------+----+-----------------------------+ + | :class:`tarantool.Interval` | -> | `INTERVAL`_ | -> | :class:`tarantool.Interval` | + +-----------------------------+----+-------------+----+-----------------------------+ + +Request response +---------------- + +Server requests (except for :meth:`~tarantool.Connection.ping`) +return :class:`~tarantool.response.Response` instance in case +of success. + +:class:`~tarantool.response.Response` is inherited from +:class:`collections.abc.Sequence`, so you can index response data +and iterate through it as with any other serializable object. + +.. _Tarantool data model documentation: https://www.tarantool.io/en/doc/latest/concepts/data_model/ +.. _MessagePack: https://msgpack.org/ +.. _Lua versus MessagePack: https://www.tarantool.io/en/doc/latest/concepts/data_model/value_store/#lua-versus-msgpack +.. _Python MessagePack: https://pypi.org/project/msgpack/ +.. _mp_str: https://github.com/msgpack/msgpack/blob/master/spec.md#str-format-family +.. _mp_bin: https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family +.. _mp_array: https://github.com/msgpack/msgpack/blob/master/spec.md#array-format-family +.. _extension types: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +.. _DECIMAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type +.. _UUID: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type +.. _DATETIME: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type +.. _INTERVAL: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type diff --git a/doc/guide.en.rst b/doc/guide.en.rst deleted file mode 100644 index d4d8aab9..00000000 --- a/doc/guide.en.rst +++ /dev/null @@ -1,286 +0,0 @@ -.. encoding: utf-8 - -Developer's guide -================= - -Basic concepts --------------- - -Spaces -^^^^^^ - -A space is a collection of tuples. -Usually, tuples in one space represent objects of the same type, -although not necessarily. - -.. note:: Spaces are analogous to tables in traditional (SQL) databases. - -Spaces have integer identifiers defined in the server configuration. -One of the ways to access a space as a named object is by using the method -:meth:`Connection.space() ` -and an instance of :class:`~tarantool.space.Space`. - -Example:: - - >>> customer = connection.space(0) - >>> customer.insert(('FFFF', 'Foxtrot')) - - -Field types -^^^^^^^^^^^ - -Three field types are supported in Tarantool: ``STR``, ``NUM``, and ``NUM64``. -These types are used only for index configuration. -They are neither saved in the tuple data nor transferred between the client and the server. -Thus, from the client point of view, fields are raw byte arrays -without explicitly defined types. - -For a Python developer, it is much easier to use native types: -``int``, ``long``, ``unicode`` (``int`` and ``str`` for Python 3.x). -For raw binary data, use ``bytes`` (in this case, type casting is not performed). - -Tarantool data types corresponds to the following Python types: - • ``RAW`` - ``bytes`` - • ``STR`` - ``unicode`` (``str`` for Python 3.x) - • ``NUM`` - ``int`` - • ``NUM64`` - ``int`` or ``long`` (``int`` for Python 3.x) - -To enable automatic type casting, please define a schema for the spaces: - - >>> import tarantool - >>> schema = { - 0: { # Space description - 'name': 'users', # Space name - 'default_type': tarantool.STR, # Type that is used to decode fields not listed below - 'fields': { - 0: ('numfield', tarantool.NUM), # (field name, field type) - 1: ('num64field', tarantool.NUM64), - 2: ('strfield', tarantool.STR), - #2: { 'name': 'strfield', 'type': tarantool.STR }, # Alternative syntax - #2: tarantool.STR # Alternative syntax - }, - 'indexes': { - 0: ('pk', [0]), # (name, [field_no]) - #0: { 'name': 'pk', 'fields': [0]}, # Alternative syntax - #0: [0], # Alternative syntax - } - } - } - >>> connection = tarantool.connect(host = 'localhost', port=33013, schema = schema) - >>> demo = connection.space('users') - >>> demo.insert((0, 12, u'this is a unicode string')) - >>> demo.select(0) - [(0, 12, u'this is a unicode string')] - -As you can see, original "raw" fields were cast to native types as defined in the schema. - -A Tarantool tuple can contain any number of fields. -If some fields are not defined, then ``default_type`` will be used. - -To prevent implicit type casting for strings, use the ``RAW`` type. -Raw byte fields should be used if the application uses binary data -(like images or Python objects packed with ``pickle``). - -You can also specify a schema for CALL results: - - >>> ... - # Copy schema decription from the 'users' space - >>> connection.call("box.select", '0', '0', 0L, space_name='users'); - [(0, 12, u'this is unicode string')] - # Provide schema description explicitly - >>> field_defs = [('numfield', tarantool.NUM), ('num64field', tarantool.NUM)] - >>> connection.call("box.select", '0', '1', 184L, field_defs = field_defs, default_type = tarantool.STR); - [(0, 12, u'this is unicode string')] - -.. note:: - - Python 2.6 adds :class:`bytes` as a synonym for the :class:`str` type, and it also supports the ``b''`` notation. - - -.. note:: **utf-8** is always used for type conversion between ``unicode`` and ``bytes``. - - - -Request response -^^^^^^^^^^^^^^^^ - -Requests (:meth:`insert() `, -:meth:`delete() `, -:meth:`update() `, -:meth:`select() `) return a -:class:`~tarantool.response.Response` instance. - -The class :class:`~tarantool.response.Response` inherits from `list`, -so a response is, in fact, a list of tuples. - -In addition, a :class:`~tarantool.response.Response` instance has the ``rowcount`` attribute. -The value of ``rowcount`` equals to the number of records affected by the request. -For example, for :meth:`delete() `, -the request ``rowcount`` equals to ``1`` if a record was deleted. - - - -Connect to a server -------------------- - -To connect to a server, use the :meth:`tarantool.connect` method. -It returns a :class:`~tarantool.connection.Connection` instance. - -Example:: - - >>> import tarantool - >>> connection = tarantool.connect("localhost", 33013) - >>> type(connection) - - - - -Data manipulation ------------------ - -Tarantool supports four basic operations: -**insert**, **delete**, **update** and **select**. - - -Inserting and replacing records -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To insert or replace records, use the :meth:`Space.insert() ` -method. - -Example:: - - >>> user.insert((user_id, email, int(time.time()))) - -The first element of a tuple is always its unique primary key. - -If an entry with the same key already exists, it will be replaced -without any warning or error message. - -.. note:: For ``insert`` requests, ``Response.rowcount`` always equals ``1``. - - -Deleting records -^^^^^^^^^^^^^^^^ - -To delete records, use the :meth:`Space.delete() ` method. - -Example:: - - >>> user.delete(primary_key) - -.. note:: If the record was deleted, ``Response.rowcount`` equals ``1``. - If the record was not found, ``Response.rowcount`` equals ``0``. - - -Updating records -^^^^^^^^^^^^^^^^ - -An *update* request in Tarantool allows updating multiple -fields of a tuple simultaneously and atomically. - -To update records, use the :meth:`Space.update() ` -method. - -Example:: - - >>> user.update(1001, [('=', 1, 'John'), ('=', 2, 'Smith')]) - -In this example, fields ``1`` and ``2`` are assigned new values. - -The :meth:`Space.update() ` method allows changing -multiple fields of the tuple at a time. - -Tarantool supports the following update operations: - • ``'='`` – assign new value to the field - • ``'+'`` – add argument to the field (*both arguments are treated as signed 32-bit ints*) - • ``'^'`` – bitwise AND (*only for 32-bit integers*) - • ``'|'`` – bitwise XOR (*only for 32-bit integers*) - • ``'&'`` – bitwise OR (*only for 32-bit integers*) - • ``'splice'`` – implementation of `Perl splice `_ - - -.. note:: The 0th field of the tuple cannot be updated, because it is the primary key. - -.. seealso:: See :meth:`Space.update() ` documentation for details. - -.. warning:: The ``'splice'`` operation is not implemented yet. - - -Selecting records -^^^^^^^^^^^^^^^^^ - -To select records, use the :meth:`Space.select() ` method. -A *SELECT* query can return one or many records. - - -.. rubric:: Select by primary key - -Select a record using its primary key, ``3800``:: - - >>> world.select(3800) - [(3800, u'USA', u'Texas', u'Dallas', 1188580)] - - -.. rubric:: Select by a secondary index - -:: - - >>> world.select('USA', index=1) - [(3796, u'USA', u'Texas', u'Houston', 1953631), - (3801, u'USA', u'Texas', u'Huston', 10000), - (3802, u'USA', u'California', u'Los Angeles', 10000), - (3805, u'USA', u'California', u'San Francisco', 776733), - (3800, u'USA', u'Texas', u'Dallas', 1188580), - (3794, u'USA', u'California', u'Los Angeles', 3694820)] - - -The argument ``index=1`` indicates that a secondary index (``1``) should be used. -The primary key (``index=0``) is used by default. - -.. note:: Secondary indexes must be explicitly declared in the server configuration. - - -.. rubric:: Select by several keys - -.. note:: This conforms to ``where key in (k1, k2, k3...)``. - -Select records with primary key values ``3800``, ``3805`` and ``3796``:: - - >>> world.select([3800, 3805, 3796]) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), - (3805, u'USA', u'California', u'San Francisco', 776733), - (3796, u'USA', u'Texas', u'Houston', 1953631)] - - -.. rubric:: Retrieve a record by using a composite index - -Select data on cities in Texas:: - - >>> world.select([('USA', 'Texas')], index=1) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), (3796, u'USA', u'Texas', u'Houston', 1953631)] - - -.. rubric:: Select records by explicitly specifying field types - -Tarantool has no strict schema, so all fields are raw binary byte arrays. -You can specify field types in the ``schema`` parameter of the connection. - -Call server-side functions --------------------------- - -A server-side function written in Lua can select and modify data, -access configuration, and perform administrative tasks. - -To call a stored function, use the -:meth:`Connection.call() ` method. -(This method has an alias, :meth:`Space.call() `.) - -Example:: - - >>> server.call("box.select_range", (1, 3, 2, 'AAAA')) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), (3794, u'USA', u'California', u'Los Angeles', 3694820)] - -.. seealso:: - - Tarantool documentation » `Insert one million tuples with a Lua stored procedure `_ diff --git a/doc/guide.ru.rst b/doc/guide.ru.rst deleted file mode 100644 index 4a7e7bcf..00000000 --- a/doc/guide.ru.rst +++ /dev/null @@ -1,284 +0,0 @@ -.. encoding: utf-8 - -Руководство разработчика -======================== - -Базовые понятия ---------------- - -Спейсы -^^^^^^ - -Спейсы в Tarantool — это коллекции кортежей. -Как правило, кортежи в спейсе представляют собой объекты одного типа, -хотя это и не обязательно. - -.. note:: Аналог спейса — таблица в традиционных (SQL) базах данных. - -Спейсы имеют целочисленные идентификаторы, которые задаются в конфигурации сервера. -Чтобы обращаться к спейсу как к именованному объекту, можно использовать метод -:meth:`Connection.space() ` -и экземпляр класса :class:`~tarantool.space.Space`. - -Пример:: - - >>> customer = connection.space(0) - >>> customer.insert(('FFFF', 'Foxtrot')) - - -Типы полей -^^^^^^^^^^ - -Tarantool поддерживает три типа полей: ``STR``, ``NUM`` и ``NUM64``. -Эти типы используются только при конфигурации индексов, -но не сохраняются с данными кортежа и не передаются между сервером и клиентом. -Таким образом, с точки зрения клиента, поля кортежей — это просто байтовые массивы -без явно заданных типов. - -Для разработчика на Python намного удобнее использовать родные типы: -``int``, ``long``, ``unicode`` (для Python 3.x - ``int`` и ``str``). -Для бинарных данных следует использовать тип ``bytes`` -(в этом случае приведение типов не производится). - -Типы данных Tarantool соответствуют следующим типам Python: - • ``RAW`` - ``bytes`` - • ``STR`` - ``unicode`` (``str`` for Python 3.x) - • ``NUM`` - ``int`` - • ``NUM64`` - ``int`` or ``long`` (``int`` for Python 3.x) - -Для автоматического приведения типов необходимо объявить схему: - >>> import tarantool - >>> schema = { - 0: { # Space description - 'name': 'users', # Space name - 'default_type': tarantool.STR, # Type that is used to decode fields not listed below - 'fields': { - 0: ('user_id', tarantool.NUM), # (field name, field type) - 1: ('num64field', tarantool.NUM64), - 2: ('strfield', tarantool.STR), - #2: { 'name': 'strfield', 'type': tarantool.STR }, # Alternative syntax - #2: tarantool.STR # Alternative syntax - }, - 'indexes': { - 0: ('pk', [0]), # (name, [field_no]) - #0: { 'name': 'pk', 'fields': [0]}, # Alternative syntax - #0: [0], # Alternative syntax - } - } - } - >>> connection = tarantool.connect(host = 'localhost', port=33013, schema = schema) - >>> demo = connection.space('users') - >>> demo.insert((0, 12, u'this is a unicode string')) - >>> demo.select(0) - [(0, 12, u'this is a unicode string')] - -Как видно из примера, все значения были преобразованы в Python-типы в соответствии со схемой. - -Кортеж Tarantool может содержать произвольное количество полей. -Если какие-то поля не объявлены в схеме, то для конвертации будет использован ``default_type``. - -Поля с "сырыми" байтами следует использовать, если приложение работает с -двоичными данными (например, с изображениями или Python-объектами, сохраненными с помощью ``pickle``). - -Возможно также указать тип для CALL запросов: - - >>> ... - # Copy schema decription from 'users' space - >>> connection.call("box.select", '0', '0', 0L, space_name='users'); - [(0, 12, u'this is unicode string')] - # Provide schema description explicitly - >>> field_defs = [('numfield', tarantool.NUM), ('num64field', tarantool.NUM)] - >>> connection.call("box.select", '0', '1', 184L, field_defs = field_defs, default_type = tarantool.STR); - [(0, 12, u'this is unicode string')] - -.. note:: - - Python 2.6 добавляет синоним :class:`bytes` к типу :class:`str` (также поддерживается синтаксис ``b''``). - - -.. note:: Для преобразования между ``bytes`` и ``unicode`` всегда используется **utf-8**. - - - -Результат запроса -^^^^^^^^^^^^^^^^^ - -Запросы (:meth:`insert() `, -:meth:`delete() `, -:meth:`update() `, -:meth:`select() `) возвращают экземпляр -класса :class:`~tarantool.response.Response`. - -Класс :class:`~tarantool.response.Response` унаследован от стандартного типа `list`, -поэтому, по сути, результат всегда представляет собой список кортежей. - -Кроме того, у экземпляра :class:`~tarantool.response.Response` есть атрибут ``rowcount``. -Этот атрибут содержит число записей, которые затронул запроc. -Например, для запроса :meth:`delete() ` -``rowcount`` равен ``1``, если запись была удалена. - - - -Подключение к серверу ---------------------- - -Для подключения к серверу следует использовать метод :meth:`tarantool.connect`. -Он возвращает экземпляр класса :class:`~tarantool.connection.Connection`. - -Пример:: - - >>> import tarantool - >>> connection = tarantool.connect("localhost", 33013) - >>> type(connection) - - - - -Работа с данными ----------------- - -Tarantool поддерживает четыре базовых операции: -**insert**, **delete**, **update** и **select**. - - -Добавление и замещение записей -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Для добавления и замещения записей следует использовать метод -:meth:`Space.insert() `:: - - >>> user.insert((user_id, email, int(time.time()))) - -Первый элемент кортежа — это всегда его уникальный первичный ключ. - -Если запись с таким ключом уже существует, она будет замещена -без какого-либо предупреждения или сообщения об ошибке. - -.. note:: Для :meth:`Space.insert() ` ``Response.rowcount`` всегда равен ``1``. - - -Удаление записей -^^^^^^^^^^^^^^^^ - -Для удаления записей следует использовать метод -:meth:`Space.delete() `:: - - >>> user.delete(primary_key) - -.. note:: ``Response.rowcount`` равен ``1``, если запись была удалена. - Если запись не найдена, то ``Response.rowcount`` равен ``0``. - - -Обновление записей -^^^^^^^^^^^^^^^^^^ - -Запрос *update* в Tarantool позволяет одновременно и атомарно обновить несколько -полей одного кортежа. - -Для обновления записей следует использовать метод -:meth:`Space.update() `. - -Пример:: - - >>> user.update(1001, [(1, '=', 'John'), (2, '=', 'Smith')]) - -В этом примере для полей ``1`` и ``2`` устанавливаются новые значения. - -Метод :meth:`Space.update() ` позволяет обновлять -сразу несколько полей кортежа. - -Tarantool поддерживает следующие операции обновления: - • ``'='`` – установить новое значение поля - • ``'+'`` – прибавить аргумент к значению поля (*оба аргумента рассматриваются как знаковые 32-битные целые числа*) - • ``'^'`` – битовый AND (*только для 32-битных полей*) - • ``'|'`` – битовый XOR (*только для 32-битных полей*) - • ``'&'`` – битовый OR (*только для 32-битных полей*) - • ``'splice'`` – аналог функции `splice в Perl `_ - - -.. note:: Нулевое (т.е. [0]) поле кортежа нельзя обновить, - поскольку оно является первичным ключом. - -.. seealso:: Подробности можно найти в документации по методу :meth:`Space.update() `. - -.. warning:: Операция ``'splice'`` пока не реализована. - - -Выборка записей -^^^^^^^^^^^^^^^ - -Для выборки записей следует использовать метод -:meth:`Space.select() `. -Запрос *SELECT* может возвращать одну или множество записей. - - -.. rubric:: Запрос по первичному ключу - -Извлечь запись по её первичному ключу ``3800``:: - - >>> world.select(3800) - [(3800, u'USA', u'Texas', u'Dallas', 1188580)] - - -.. rubric:: Запрос по вторичному индексу - -:: - - >>> world.select('USA', index=1) - [(3796, u'USA', u'Texas', u'Houston', 1953631), - (3801, u'USA', u'Texas', u'Huston', 10000), - (3802, u'USA', u'California', u'Los Angeles', 10000), - (3805, u'USA', u'California', u'San Francisco', 776733), - (3800, u'USA', u'Texas', u'Dallas', 1188580), - (3794, u'USA', u'California', u'Los Angeles', 3694820)] - - -Аргумент ``index=1`` указывает, что при запросе следует использовать индекс ``1``. -По умолчанию используется первичный ключ (``index=0``). - -.. note:: Вторичные индексы должны быть явно объявлены в конфигурации сервера. - - -.. rubric:: Запрос записей по нескольким ключам - -.. note:: Это аналог ``where key in (k1, k2, k3...)``. - -Извлечь записи со значениями первичного ключа ``3800``, ``3805`` и ``3796``:: - - >>>> world.select([3800, 3805, 3796]) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), - (3805, u'USA', u'California', u'San Francisco', 776733), - (3796, u'USA', u'Texas', u'Houston', 1953631)] - - -.. rubric:: Запрос по составному индексу - -Извлечь данные о городах в Техасе:: - - >>> world.select([('USA', 'Texas')], index=1) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), (3796, u'USA', u'Texas', u'Houston', 1953631)] - - -.. rubric:: Запрос с явным указанием типов полей - -Tarantool не имеет строгой схемы, так что поля кортежей являются просто байтовыми массивами. -Можно указывать типы полей непосредственно в параметре ``schema`` для ```Connection``. - -Вызов хранимых функций ----------------------- - -С помощью хранимых процедур на Lua можно делать выборки и изменять данные, -получать доcтуп к конфигурации и выполнять административные функции. - -Для вызова хранимых функций следует использовать метод -:meth:`Connection.call() `. -Кроме того, у этого метода есть псевдоним: :meth:`Space.call() `. - -Пример:: - - >>> server.call("box.select_range", (1, 3, 2, 'AAAA')) - [(3800, u'USA', u'Texas', u'Dallas', 1188580), (3794, u'USA', u'California', u'Los Angeles', 3694820)] - -.. seealso:: - - Tarantool documentation » `Insert one million tuples with a Lua stored procedure `_ diff --git a/doc/index.rst b/doc/index.rst index 6f4ff0ca..a3be248e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,46 +5,45 @@ Python client library for Tarantool :Version: |version| -.. sidebar:: Download - - * `PyPI`_ - * `GitHub`_ - - **Install** - - .. code-block:: bash - - $ pip install tarantool +`Tarantool`_ is an in-memory computing platform originally designed by +`VK`_ and released under the terms of `BSD license`_. +Install Tarantool Python connector with ``pip`` (`PyPI`_ page): -`Tarantool`_ is a damn fast in-memory computing platform originally designed by -`VK`_ and released under the terms of `BSD license`_. +.. code-block:: bash + $ pip install tarantool +Source code is available on `GitHub`_. Documentation ------------- .. toctree:: :maxdepth: 1 - quick-start.en - guide.en + quick-start + dev-guide .. seealso:: `Tarantool documentation`_ - API Reference ------------- .. toctree:: :maxdepth: 2 api/module-tarantool.rst - api/class-connection.rst - api/class-mesh-connection.rst - api/class-space.rst - api/class-response.rst - - + api/submodule-connection.rst + api/submodule-connection-pool.rst + api/submodule-dbapi.rst + api/submodule-error.rst + api/submodule-mesh-connection.rst + api/submodule-msgpack-ext.rst + api/submodule-msgpack-ext-types.rst + api/submodule-request.rst + api/submodule-response.rst + api/submodule-schema.rst + api/submodule-space.rst + api/submodule-utils.rst .. Indices and tables .. ================== @@ -53,7 +52,6 @@ API Reference .. * :ref:`modindex` .. * :ref:`search` - .. _`Tarantool`: .. _`Tarantool homepage`: https://tarantool.io .. _`Tarantool documentation`: https://www.tarantool.io/en/doc/latest/ diff --git a/doc/index.ru.rst b/doc/index.ru.rst deleted file mode 100644 index 562dd6c6..00000000 --- a/doc/index.ru.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. encoding: utf-8 - -Клиентская библиотека для платформы Tarantool -============================================= - -:Версия: |version| - -.. sidebar:: Загрузить - - * `PyPI`_ - * `GitHub`_ - - **Установить** - - .. code-block:: none - - $ pip install tarantool - - -`Tarantool`_ – это очень быстрая платформа in-memory-вычислений. -Изначально разработана в `VK`_ и выпущена под лицензией `BSD`_. - - - -Документация ------------- -.. toctree:: - :maxdepth: 1 - - quick-start.ru - guide.ru - -.. seealso:: `Документация Tarantool`_ - - -Справочник по API ------------------ -.. toctree:: - :maxdepth: 2 - - api/module-tarantool.rst - api/class-connection.rst - api/class-mesh-connection.rst - api/class-space.rst - api/class-response.rst - - - -.. Indices and tables -.. ================== -.. -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` - - - -.. _`Tarantool`: -.. _`Tarantool homepage`: https://tarantool.io -.. _`Документация Tarantool`: https://www.tarantool.io/en/doc/latest/ -.. _`VK`: https://vk.company -.. _`BSD`: -.. _`BSD license`: http://www.gnu.org/licenses/license-list.html#ModifiedBSD -.. _`PyPI`: http://pypi.python.org/pypi/tarantool -.. _`GitHub`: https://github.com/coxx/tarantool-python diff --git a/doc/quick-start.en.rst b/doc/quick-start.en.rst deleted file mode 100644 index 4f024ef3..00000000 --- a/doc/quick-start.en.rst +++ /dev/null @@ -1,97 +0,0 @@ -Quick start -=========== - -Connecting to the server ------------------------- - -Create a connection to the server:: - - >>> import tarantool - >>> server = tarantool.connect("localhost", 33013) - - -Creating a space instance -------------------------- - -An instance of :class:`~tarantool.space.Space` is a named object to access -the key space. - -Create a ``demo`` object that will be used to access the space ``cool_space`` :: - - >>> demo = server.space(cool_space) - -All subsequent operations with ``cool_space`` are performed using the methods of ``demo``. - - -Data manipulation ------------------ - -Select -^^^^^^ - -Select one single record with id ``'AAAA'`` from the space ``demo`` -using primary key (index zero):: - - >>> demo.select('AAAA') - -Select several records using primary index:: - - >>> demo.select(['AAAA', 'BBBB', 'CCCC']) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo'), ('CCCC', 'Charlie')] - - -Insert -^^^^^^ - -Insert the tuple ``('DDDD', 'Delta')`` into the space ``demo``:: - - >>> demo.insert(('DDDD', 'Delta')) - -The first element is the primary key for the tuple. - - -Update -^^^^^^ - -Update the record with id ``'DDDD'`` placing the value ``'Denver'`` -into the field ``1``:: - - >>> demo.update('DDDD', [(1, '=', 'Denver')]) - [('DDDD', 'Denver')] - -To find the record, :meth:`~tarantool.space.Space.update` always uses -the primary index. -Field numeration starts from zero, so the field ``0`` is the first element in the tuple. - - -Delete -^^^^^^ - -Delete a single record identified by id ``'DDDD'``:: - - >>> demo.delete('DDDD') - [('DDDD', 'Denver')] - -To find the record, :meth:`~tarantool.space.Space.delete` always uses -the primary index. - - -Call server-side functions --------------------------- - -One of the ways to call a stored function is using -:meth:`Connection.call() `:: - - >>> server.call("box.select_range", (0, 0, 2, 'AAAA')) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo')] - -Another way is using -:meth:`Space.call() `:: - - >>> demo = server.space(``cool_space``) - >>> demo.call("box.select_range", (0, 0, 2, 'AAAA')) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo')] - -The method :meth:`Space.call() ` is just -an alias for -:meth:`Connection.call() `. diff --git a/doc/quick-start.rst b/doc/quick-start.rst new file mode 100644 index 00000000..0f22a7bf --- /dev/null +++ b/doc/quick-start.rst @@ -0,0 +1,202 @@ +Quick start +=========== + +Connecting to the server +------------------------ + +Create a connection to the server: + +.. code-block:: python + + >>> import tarantool + >>> conn = tarantool.Connection('localhost', 3301, user='user', password='pass') + +Data manipulation +----------------- + +Select +^^^^^^ + +:meth:`~tarantool.Connection.select` a tuple with id ``'AAAA'`` from +the space ``demo`` using primary index: + +.. code-block:: python + + >>> resp = conn.select('demo', 'AAAA') + >>> len(resp) + 1 + >>> resp[0] + ['AAAA', 'Alpha'] + +:meth:`~tarantool.Connection.select` a tuple with secondary index +key ``'Alpha'`` from the space ``demo`` with secondary index ``sec``: + +.. code-block:: python + + >>> resp = conn.select('demo', 'Alpha', index='sec') + >>> resp + - ['AAAA', 'Alpha'] + +Insert +^^^^^^ + +:meth:`~tarantool.Connection.insert` the tuple ``('BBBB', 'Bravo')`` +into the space ``demo``: + +.. code-block:: python + + >>> conn.insert('demo', ('BBBB', 'Bravo')) + - ['BBBB', 'Bravo'] + +Throws an error if there is already a tuple with the same primary key. + +.. code-block:: python + + >>> try: + ... conn.insert('demo', ('BBBB', 'Bravo')) + ... except Exception as exc: + ... print(exc) + ... + (3, 'Duplicate key exists in unique index "pk" in space "demo" with old tuple - ["BBBB", "Bravo"] and new tuple - ["BBBB", "Bravo"]') + +Replace +^^^^^^^ + +:meth:`~tarantool.Connection.replace` inserts the tuple +``('CCCC', 'Charlie')`` into the space ``demo``, if there is no tuple +with primary key ``'CCCC'``: + +.. code-block:: python + + >>> conn.replace('demo', ('CCCC', 'Charlie')) + - ['CCCC', 'Charlie'] + +If there is already a tuple with the same primary key, replaces it: + +.. code-block:: python + + >>> conn.replace('demo', ('CCCC', 'Charlie-2')) + - ['CCCC', 'Charlie-2'] + +Update +^^^^^^ + +:meth:`~tarantool.Connection.update` the tuple with id ``'BBBB'`` placing +the value ``'Bravo-2'`` into the field ``1``: + +.. code-block:: python + + >>> conn.update('demo', 'BBBB', [('=', 1, 'Bravo-2')]) + - ['BBBB', 'Bravo-2'] + +Field numeration starts from zero, so the field ``0`` is the first element +in the tuple. Tarantool 2.3.1 and newer supports field name identifiers. + +Upsert +^^^^^^ + +:meth:`~tarantool.Connection.upsert` inserts the tuple, if tuple with +id ``'DDDD'`` not exists. Otherwise, updates tuple fields. + +.. code-block:: python + + >>> conn.upsert('demo', ('DDDD', 'Delta'), [('=', 1, 'Delta-2')]) + + >>> conn.select('demo', 'DDDD') + - ['DDDD', 'Delta'] + >>> conn.upsert('demo', ('DDDD', 'Delta'), [('=', 1, 'Delta-2')]) + + >>> conn.select('demo', 'DDDD') + - ['DDDD', 'Delta-2'] + +Delete +^^^^^^ + +:meth:`~tarantool.Connection.delete` a tuple identified by id ``'AAAA'``: + +.. code-block:: python + + >>> conn.delete('demo', 'AAAA') + - [('AAAA', 'Alpha')] + +Creating a space instance +------------------------- + +An instance of :class:`~tarantool.space.Space` is a named object to access +the key space. + +Create a ``demo`` object that will be used to access the space +with id ``'demo'``: + +.. code-block:: python + + >>> demo = conn.space('demo') + +You can use the space instance to do data manipulations without +specifying space id. + +.. code-block:: python + + >>> demo.select('AAAA') + - ['AAAA', 'Alpha'] + >>> demo.insert(('BBBB', 'Bravo')) + - ['BBBB', 'Bravo'] + +Call server-side functions +-------------------------- + +:meth:`~tarantool.Connection.call` a stored Lua procedure: + +.. code-block:: python + + >>> conn.call("my_add", (1, 2)) + - 3 + +Evaluate Lua code +----------------- + +:meth:`~tarantool.Connection.eval` arbitrary Lua code on a server: + +.. code-block:: python + + >>> lua_code = r""" + ... local a, b = ... + ... return a + b + ... """ + >>> conn.eval(lua_code, (1, 2)) + - 3 + +Execute SQL query +----------------- + +:meth:`~tarantool.Connection.execute` SQL query on a Tarantool server: + +.. code-block:: python + + >>> conn.execute('insert into "demo" values (:id, :name)', {'id': 'BBBB', 'name': 'Bravo'}) + + +Connecting to a cluster of servers +---------------------------------- + +Create a connection to several servers: + +.. code-block:: python + + >>> import tarantool + >>> conn = tarantool.ConnectionPool( + ... [{'host':'localhost', 'port':3301}, + ... {'host':'localhost', 'port':3302}], + ... user='user', password='pass') + +:class:`~tarantool.ConnectionPool` is best suited to work with +a single replicaset. Its API is the same as a single server +:class:`~tarantool.Connection`, but requests support ``mode`` +parameter (a :class:`tarantool.Mode` value) to choose between +read-write and read-only pool instances: + +.. code-block:: python + + >>> resp = conn.select('demo', 'AAAA', mode=tarantool.Mode.PREFER_RO) + >>> resp + - ['AAAA', 'Alpha'] diff --git a/doc/quick-start.ru.rst b/doc/quick-start.ru.rst deleted file mode 100644 index 5c9c0170..00000000 --- a/doc/quick-start.ru.rst +++ /dev/null @@ -1,97 +0,0 @@ -Краткое руководство -=================== - -Подключение к серверу ---------------------- - -Создаем подключение к серверу:: - - >>> import tarantool - >>> server = tarantool.connect("localhost", 33013) - - -Создаем объект доступа к спейсу -------------------------------- - -Экземпляр :class:`~tarantool.space.Space` — это именованный объект для доступа -к спейсу ключей. - -Создаем объект ``demo``, который будет использоваться для доступа к спейсу ``cool_space``:: - - >>> demo = server.space(cool_space) - -Все последующие операции с ``cool_space`` выполняются при помощи методов объекта ``demo``. - - -Работа с данными ----------------- - -Select -^^^^^^ - -Извлечь одну запись с id ``'AAAA'`` из спейса ``demo`` -по первичному ключу (нулевой индекс):: - - >>> demo.select('AAAA') - -Извлечь несколько записей, используя первичный индекс:: - - >>> demo.select(['AAAA', 'BBBB', 'CCCC']) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo'), ('CCCC', 'Charlie')] - - -Insert -^^^^^^ - -Вставить кортеж ``('DDDD', 'Delta')`` в спейс ``demo``:: - - >>> demo.insert(('DDDD', 'Delta')) - -Первый элемент является первичным ключом для этого кортежа. - - -Update -^^^^^^ - -Обновить запись с id ``'DDDD'``, поместив значение ``'Denver'`` -в поле ``1``:: - - >>> demo.update('DDDD', [(1, '=', 'Denver')]) - [('DDDD', 'Denver')] - -Для поиска записи :meth:`~tarantool.space.Space.update` всегда использует -первичный индекс. -Номера полей начинаются с нуля. -Таким образом, поле ``0`` — это первый элемент кортежа. - - -Delete -^^^^^^ - -Удалить одиночную запись с идентификатором ``'DDDD'``:: - - >>> demo.delete('DDDD') - [('DDDD', 'Denver')] - -Для поиска записи :meth:`~tarantool.space.Space.delete` всегда использует -первичный индекс. - - -Вызов хранимых функций ----------------------- - -Для вызова хранимых функций можно использовать метод -:meth:`Connection.call() `:: - - >>> server.call("box.select_range", (0, 0, 2, 'AAAA')) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo')] - -То же самое можно получить при помощи метода -:meth:`Space.call() `:: - - >>> demo.call("box.select_range", (0, 0, 2, 'AAAA')) - [('AAAA', 'Alpha'), ('BBBB', 'Bravo')] - -Метод :meth:`Space.call() ` — это просто -псевдоним для -:meth:`Connection.call() ` diff --git a/requirements-doc.txt b/requirements-doc.txt new file mode 100644 index 00000000..964c9f18 --- /dev/null +++ b/requirements-doc.txt @@ -0,0 +1,3 @@ +sphinx==5.2.1 +sphinx-paramlinks==0.5.4 +sphinx-favicon==0.2 diff --git a/setup.py b/setup.py index 3ee55f05..4aa0e7f1 100755 --- a/setup.py +++ b/setup.py @@ -66,8 +66,8 @@ def find_version(*file_paths): package_dir={"tarantool": os.path.join("tarantool")}, version=find_version('tarantool', '__init__.py'), platforms=["all"], - author="Konstantin Cherkasoff", - author_email="k.cherkasoff@gmail.com", + author="tarantool-python AUTHORS", + author_email="admin@tarantool.org", url="https://github.com/tarantool/tarantool-python", license="BSD", description="Python client library for Tarantool 1.6 Database", diff --git a/tarantool/__init__.py b/tarantool/__init__.py index 9cef1d01..307c72ff 100644 --- a/tarantool/__init__.py +++ b/tarantool/__init__.py @@ -49,16 +49,40 @@ def connect(host="localhost", port=33013, user=None, password=None, ssl_cert_file=DEFAULT_SSL_CERT_FILE, ssl_ca_file=DEFAULT_SSL_CA_FILE, ssl_ciphers=DEFAULT_SSL_CIPHERS): - ''' + """ Create a connection to the Tarantool server. - :param str host: Server hostname or IP-address - :param int port: Server port + :param host: Refer to :paramref:`~tarantool.Connection.params.host`. - :rtype: :class:`~tarantool.connection.Connection` + :param port: Refer to :paramref:`~tarantool.Connection.params.port`. - :raise: `NetworkError` - ''' + :param user: Refer to :paramref:`~tarantool.Connection.params.user`. + + :param password: Refer to + :paramref:`~tarantool.Connection.params.password`. + + :param encoding: Refer to + :paramref:`~tarantool.Connection.params.encoding`. + + :param transport: Refer to + :paramref:`~tarantool.Connection.params.transport`. + + :param ssl_key_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_key_file`. + + :param ssl_cert_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_cert_file`. + + :param ssl_ca_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_ca_file`. + + :param ssl_ciphers: Refer to + :paramref:`~tarantool.Connection.params.ssl_ciphers`. + + :rtype: :class:`~tarantool.Connection` + + :raise: :class:`~tarantool.Connection` exceptions + """ return Connection(host, port, user=user, @@ -77,15 +101,25 @@ def connect(host="localhost", port=33013, user=None, password=None, def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None, password=None, encoding=ENCODING_DEFAULT): - ''' - Create a connection to a mesh of Tarantool servers. + """ + Create a connection to a cluster of Tarantool servers. + + :param addrs: Refer to + :paramref:`~tarantool.MeshConnection.params.addrs`. + + :param user: Refer to + :paramref:`~tarantool.MeshConnection.params.user`. + + :param password: Refer to + :paramref:`~tarantool.MeshConnection.params.password`. - :param list addrs: List of maps: {'host':(HOSTNAME|IP_ADDR), 'port':PORT}. + :param encoding: Refer to + :paramref:`~tarantool.MeshConnection.params.encoding`. - :rtype: :class:`~tarantool.mesh_connection.MeshConnection` + :rtype: :class:`~tarantool.MeshConnection` - :raise: `NetworkError` - ''' + :raise: :class:`~tarantool.MeshConnection` exceptions + """ return MeshConnection(addrs=addrs, user=user, diff --git a/tarantool/connection.py b/tarantool/connection.py index eb11ab5a..f971367e 100644 --- a/tarantool/connection.py +++ b/tarantool/connection.py @@ -1,7 +1,7 @@ # pylint: disable=C0301,W0105,W0401,W0614 -''' -This module provides low-level API for Tarantool -''' +""" +This module provides API for interaction with a Tarantool server. +""" import os import time @@ -86,6 +86,15 @@ # Based on https://realpython.com/python-interface/ class ConnectionInterface(metaclass=abc.ABCMeta): + """ + Represents a connection to single or multiple Tarantool servers. + + Interface requires that a connection object has methods to open and + close a connection, check its status, call procedures and evaluate + Lua code on server, make simple data manipulations and execute SQL + queries. + """ + @classmethod def __subclasshook__(cls, subclass): return (hasattr(subclass, 'close') and @@ -118,80 +127,211 @@ def __subclasshook__(cls, subclass): @abc.abstractmethod def close(self): + """ + Reference implementation: :meth:`~tarantool.Connection.close`. + """ + raise NotImplementedError @abc.abstractmethod def is_closed(self): + """ + Reference implementation + :meth:`~tarantool.Connection.is_closed`. + """ + raise NotImplementedError @abc.abstractmethod def connect(self): + """ + Reference implementation: :meth:`~tarantool.Connection.connect`. + """ + raise NotImplementedError @abc.abstractmethod - def call(self, func_name, *args, **kwargs): + def call(self, func_name, *args): + """ + Reference implementation: :meth:`~tarantool.Connection.call`. + """ + raise NotImplementedError @abc.abstractmethod - def eval(self, expr, *args, **kwargs): + def eval(self, expr, *args): + """ + Reference implementation: :meth:`~tarantool.Connection.eval`. + """ + raise NotImplementedError @abc.abstractmethod def replace(self, space_name, values): + """ + Reference implementation: :meth:`~tarantool.Connection.replace`. + """ + raise NotImplementedError @abc.abstractmethod def insert(self, space_name, values): + """ + Reference implementation: :meth:`~tarantool.Connection.insert`. + """ + raise NotImplementedError @abc.abstractmethod - def delete(self, space_name, key, **kwargs): + def delete(self, space_name, key, *, index=None): + """ + Reference implementation: :meth:`~tarantool.Connection.delete`. + """ + raise NotImplementedError @abc.abstractmethod - def upsert(self, space_name, tuple_value, op_list, **kwargs): + def upsert(self, space_name, tuple_value, op_list, *, index=None): + """ + Reference implementation: :meth:`~tarantool.Connection.upsert`. + """ + raise NotImplementedError @abc.abstractmethod - def update(self, space_name, key, op_list, **kwargs): + def update(self, space_name, key, op_list, *, index=None): + """ + Reference implementation: :meth:`~tarantool.Connection.update`. + """ + raise NotImplementedError @abc.abstractmethod def ping(self, notime): + """ + Reference implementation: :meth:`~tarantool.Connection.ping`. + """ + raise NotImplementedError @abc.abstractmethod - def select(self, space_name, key, **kwargs): + def select(self, space_name, key, *, offset=None, limit=None, + index=None, iterator=None): + """ + Reference implementation: :meth:`~tarantool.Connection.select`. + """ + raise NotImplementedError @abc.abstractmethod - def execute(self, query, params, **kwargs): + def execute(self, query, params): + """ + Reference implementation: :meth:`~tarantool.Connection.execute`. + """ + raise NotImplementedError class Connection(ConnectionInterface): - ''' - Represents connection to the Tarantool server. - - This class is responsible for connection and network exchange with - the server. - It also provides a low-level interface for data manipulation - (insert/delete/update/select). - ''' + """ + Represents a connection to the Tarantool server. + + A connection object has methods to open and close a connection, + check its status, call procedures and evaluate Lua code on server, + make simple data manipulations and execute SQL queries. + """ + # DBAPI Extension: supply exceptions as attributes on the connection Error = Error + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.Error` + """ + DatabaseError = DatabaseError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.DatabaseError` + """ + InterfaceError = InterfaceError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.InterfaceError` + """ + ConfigurationError = ConfigurationError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.ConfigurationError` + """ + SchemaError = SchemaError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.SchemaError` + """ + NetworkError = NetworkError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.NetworkError` + """ + Warning = Warning + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.Warning` + """ + DataError = DataError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.DataError` + """ + OperationalError = OperationalError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.OperationalError` + """ + IntegrityError = IntegrityError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.IntegrityError` + """ + InternalError = InternalError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.InternalError` + """ + ProgrammingError = ProgrammingError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.ProgrammingError` + """ + NotSupportedError = NotSupportedError + """ + DBAPI compatibility. + + :value: :exc:`~tarantool.error.NotSupportedError` + """ def __init__(self, host, port, user=None, @@ -209,24 +349,119 @@ def __init__(self, host, port, ssl_cert_file=DEFAULT_SSL_CERT_FILE, ssl_ca_file=DEFAULT_SSL_CA_FILE, ssl_ciphers=DEFAULT_SSL_CIPHERS): - ''' - Initialize a connection to the server. - - :param str host: server hostname or IP address - :param int port: server port - :param bool connect_now: if True (default), __init__() actually - creates a network connection; if False, you have to call - connect() manually - :param str transport: set to `ssl` to enable - SSL encryption for a connection. - SSL encryption requires Python >= 3.5 - :param str ssl_key_file: path to the private SSL key file - :param str ssl_cert_file: path to the SSL certificate file - :param str ssl_ca_file: path to the trusted certificate authority - (CA) file - :param str ssl_ciphers: colon-separated (:) list of SSL cipher suites - the connection can use - ''' + """ + :param host: Server hostname or IP address. Use ``None`` for + Unix sockets. + :type host: :obj:`str` or :obj:`None` + + :param port: Server port or Unix socket path. + :type port: :obj:`int` or :obj:`str` + + :param user: User name for authentication on the Tarantool + server. + :type user: :obj:`str` or :obj:`None`, optional + + :param password: User password for authentication on the + Tarantool server. + :type password: :obj:`str` or :obj:`None`, optional + + :param socket_timeout: Timeout on blocking socket operations, + in seconds (see `socket.settimeout()`_). + :type socket_timeout: :obj:`float` or :obj:`None`, optional + + :param reconnect_max_attempts: Count of maximum attempts to + reconnect on API call if connection is lost. + :type reconnect_max_attempts: :obj:`int`, optional + + :param reconnect_delay: Delay between attempts to reconnect on + API call if connection is lost, in seconds. + :type reconnect_delay: :obj:`float`, optional + + :param bool connect_now: If ``True``, connect to server on + initialization. Otherwise, you have to call + :meth:`~tarantool.Connection.connect` manually after + initialization. + :type connect_now: :obj:`bool`, optional + + :param encoding: ``'utf-8'`` or ``None``. Use ``None`` to work + with non-UTF8 strings. + + If ``'utf-8'``, pack Unicode string (:obj:`str`) to + MessagePack string (`mp_str`_) and unpack MessagePack string + (`mp_str`_) Unicode string (:obj:`str`), pack :obj:`bytes` + to MessagePack binary (`mp_bin`_) and unpack MessagePack + binary (`mp_bin`_) to :obj:`bytes`. + + +--------------+----+----------------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+==================================+====+==============+ + | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`str` | + +--------------+----+----------------------------------+----+--------------+ + | :obj:`bytes` | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + + If ``None``, pack Unicode string (:obj:`str`) and + :obj:`bytes` to MessagePack string (`mp_str`_), unpack + MessagePack string (`mp_str`_) and MessagePack binary + (`mp_bin`_) to :obj:`bytes`. + + +--------------+----+----------------------------------+----+--------------+ + | Python | -> | MessagePack (Tarantool/Lua) | -> | Python | + +==============+====+==================================+====+==============+ + | :obj:`bytes` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + | :obj:`str` | -> | `mp_str`_ (``string``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + | | -> | `mp_bin`_ (``binary``/``cdata``) | -> | :obj:`bytes` | + +--------------+----+----------------------------------+----+--------------+ + + :type encoding: :obj:`str` or :obj:`None`, optional + + :param use_list: + If ``True``, unpack MessagePack array (`mp_array`_) to + :obj:`list`. Otherwise, unpack to :obj:`tuple`. + :type use_list: :obj:`bool`, optional + + :param call_16: + If ``True``, enables compatibility mode with Tarantool 1.6 + and older for `call` operations. + :type call_16: :obj:`bool`, optional + + :param connection_timeout: Time to establish initial socket + connection, in seconds. + :type connection_timeout: :obj:`float` or :obj:`None`, optional + + :param transport: ``''`` or ``'ssl'``. Set to ``'ssl'`` to + enable SSL encryption for a connection (requires + Python >= 3.5). + :type transport: :obj:`str`, optional + + :param ssl_key_file: Path to a private SSL key file. Mandatory, + if the server uses a trusted certificate authorities (CA) + file. + :type ssl_key_file: :obj:`str` or :obj:`None`, optional + + :param str ssl_cert_file: Path to a SSL certificate file. + Mandatory, if the server uses a trusted certificate + authorities (CA) file. + :type ssl_cert_file: :obj:`str` or :obj:`None`, optional + + :param ssl_ca_file: Path to a trusted certificate authority (CA) + file. + :type ssl_ca_file: :obj:`str` or :obj:`None`, optional + + :param ssl_ciphers: Colon-separated (``:``) list of SSL cipher + suites the connection can use. + :type ssl_ciphers: :obj:`str` or :obj:`None`, optional + + :raise: :exc:`~tarantool.error.ConfigurationError`, + :meth:`~tarantool.Connection.connect` exceptions + + .. _socket.settimeout(): https://docs.python.org/3/library/socket.html#socket.socket.settimeout + .. _mp_str: https://github.com/msgpack/msgpack/blob/master/spec.md#str-format-family + .. _mp_bin: https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family + .. _mp_array: https://github.com/msgpack/msgpack/blob/master/spec.md#array-format-family + """ if msgpack.version >= (1, 0, 0) and encoding not in (None, 'utf-8'): raise ConfigurationError("msgpack>=1.0.0 only supports None and " + @@ -267,31 +502,46 @@ def __init__(self, host, port, self.connect() def close(self): - ''' + """ Close a connection to the server. - ''' + """ + self._socket.close() self._socket = None def is_closed(self): - ''' - Returns the state of a Connection instance. - :rtype: Boolean - ''' + """ + Returns ``True`` if connection is closed. ``False`` otherwise. + + :rtype: :obj:`bool` + """ + return self._socket is None def connect_basic(self): + """ + Establish a connection to the host and port specified on + initialization. + + :raise: :exc:`~tarantool.error.NetworkError` + + :meta private: + """ + if self.host is None: self.connect_unix() else: self.connect_tcp() def connect_tcp(self): - ''' - Create a connection to the host and port specified in __init__(). + """ + Establish a TCP connection to the host and port specified on + initialization. + + :raise: :exc:`~tarantool.error.NetworkError` - :raise: `NetworkError` - ''' + :meta private: + """ try: # If old socket already exists - close it and re-create @@ -307,11 +557,14 @@ def connect_tcp(self): raise NetworkError(e) def connect_unix(self): - ''' - Create a connection to the host and port specified in __init__(). + """ + Create a connection to the Unix socket specified on + initialization. - :raise: `NetworkError` - ''' + :raise: :exc:`~tarantool.error.NetworkError` + + :meta private: + """ try: # If old socket already exists - close it and re-create @@ -327,12 +580,14 @@ def connect_unix(self): raise NetworkError(e) def wrap_socket_ssl(self): - ''' + """ Wrap an existing socket with an SSL socket. - :raise: SslError - :raise: `ssl.SSLError` - ''' + :raise: :exc:`~tarantool.error.SslError` + + :meta private: + """ + if not is_ssl_supported: raise SslError("Your version of Python doesn't support SSL") @@ -394,6 +649,15 @@ def password_raise_error(): raise SslError(e) def handshake(self): + """ + Process greeting with Tarantool server. + + :raise: :exc:`~ValueError`, + :exc:`~tarantool.error.NetworkError` + + :meta private: + """ + greeting_buf = self._recv(IPROTO_GREETING_SIZE) greeting = greeting_decode(greeting_buf) if greeting.protocol != "Binary": @@ -405,14 +669,17 @@ def handshake(self): self.authenticate(self.user, self.password) def connect(self): - ''' - Create a connection to the host and port specified in __init__(). - Usually, there is no need to call this method directly, - because it is called when you create a `Connection` instance. - - :raise: `NetworkError` - :raise: `SslError` - ''' + """ + Create a connection to the host and port specified on + initialization. There is no need to call this method explicitly + until you have set ``connect_now=False`` on initialization. + + :raise: :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.DatabaseError` + """ + try: self.connect_basic() if self.transport == SSL_TRANSPORT: @@ -426,6 +693,18 @@ def connect(self): raise NetworkError(e) def _recv(self, to_read): + """ + Receive binary data from connection socket. + + :param to_read: Amount of data to read, in bytes. + :type to_read: :obj:`int` + + :return: Buffer with read data + :rtype: :obj:`bytes` + + :meta private: + """ + buf = b"" while to_read > 0: try: @@ -455,23 +734,37 @@ def _recv(self, to_read): return buf def _read_response(self): - ''' + """ Read response from the transport (socket). - :return: tuple of the form (header, body) - :rtype: tuple of two byte arrays - ''' + :return: Tuple of the form ``(header, body)``. + :rtype: :obj:`tuple` + + :meta private: + """ + # Read packet length length = msgpack.unpackb(self._recv(5)) # Read the packet return self._recv(length) def _send_request_wo_reconnect(self, request): - ''' - :rtype: `Response` instance or subclass + """ + Send request without trying to reconnect. + Reload schema, if required. + + :param request: Request to send. + :type request: :class:`~tarantool.request.Request` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError` + + :meta private: + """ - :raise: NetworkError - ''' assert isinstance(request, Request) response = None @@ -487,11 +780,17 @@ def _send_request_wo_reconnect(self, request): return response def _opt_reconnect(self): - ''' - Check that the connection is alive - using low-level recv from libc(ctypes). - **Bug in Python: timeout is an internal Python construction. - ''' + """ + Check that the connection is alive using low-level recv from + libc(ctypes). + + :raise: :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + :meta private: + """ + + # **Bug in Python: timeout is an internal Python construction (???). if not self._socket: return self.connect() @@ -501,7 +800,7 @@ def check(): # Check that connection is alive sock_fd = self._socket.fileno() except socket.error as e: if e.errno == errno.EBADF: - return errno.ECONNRESET + return errno.ECONNRESETtuple_value else: if os.name == 'nt': flag = socket.MSG_PEEK @@ -553,15 +852,22 @@ def check(): # Check that connection is alive self.handshake() def _send_request(self, request): - ''' + """ Send a request to the server through the socket. - Return an instance of the `Response` class. - :param request: object representing a request - :type request: `Request` instance + :param request: Request to send. + :type request: :class:`~tarantool.request.Request` + + :rtype: :class:`~tarantool.response.Response` - :rtype: `Response` instance - ''' + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + :meta private: + """ assert isinstance(request, Request) self._opt_reconnect() @@ -569,28 +875,63 @@ def _send_request(self, request): return self._send_request_wo_reconnect(request) def load_schema(self): + """ + Fetch space and index schema. + + :raise: :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.DatabaseError` + + :meta private: + """ + self.schema.fetch_space_all() self.schema.fetch_index_all() def update_schema(self, schema_version): + """ + Set new schema version metainfo, reload space and index schema. + + :param schema_version: New schema version metainfo. + :type schema_version: :obj:`int` + + :raise: :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.DatabaseError` + + :meta private: + """ + self.schema_version = schema_version self.flush_schema() def flush_schema(self): + """ + Reload space and index schema. + + :raise: :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.DatabaseError` + """ + self.schema.flush() self.load_schema() def call(self, func_name, *args): - ''' - Execute a CALL request. Call a stored Lua function. + """ + Execute a CALL request: call a stored Lua function. + + :param func_name: Stored Lua function name. + :type func_name: :obj:`str` + + :param args: Stored Lua function arguments. + :type args: :obj:`tuple` - :param func_name: stored Lua function name - :type func_name: str - :param args: list of function arguments - :type args: list or tuple + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ - :rtype: `Response` instance - ''' assert isinstance(func_name, str) # This allows to use a tuple or list as an argument @@ -602,16 +943,24 @@ def call(self, func_name, *args): return response def eval(self, expr, *args): - ''' - Execute an EVAL request. Eval a Lua expression. + """ + Execute an EVAL request: evaluate a Lua expression. + + :param expr: Lua expression. + :type expr: :obj:`str` + + :param args: Lua expression arguments. + :type args: :obj:`tuple` - :param expr: Lua expression - :type expr: str - :param args: list of function arguments - :type args: list or tuple + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ - :rtype: `Response` instance - ''' assert isinstance(expr, str) # This allows to use a tuple or list as an argument @@ -623,32 +972,54 @@ def eval(self, expr, *args): return response def replace(self, space_name, values): - ''' - Execute a REPLACE request. - Doesn't throw an error if there is no tuple with the specified PK. - - :param int space_name: space id to insert a record - :type space_name: int or str - :param values: record to be inserted. The tuple must contain - only scalar (integer or strings) values - :type values: tuple - - :rtype: `Response` instance - ''' + """ + Execute a REPLACE request: `replace`_ a tuple in the space. + Doesn't throw an error if there is no tuple with the specified + primary key. + + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` + + :param values: Tuple to be replaced. + :type values: :obj:`tuple` or :obj:`list` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _replace: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/replace/ + """ + if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid request = RequestReplace(self, space_name, values) return self._send_request(request) def authenticate(self, user, password): - ''' - Execute an AUTHENTICATE request. + """ + Execute an AUTHENTICATE request: authenticate a connection. + There is no need to call this method explicitly until you want + to reauthenticate with different parameters. - :param string user: user to authenticate - :param string password: password for the user + :param user: User to authenticate. + :type user: :obj:`str` + + :param password: Password for the user. + :type password: :obj:`str` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ - :rtype: `Response` instance - ''' self.user = user self.password = password if not self._socket: @@ -662,6 +1033,19 @@ def authenticate(self, user, password): return auth_response def _join_v16(self, server_uuid): + """ + Execute a JOIN request for Tarantool 1.6 and older. + + :param server_uuid: UUID of Tarantool server to join. + :type server_uuid: :obj:`str` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ + request = RequestJoin(self, server_uuid) self._socket.sendall(bytes(request)) @@ -673,6 +1057,19 @@ def _join_v16(self, server_uuid): self.close() # close connection after JOIN def _join_v17(self, server_uuid): + """ + Execute a JOIN request for Tarantool 1.7 and newer. + + :param server_uuid: UUID of Tarantool server to join. + :type server_uuid: :obj:`str` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ + class JoinState: Handshake, Initial, Final, Done = range(4) @@ -699,12 +1096,49 @@ def _ops_process(self, space, update_ops): return new_ops def join(self, server_uuid): + """ + Execute a JOIN request: `join`_ a replicaset. + + :param server_uuid: UUID of connector "server". + :type server_uuid: :obj:`str` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _join: https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#iproto-join-0x41 + """ + self._opt_reconnect() if self.version_id < version_id(1, 7, 0): return self._join_v16(server_uuid) return self._join_v17(server_uuid) def subscribe(self, cluster_uuid, server_uuid, vclock=None): + """ + Execute a SUBSCRIBE request: `subscribe`_ to a replicaset + updates. Connection is closed after subscribing. + + :param cluster_uuid: UUID of replicaset cluster. + :type cluster_uuid: :obj:`str` + + :param server_uuid: UUID of connector "server". + :type server_uuid: :obj:`str` + + :param vclock: Connector "server" vclock. + :type vclock: :obj:`dict` or :obj:`None`, optional + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _subscribe: https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/#iproto-subscribe-0x42 + """ + vclock = vclock or {} request = RequestSubscribe(self, cluster_uuid, server_uuid, vclock) self._socket.sendall(bytes(request)) @@ -716,203 +1150,214 @@ def subscribe(self, cluster_uuid, server_uuid, vclock=None): self.close() # close connection after SUBSCRIBE def insert(self, space_name, values): - ''' - Execute an INSERT request. - Throws an error if there is a tuple with the same PK. - - :param int space_name: space id to insert the record - :type space_name: int or str - :param values: record to be inserted. The tuple must contain - only scalar (integer or strings) values - :type values: tuple - - :rtype: `Response` instance - ''' + """ + Execute an INSERT request: `insert`_ a tuple to the space. + Throws an error if there is already a tuple with the same + primary key. + + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` + + :param values: Record to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _insert: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/insert/ + """ + if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid request = RequestInsert(self, space_name, values) return self._send_request(request) - def delete(self, space_name, key, **kwargs): - ''' - Execute DELETE request. - Delete a single record identified by `key`. If you're using a secondary - index, it must be unique. + def delete(self, space_name, key, *, index=0): + """ + Execute a DELETE request: `delete`_ a tuple in the space. - :param space_name: space number or name to delete a record - :type space_name: int or name - :param key: key that identifies a record - :type key: int or str + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` - :rtype: `Response` instance - ''' - index_name = kwargs.get("index", 0) + :param key: Key of a tuple to be deleted. + + :param index: Index name or index id. If you're using a + secondary index, it must be unique. Defaults to primary + index. + :type index: :obj:`str` or :obj:`int`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _delete: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/delete/ + """ key = check_key(key) if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid - if isinstance(index_name, str): - index_name = self.schema.get_index(space_name, index_name).iid - request = RequestDelete(self, space_name, index_name, key) + if isinstance(index, str): + index = self.schema.get_index(space_name, index).iid + request = RequestDelete(self, space_name, index, key) return self._send_request(request) - def upsert(self, space_name, tuple_value, op_list, **kwargs): - ''' - Execute UPSERT request. + def upsert(self, space_name, tuple_value, op_list, *, index=0): + """ + Execute an UPSERT request: `upsert`_ a tuple to the space. If an existing tuple matches the key fields of - `tuple_value`, then the request has the same effect as UPDATE - and the [(field_1, symbol_1, arg_1), ...] parameter is used. - - If there is no tuple matching the key fields of - `tuple_value`, then the request has the same effect as INSERT - and the `tuple_value` parameter is used. However, unlike insert - or update, upsert will neither read the tuple nor perform error checks - before returning -- this is a design feature which enhances - throughput but requires more caution on the part of the user. - - If you're using a secondary index, it must be unique. - - The list of operations allows updating individual fields. - - For every operation, you must provide the field number to apply this - operation to. - - *Allowed operations:* - - * `+` for addition (values must be numeric) - * `-` for subtraction (values must be numeric) - * `&` for bitwise AND (values must be unsigned numeric) - * `|` for bitwise OR (values must be unsigned numeric) - * `^` for bitwise XOR (values must be unsigned numeric) - * `:` for string splice (you must provide `offset`, `count`, - and `value` for this operation) - * `!` for insertion (provide any element to insert) - * `=` for assignment (provide any element to assign) - * `#` for deletion (provide count of fields to delete) - - :param space_name: space number or name to update a record - :type space_name: int or str - :param index: index number or name to update a record - :type index: int or str - :param tuple_value: tuple - :type tuple_value: - :param op_list: list of operations. Each operation - is a tuple of three (or more) values - :type op_list: list of the form [(symbol_1, field_1, arg_1), - (symbol_2, field_2, arg_2_1, arg_2_2, arg_2_3),...] - - :rtype: `Response` instance - - Operation examples: + ``tuple_value``, then the request has the same effect as UPDATE + and the ``[(field_1, symbol_1, arg_1), ...]`` parameter is used. - .. code-block:: python + If there is no tuple matching the key fields of ``tuple_value``, + then the request has the same effect as INSERT and the + ``tuple_value`` parameter is used. However, unlike insert or + update, upsert will neither read the tuple nor perform error + checks before returning -- this is a design feature which + enhances throughput but requires more caution on the part of the + user. + + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` + + :param tuple_value: Tuple to be upserted. + :type tuple_value: :obj:`tuple` or :obj:`list` + + :param op_list: Refer to :meth:`~tarantool.Connection.update` + :paramref:`~tarantool.Connection.update.params.op_list`. + :type op_list: :obj:`tuple` or :obj:`list` + + :param index: Index name or index id. If you're using a + secondary index, it must be unique. Defaults to primary + index. + :type index: :obj:`str` or :obj:`int`, optional + + :rtype: :class:`~tarantool.response.Response` - # 'ADD' 55 to the second field - # Assign 'x' to the third field - [('+', 2, 55), ('=', 3, 'x')] - # 'OR' the third field with '1' - # Cut three symbols, starting from the second, - # and replace them with '!!' - # Insert 'hello, world' field before the fifth element of the tuple - [('|', 3, 1), (':', 2, 2, 3, '!!'), ('!', 5, 'hello, world')] - # Delete two fields, starting with the second field - [('#', 2, 2)] - ''' - index_name = kwargs.get("index", 0) + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _upsert: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/upsert/ + """ if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid - if isinstance(index_name, str): - index_name = self.schema.get_index(space_name, index_name).iid + if isinstance(index, str): + index = self.schema.get_index(space_name, index).iid op_list = self._ops_process(space_name, op_list) - request = RequestUpsert(self, space_name, index_name, tuple_value, + request = RequestUpsert(self, space_name, index, tuple_value, op_list) return self._send_request(request) - def update(self, space_name, key, op_list, **kwargs): - ''' - Execute an UPDATE request. - - The `update` function supports operations on fields — assignment, - arithmetic (if the field is unsigned numeric), cutting and pasting - fragments of the field, deleting or inserting a field. Multiple - operations can be combined in a single update request, and in this - case they are performed atomically and sequentially. Each operation - requires that you specify a field number. With multiple operations, - the field number for each operation is assumed to be relative - to the most recent state of the tuple, that is, as if all previous - operations in the multi-operation update have already been applied. - In other words, it is always safe to merge multiple update invocations - into a single invocation, with no change in semantics. - - Update a single record identified by `key`. - - The list of operations allows updating individual fields. - - For every operation, you must provide the field number to apply this - operation to. - - *Allowed operations:* - - * `+` for addition (values must be numeric) - * `-` for subtraction (values must be numeric) - * `&` for bitwise AND (values must be unsigned numeric) - * `|` for bitwise OR (values must be unsigned numeric) - * `^` for bitwise XOR (values must be unsigned numeric) - * `:` for string splice (you must provide `offset`, `count` and `value` - for this operation) - * `!` for insertion (before) (provide any element to insert) - * `=` for assignment (provide any element to assign) - * `#` for deletion (provide count of fields to delete) - - :param space_name: space number or name to update the record - :type space_name: int or str - :param index: index number or name to update the record - :type index: int or str - :param key: key that identifies the record - :type key: int or str - :param op_list: list of operations. Each operation - is a tuple of three (or more) values - :type op_list: list of the form [(symbol_1, field_1, arg_1), - (symbol_2, field_2, arg_2_1, arg_2_2, arg_2_3), ...] - - :rtype: ``Response`` instance - - Operation examples: + def update(self, space_name, key, op_list, *, index=0): + """ + Execute an UPDATE request: `update`_ a tuple in the space. - .. code-block:: python + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` + + :param key: Key of a tuple to be updated. + + :param op_list: The list of operations to update individual + fields. Each operation is a :obj:`tuple` of three (or more) + values: ``(operator, field_identifier, value)``. + + Possible operators are: + + * ``'+'`` for addition. values must be numeric + * ``'-'`` for subtraction. values must be numeric + * ``'&'`` for bitwise AND. values must be unsigned numeric + * ``'|'`` for bitwise OR. values must be unsigned numeric + * ``'^'`` for bitwise XOR. values must be unsigned numeric + * ``':'`` for string splice. you must provide ``offset``, + ``count``, and ``value`` for this operation + * ``'!'`` for insertion. provide any element to insert) + * ``'='`` for assignment. (provide any element to assign) + * ``'#'`` for deletion. provide count of fields to delete) + + Possible field_identifiers are: + + * Positive field number. The first field is 1, the second + field is 2, and so on. + * Negative field number. The last field is -1, the + second-last field is -2, and so on. + In other words: ``(#tuple + negative field number + 1)``. + * Name. If the space was formatted with + ``space_object:format()``, then this can be a string for + the field ``name`` (Since Tarantool 2.3.1). + + Operation examples: + + .. code-block:: python - # 'ADD' 55 to second field - # Assign 'x' to the third field - [('+', 2, 55), ('=', 3, 'x')] - # 'OR' the third field with '1' - # Cut three symbols, starting from second, - # and replace them with '!!' - # Insert 'hello, world' field before the fifth element of the tuple - [('|', 3, 1), (':', 2, 2, 3, '!!'), ('!', 5, 'hello, world')] - # Delete two fields, starting with the second field - [('#', 2, 2)] - ''' - index_name = kwargs.get("index", 0) + # 'ADD' 55 to the second field + # Assign 'x' to the third field + [('+', 2, 55), ('=', 3, 'x')] + # 'OR' the third field with '1' + # Cut three symbols, starting from the second, + # and replace them with '!!' + # Insert 'hello, world' field before the fifth element of the tuple + [('|', 3, 1), (':', 2, 2, 3, '!!'), ('!', 5, 'hello, world')] + # Delete two fields, starting with the second field + [('#', 2, 2)] + + :type op_list: :obj:`tuple` or :obj:`list` + + :param index: Index name or index id. If you're using a + secondary index, it must be unique. Defaults to primary + index. + :type index: :obj:`str` or :obj:`int`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _update: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/update/ + """ key = check_key(key) if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid - if isinstance(index_name, str): - index_name = self.schema.get_index(space_name, index_name).iid + if isinstance(index, str): + index = self.schema.get_index(space_name, index).iid op_list = self._ops_process(space_name, op_list) - request = RequestUpdate(self, space_name, index_name, key, op_list) + request = RequestUpdate(self, space_name, index, key, op_list) return self._send_request(request) def ping(self, notime=False): - ''' - Execute a PING request. - Send an empty request and receive an empty response from the server. + """ + Execute a PING request: send an empty request and receive + an empty response from the server. - :return: response time in seconds - :rtype: float - ''' + :param notime: If ``False``, returns response time. + Otherwise, it returns ``'Success'``. + :type notime: :obj:`bool`, optional + + :return: Response time or ``'Success'``. + :rtype: :obj:`float` or :obj:`str` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + """ request = RequestPing(self) t0 = time.time() @@ -923,58 +1368,151 @@ def ping(self, notime=False): return "Success" return t1 - t0 - def select(self, space_name, key=None, **kwargs): - ''' - Execute a SELECT request. - Select and retrieve data from the database. - - :param space_name: space to query - :type space_name: int or str - :param values: values to search by index - :type values: list, tuple, set, frozenset of tuples - :param index: index to search by (default is **0**, which - means that the **primary index** is used) - :type index: int or str - :param offset: offset in the resulting tuple set - :type offset: int - :param limit: limits the total number of returned tuples - :type limit: int - - :rtype: `Response` instance - - You can use index/space names. The driver will get - the matching id's -> names from the server. - - Select a single record (from space=0 and using index=0) - >>> select(0, 1) - - Select a single record from space=0 (with name='space') using - composite index=1 (with name '_name'). - >>> select(0, [1,'2'], index=1) - # OR - >>> select(0, [1,'2'], index='_name') - # OR - >>> select('space', [1,'2'], index='_name') - # OR - >>> select('space', [1,'2'], index=1) - - Select all records - >>> select(0) - # OR - >>> select(0, []) - ''' - - # Initialize arguments and its defaults from **kwargs - offset = kwargs.get("offset", 0) - limit = kwargs.get("limit", 0xffffffff) - index_name = kwargs.get("index", 0) - iterator_type = kwargs.get("iterator") - - if iterator_type is None: - iterator_type = ITERATOR_EQ + def select(self, space_name, key=None, *, offset=0, limit=0xffffffff, index=0, iterator=None): + """ + Execute a SELECT request: `select`_ a tuple from the space. + + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` + + :param key: Key of a tuple to be selected. + :type key: optional + + :param offset: Number of tuples to skip. + :type offset: :obj:`int`, optional + + :param limit: Maximum number of tuples to select. + :type limit: :obj:`int`, optional + + :param index: Index name or index id to select. + Defaults to primary index. + :type limit: :obj:`str` or :obj:`int`, optional + + :param iterator: Index iterator type. + + Iterator types for TREE indexes: + + +---------------+-----------+---------------------------------------------+ + | Iterator type | Arguments | Description | + +===============+===========+=============================================+ + | ``'EQ'`` | search | The comparison operator is '==' (equal to). | + | | value | If an index key is equal to a search value, | + | | | it matches. | + | | | Tuples are returned in ascending order by | + | | | index key. This is the default. | + +---------------+-----------+---------------------------------------------+ + | ``'REQ'`` | search | Matching is the same as for ``'EQ'``. | + | | value | Tuples are returned in descending order by | + | | | index key. | + +---------------+-----------+---------------------------------------------+ + | ``'GT'`` | search | The comparison operator is '>' (greater | + | | value | than). | + | | | If an index key is greater than a search | + | | | value, it matches. | + | | | Tuples are returned in ascending order by | + | | | index key. | + +---------------+-----------+---------------------------------------------+ + | ``'GE'`` | search | The comparison operator is '>=' (greater | + | | value | than or equal to). | + | | | If an index key is greater than or equal to | + | | | a search value, it matches. | + | | | Tuples are returned in ascending order by | + | | | index key. | + +---------------+-----------+---------------------------------------------+ + | ``'ALL'`` | search | Same as ``'GE'`` | + | | value | | + | | | | + +---------------+-----------+---------------------------------------------+ + | ``'LT'`` | search | The comparison operator is '<' (less than). | + | | value | If an index key is less than a search | + | | | value, it matches. | + | | | Tuples are returned in descending order by | + | | | index key. | + +---------------+-----------+---------------------------------------------+ + | ``'LE'`` | search | The comparison operator is '<=' (less than | + | | value | or equal to). | + | | | If an index key is less than or equal to a | + | | | search value, it matches. | + | | | Tuples are returned in descending order by | + | | | index key. | + +---------------+-----------+---------------------------------------------+ + + Iterator types for HASH indexes: + + +---------------+-----------+------------------------------------------------+ + | Type | Arguments | Description | + +===============+===========+================================================+ + | ``'ALL'`` | none | All index keys match. | + | | | Tuples are returned in ascending order by | + | | | hash of index key, which will appear to be | + | | | random. | + +---------------+-----------+------------------------------------------------+ + | ``'EQ'`` | search | The comparison operator is '==' (equal to). | + | | value | If an index key is equal to a search value, | + | | | it matches. | + | | | The number of returned tuples will be 0 or 1. | + | | | This is the default. | + +---------------+-----------+------------------------------------------------+ + | ``'GT'`` | search | The comparison operator is '>' (greater than). | + | | value | If a hash of an index key is greater than a | + | | | hash of a search value, it matches. | + | | | Tuples are returned in ascending order by hash | + | | | of index key, which will appear to be random. | + | | | Provided that the space is not being updated, | + | | | one can retrieve all the tuples in a space, | + | | | N tuples at a time, by using | + | | | ``iterator='GT',limit=N`` | + | | | in each search, and using the last returned | + | | | value from the previous result as the start | + | | | search value for the next search. | + +---------------+-----------+------------------------------------------------+ + + Iterator types for BITSET indexes: + + +----------------------------+-----------+----------------------------------------------+ + | Type | Arguments | Description | + +============================+===========+==============================================+ + | ``'ALL'`` | none | All index keys match. | + | | | Tuples are returned in their order within | + | | | the space. | + +----------------------------+-----------+----------------------------------------------+ + | ``'EQ'`` | bitset | If an index key is equal to a bitset value, | + | | value | it matches. | + | | | Tuples are returned in their order within | + | | | the space. This is the default. | + +----------------------------+-----------+----------------------------------------------+ + | ``'BITS_ALL_SET'`` | bitset | If all of the bits which are 1 in the bitset | + | | value | value are 1 in the index key, it matches. | + | | | Tuples are returned in their order within | + | | | the space. | + +----------------------------+-----------+----------------------------------------------+ + | ``'BITS_ANY_SET'`` | bitset | If any of the bits which are 1 in the bitset | + | | value | value are 1 in the index key, it matches. | + | | | Tuples are returned in their order within | + | | | the space. | + +----------------------------+-----------+----------------------------------------------+ + | ``'BITS_ALL_NOT_SET'`` | bitset | If all of the bits which are 1 in the bitset | + | | value | value are 0 in the index key, it matches. | + | | | Tuples are returned in their order within | + | | | the space. | + +----------------------------+-----------+----------------------------------------------+ + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + .. _select: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/select/ + """ + + if iterator is None: + iterator = ITERATOR_EQ if key is None or (isinstance(key, (list, tuple)) and len(key) == 0): - iterator_type = ITERATOR_ALL + iterator = ITERATOR_ALL # Perform smart type checking (scalar / list of scalars / list of # tuples) @@ -982,61 +1520,83 @@ def select(self, space_name, key=None, **kwargs): if isinstance(space_name, str): space_name = self.schema.get_space(space_name).sid - if isinstance(index_name, str): - index_name = self.schema.get_index(space_name, index_name).iid - request = RequestSelect(self, space_name, index_name, key, offset, - limit, iterator_type) + if isinstance(index, str): + index = self.schema.get_index(space_name, index).iid + request = RequestSelect(self, space_name, index, key, offset, + limit, iterator) response = self._send_request(request) return response def space(self, space_name): - ''' - Create a `Space` instance for a particular space. + """ + Create a :class:`~tarantool.space.Space` instance for a + particular space. + + :param space_name: Space name or space id. + :type space_name: :obj:`str` or :obj:`int` - A `Space` instance encapsulates the identifier - of the space and provides a more convenient syntax - for accessing the database space. + :rtype: :class:`~tarantool.space.Space` - :param space_name: identifier of the space - :type space_name: int or str + :raise: :exc:`~tarantool.error.SchemaError` + """ - :rtype: `Space` instance - ''' return Space(self, space_name) def generate_sync(self): - ''' - Need override for async io connection. - ''' + """ + Generate IPROTO_SYNC code for a request. Since the connector is + synchronous, any constant value would be sufficient. + + :return: ``0`` + :rtype: :obj:`int` + + :meta private: + """ + return 0 def execute(self, query, params=None): - ''' - Execute an SQL request. + """ + Execute an SQL request: see `documentation`_ for syntax + reference. - The Tarantool binary protocol for SQL requests - supports "qmark" and "named" param styles. - A sequence of values can be used for "qmark" style. - A mapping is used for "named" param style + The Tarantool binary protocol for SQL requests supports "qmark" + and "named" param styles. A sequence of values can be used for + "qmark" style. A mapping is used for "named" param style without the leading colon in the keys. Example for "qmark" arguments: - >>> args = ['email@example.com'] - >>> c.execute('select * from "users" where "email"=?', args) + + .. code-block:: python + + args = ['email@example.com'] + c.execute('select * from "users" where "email"=?', args) Example for "named" arguments: - >>> args = {'email': 'email@example.com'} - >>> c.execute('select * from "users" where "email"=:email', args) - :param query: SQL syntax query - :type query: str + .. code-block:: python + + args = {'email': 'email@example.com'} + c.execute('select * from "users" where "email"=:email', args) + + :param query: SQL query. + :type query: :obj:`str` + + :param params: SQL query bind values. + :type params: :obj:`dict` or :obj:`list` or :obj:`None`, + optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` - :param params: bind values to use in the query - :type params: list, dict + .. _documentation: https://www.tarantool.io/en/doc/latest/how-to/sql/ + """ - :return: query result data - :rtype: `Response` instance - ''' if not params: params = [] request = RequestExecute(self, query, params) diff --git a/tarantool/connection_pool.py b/tarantool/connection_pool.py index 165d655e..de4a869d 100644 --- a/tarantool/connection_pool.py +++ b/tarantool/connection_pool.py @@ -1,3 +1,7 @@ +""" +This module provides API for interaction with Tarantool servers cluster. +""" + import abc import itertools import queue @@ -28,43 +32,140 @@ class Mode(Enum): + """ + Request mode. + """ + ANY = 1 + """ + Send a request to any server. + """ + RW = 2 + """ + Send a request to RW server. + """ + RO = 3 + """ + Send a request to RO server. + """ + PREFER_RW = 4 + """ + Send a request to RW server, if possible, RO server otherwise. + """ + PREFER_RO = 5 + """ + Send a request to RO server, if possible, RW server otherwise. + """ class Status(Enum): + """ + Cluster single server status. + """ + HEALTHY = 1 + """ + Server is healthy: connection is successful, + `box.info.ro`_ could be extracted, `box.info.status`_ is "running". + """ + UNHEALTHY = 2 + """ + Server is unhealthy: either connection is failed, + `box.info`_ cannot be extracted, `box.info.status`_ is not + "running". + """ @dataclass class InstanceState(): + """ + Cluster single server state. + """ + status: Status = Status.UNHEALTHY + """ + :type: :class:`~tarantool.connection_pool.Status` + """ ro: typing.Optional[bool] = None + """ + :type: :obj:`bool`, optional + """ def QueueFactory(): + """ + Build a queue-based channel. + """ + return queue.Queue(maxsize=1) @dataclass class PoolUnit(): + """ + Class to store a Tarantool server metainfo and + to work with it as a part of connection pool. + """ + addr: dict + """ + ``{"host": host, "port": port}`` info. + + :type: :obj:`dict` + """ + conn: Connection + """ + :type: :class:`~tarantool.Connection` + """ + input_queue: queue.Queue = field(default_factory=QueueFactory) + """ + Channel to pass requests for the server thread. + + :type: :obj:`queue.Queue` + """ + output_queue: queue.Queue = field(default_factory=QueueFactory) + """ + Channel to receive responses from the server thread. + + :type: :obj:`queue.Queue` + """ + thread: typing.Optional[threading.Thread] = None + """ + Background thread to process requests for the server. + + :type: :obj:`threading.Thread` + """ + state: InstanceState = field(default_factory=InstanceState) - # request_processing_enabled is used to stop requests processing - # in background thread on close or destruction. + """ + Current server state. + + :type: :class:`~tarantool.connection_pool.InstanceState` + """ + request_processing_enabled: bool = False + """ + Flag used to stop requests processing requests in the background + thread on connection close or destruction. + :type: :obj:`bool` + """ # Based on https://realpython.com/python-interface/ class StrategyInterface(metaclass=abc.ABCMeta): + """ + Defines strategy to choose a pool server based on a request mode. + """ + @classmethod def __subclasshook__(cls, subclass): return (hasattr(subclass, '__init__') and @@ -77,21 +178,42 @@ def __subclasshook__(cls, subclass): @abc.abstractmethod def __init__(self, pool): + """ + :type: :obj:`list` of + :class:`~tarantool.connection_pool.PoolUnit` objects + """ + raise NotImplementedError @abc.abstractmethod def update(self): + """ + Refresh the strategy state. + """ raise NotImplementedError @abc.abstractmethod def getnext(self, mode): + """ + Get a pool server based on a request mode. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + """ + raise NotImplementedError class RoundRobinStrategy(StrategyInterface): """ - Simple round-robin connection rotation + Simple round-robin pool servers rotation. """ + def __init__(self, pool): + """ + :type: :obj:`list` of + :class:`~tarantool.connection_pool.PoolUnit` objects + """ + self.ANY_iter = None self.RW_iter = None self.RO_iter = None @@ -99,6 +221,11 @@ def __init__(self, pool): self.rebuild_needed = True def build(self): + """ + Initialize (or re-initialize) internal pools to rotate servers + based on `box.info.ro`_ state. + """ + ANY_pool = [] RW_pool = [] RO_pool = [] @@ -134,9 +261,26 @@ def build(self): self.rebuild_needed = False def update(self): + """ + Set flag to re-initialize internal pools on next + :meth:`~tarantool.connection_pool.RoundRobinStrategy.getnext` + call. + """ + self.rebuild_needed = True def getnext(self, mode): + """ + Get server based on the request mode. + + :param mode: Request mode + :type mode: :class:`~tarantool.Mode` + + :rtype: :class:`~tarantool.connection_pool.PoolUnit` + + :raise: :exc:`~tarantool.error.PoolTolopogyError` + """ + if self.rebuild_needed: self.build() @@ -173,32 +317,53 @@ def getnext(self, mode): @dataclass class PoolTask(): + """ + Store request type and arguments to pass them to some server thread. + """ + method_name: str + """ + :class:`~tarantool.Connection` method name. + + :type: :obj:`str` + """ + args: tuple + """ + :class:`~tarantool.Connection` method args. + + :type: :obj:`tuple` + """ + kwargs: dict + """ + :class:`~tarantool.Connection` method kwargs. + + :type: :obj:`dict` + """ class ConnectionPool(ConnectionInterface): - ''' - Represents the pool of connections to a cluster of Tarantool servers. - - ConnectionPool API is the same as Connection API. - On each request, a connection is chosen to execute the request. - The connection is selected based on request mode: - - * Mode.ANY chooses any instance. - * Mode.RW chooses an RW instance. - * Mode.RO chooses an RO instance. - * Mode.PREFER_RW chooses an RW instance, if possible, an RO instance - otherwise. - * Mode.PREFER_RO chooses an RO instance, if possible, an RW instance - otherwise. - - All requests that guarantee to write data (insert, replace, delete, - upsert, update) use the RW mode by default. select uses ANY by default. You - can set the mode explicitly. The call, eval, execute, and ping requests - require to set the mode explicitly. - ''' + """ + Represents the pool of connections to a cluster of Tarantool + servers. + + To work with :class:`~tarantool.connection_pool.ConnectionPool`, + `box.info`_ must be callable for the user on each server. + + :class:`~tarantool.ConnectionPool` is best suited to work with + a single replicaset. Its API is the same as a single server + :class:`~tarantool.Connection`, but requests support ``mode`` + parameter (a :class:`tarantool.Mode` value) to choose between + read-write and read-only pool instances: + + .. code-block:: python + + >>> resp = conn.select('demo', 'AAAA', mode=tarantool.Mode.PREFER_RO) + >>> resp + - ['AAAA', 'Alpha'] + """ + def __init__(self, addrs, user=None, @@ -212,46 +377,83 @@ def __init__(self, connection_timeout=CONNECTION_TIMEOUT, strategy_class=RoundRobinStrategy, refresh_delay=POOL_REFRESH_DELAY): - ''' - Initialize connections to a cluster of servers. - - :param list addrs: List of + """ + :param addrs: List of dictionaries describing server addresses: .. code-block:: python { - host: "str", # optional - port: int or "str", # mandatory - transport: "str", # optional - ssl_key_file: "str", # optional - ssl_cert_file: "str", # optional - ssl_ca_file: "str", # optional - ssl_ciphers: "str" # optional + "host': "str" or None, # mandatory + "port": int or "str", # mandatory + "transport": "str", # optional + "ssl_key_file": "str", # optional + "ssl_cert_file": "str", # optional + "ssl_ca_file": "str", # optional + "ssl_ciphers": "str" # optional } - dictionaries describing server addresses. - See similar :func:`tarantool.Connection` parameters. - :param str user: Username used to authenticate. User must be able - to call the box.info function. For example, to grant permissions to - the 'guest' user, evaluate: - box.schema.func.create('box.info') - box.schema.user.grant('guest', 'execute', 'function', 'box.info') - on Tarantool instances. - :param int reconnect_max_attempts: Max attempts to reconnect - for each connection in the pool. Be careful with reconnect - parameters in ConnectionPool since every status refresh is - also a request with reconnection. Default is 0 (fail after - first attempt). - :param float reconnect_delay: Time between reconnect - attempts for each connection in the pool. Be careful with - reconnect parameters in ConnectionPool since every status - refresh is also a request with reconnection. Default is 0. - :param StrategyInterface strategy_class: Class for choosing - instance based on request mode. By default, the round-robin - strategy is used. - :param int refresh_delay: Minimal time between RW/RO status - refreshes. - ''' + Refer to corresponding :class:`~tarantool.Connection` + parameters. + :type addrs: :obj:`list` + + :param user: Refer to + :paramref:`~tarantool.Connection.params.user`. + The value is used for each connection in the pool. + + :param password: Refer to + :paramref:`~tarantool.Connection.params.password`. + The value is used for each connection in the pool. + + :param socket_timeout: Refer to + :paramref:`~tarantool.Connection.params.socket_timeout`. + The value is used for each connection in the pool. + + :param reconnect_max_attempts: Refer to + :paramref:`~tarantool.Connection.params.reconnect_max_attempts`. + The value is used for each connection in the pool. + Be careful: it is internal :class:`~tarantool.Connection` + reconnect unrelated to pool reconnect mechanisms. + + :param reconnect_delay: Refer to + :paramref:`~tarantool.Connection.params.reconnect_delay`. + The value is used for each connection in the pool. + Be careful: it is internal :class:`~tarantool.Connection` + reconnect unrelated to pool reconnect mechanisms. + + :param connect_now: If ``True``, connect to all pool servers on + initialization. Otherwise, you have to call + :meth:`~tarantool.connection_pool.ConnectionPool.connect` + manually after initialization. + :type connect_now: :obj:`bool`, optional + + :param encoding: Refer to + :paramref:`~tarantool.Connection.params.encoding`. + The value is used for each connection in the pool. + + :param call_16: Refer to + :paramref:`~tarantool.Connection.params.call_16`. + The value is used for each connection in the pool. + + :param connection_timeout: Refer to + :paramref:`~tarantool.Connection.params.connection_timeout`. + The value is used for each connection in the pool. + + :param strategy_class: Strategy for choosing a server based on a + request mode. Defaults to the round-robin strategy. + :type strategy_class: :class:`~tarantool.connection_pool.StrategyInterface`, + optional + + :param refresh_delay: Minimal time between pool server + `box.info.ro`_ status background refreshes, in seconds. + :type connection_timeout: :obj:`float`, optional + + :raise: :exc:`~tarantool.error.ConfigurationError`, + :class:`~tarantool.Connection` exceptions + + .. _box.info.ro: + .. _box.info.status: + .. _box.info: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_info/ + """ if not isinstance(addrs, list) or len(addrs) == 0: raise ConfigurationError("addrs must be non-empty list") @@ -300,9 +502,31 @@ def __del__(self): self.close() def _make_key(self, addr): + """ + Make a unique key for a server based on its address. + + :param addr: `{"host": host, "port": port}` dictionary. + :type addr: :obj:`dict` + + :rtype: :obj:`str` + + :meta private: + """ + return '{0}:{1}'.format(addr['host'], addr['port']) def _get_new_state(self, unit): + """ + Get new pool server state. + + :param unit: Server metainfo. + :type unit: :class:`~tarantool.connection_pool.PoolUnit` + + :rtype: :class:`~tarantool.connection_pool.InstanceState` + + :meta private: + """ + conn = unit.conn if conn.is_closed(): @@ -347,6 +571,16 @@ def _get_new_state(self, unit): return InstanceState(Status.HEALTHY, ro) def _refresh_state(self, key): + """ + Refresh pool server state. + + :param key: Result of + :meth:`~tarantool.connection_pool._make_key`. + :type key: :obj:`str` + + :meta private: + """ + unit = self.pool[key] state = self._get_new_state(unit) @@ -355,6 +589,9 @@ def _refresh_state(self, key): self.strategy.update() def close(self): + """ + Stop request processing, close each connection in the pool. + """ for unit in self.pool.values(): unit.request_processing_enabled = False unit.thread.join() @@ -363,9 +600,31 @@ def close(self): unit.conn.close() def is_closed(self): + """ + Returns ``False`` if at least one connection is not closed and + is ready to process requests. Otherwise, returns ``True``. + + :rtype: :obj:`bool` + """ + return all(unit.request_processing_enabled == False for unit in self.pool.values()) def _request_process_loop(self, key, unit, last_refresh): + """ + Request process background loop for a pool server. Started in + a separate thread, one thread per server. + + :param key: Result of + :meth:`~tarantool.connection_pool._make_key`. + :type key: :obj:`str` + + :param unit: Server metainfo. + :type unit: :class:`~tarantool.connection_pool.PoolUnit` + + :param last_refresh: Time of last metainfo refresh. + :type last_refresh: :obj:`float` + """ + while unit.request_processing_enabled: if not unit.input_queue.empty(): task = unit.input_queue.get() @@ -384,6 +643,18 @@ def _request_process_loop(self, key, unit, last_refresh): last_refresh = time.time() def connect(self): + """ + Create a connection to each address specified on + initialization and start background process threads for them. + There is no need to call this method explicitly until you have + set ``connect_now=False`` on initialization. + + If some connections have failed to connect successfully or + provide `box.info`_ status (including the case when all of them + have failed), no exceptions are raised. Attempts to reconnect + and refresh the info would be processed in the background. + """ + for key in self.pool: unit = self.pool[key] @@ -399,6 +670,34 @@ def connect(self): unit.thread.start() def _send(self, mode, method_name, *args, **kwargs): + """ + Request wrapper. Choose a pool server based on mode and send + a request with arguments. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + + :param method_name: :class:`~tarantool.Connection` + method name. + :type method_name: :obj:`str` + + :param args: Method args. + :type args: :obj:`tuple` + + :param kwargs: Method kwargs. + :type kwargs: :obj:`dict` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :exc:`~tarantool.error.SslError` + + :meta private: + """ + key = self.strategy.getnext(mode) unit = self.pool[key] @@ -413,9 +712,24 @@ def _send(self, mode, method_name, *args, **kwargs): return resp def call(self, func_name, *args, mode=None): - ''' - :param tarantool.Mode mode: Request mode. - ''' + """ + Execute a CALL request on the pool server: call a stored Lua + function. Refer to :meth:`~tarantool.Connection.call`. + + :param func_name: Refer to + :paramref:`~tarantool.Connection.call.params.func_name`. + + :param args: Refer to + :paramref:`~tarantool.Connection.call.params.args`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~ValueError`, + :meth:`~tarantool.Connection.call` exceptions + """ if mode is None: raise ValueError("Please, specify 'mode' keyword argument") @@ -423,9 +737,24 @@ def call(self, func_name, *args, mode=None): return self._send(mode, 'call', func_name, *args) def eval(self, expr, *args, mode=None): - ''' - :param tarantool.Mode mode: Request mode. - ''' + """ + Execute an EVAL request on the pool server: evaluate a Lua + expression. Refer to :meth:`~tarantool.Connection.eval`. + + :param expr: Refer to + :paramref:`~tarantool.Connection.eval.params.expr`. + + :param args: Refer to + :paramref:`~tarantool.Connection.eval.params.args`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~ValueError`, + :meth:`~tarantool.Connection.eval` exceptions + """ if mode is None: raise ValueError("Please, specify 'mode' keyword argument") @@ -433,64 +762,216 @@ def eval(self, expr, *args, mode=None): return self._send(mode, 'eval', expr, *args) def replace(self, space_name, values, *, mode=Mode.RW): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + """ + Execute a REPLACE request on the pool server: `replace`_ a tuple + in the space. Refer to :meth:`~tarantool.Connection.replace`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.replace.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.replace.params.values`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.replace` exceptions + + .. _replace: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/replace/ + """ return self._send(mode, 'replace', space_name, values) def insert(self, space_name, values, *, mode=Mode.RW): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + """ + Execute an INSERT request on the pool server: `insert`_ a tuple + to the space. Refer to :meth:`~tarantool.Connection.insert`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.insert.params.space_name`. + + :param values: Refer to + :paramref:`~tarantool.Connection.insert.params.values`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.insert` exceptions + + .. _insert: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/insert/ + """ return self._send(mode, 'insert', space_name, values) - def delete(self, space_name, key, *, mode=Mode.RW, **kwargs): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + def delete(self, space_name, key, *, index=0, mode=Mode.RW): + """ + Execute an DELETE request on the pool server: `delete`_ a tuple + in the space. Refer to :meth:`~tarantool.Connection.delete`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.delete.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.delete.params.key`. + + :param index: Refer to + :paramref:`~tarantool.Connection.delete.params.index`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.delete` exceptions + + .. _delete: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/delete/ + """ - return self._send(mode, 'delete', space_name, key, **kwargs) + return self._send(mode, 'delete', space_name, key, index=index) - def upsert(self, space_name, tuple_value, op_list, *, mode=Mode.RW, **kwargs): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + def upsert(self, space_name, tuple_value, op_list, *, index=0, mode=Mode.RW): + """ + Execute an UPSERT request on the pool server: `upsert`_ a tuple to + the space. Refer to :meth:`~tarantool.Connection.upsert`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.upsert.params.space_name`. + + :param tuple_value: Refer to + :paramref:`~tarantool.Connection.upsert.params.tuple_value`. + + :param op_list: Refer to + :paramref:`~tarantool.Connection.upsert.params.op_list`. + + :param index: Refer to + :paramref:`~tarantool.Connection.upsert.params.index`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.upsert` exceptions + + .. _upsert: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/upsert/ + """ return self._send(mode, 'upsert', space_name, tuple_value, - op_list, **kwargs) + op_list, index=index) + + def update(self, space_name, key, op_list, *, index=0, mode=Mode.RW): + """ + Execute an UPDATE request on the pool server: `update`_ a tuple + in the space. Refer to :meth:`~tarantool.Connection.update`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.update.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.update.params.key`. + + :param op_list: Refer to + :paramref:`~tarantool.Connection.update.params.op_list`. - def update(self, space_name, key, op_list, *, mode=Mode.RW, **kwargs): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + :param index: Refer to + :paramref:`~tarantool.Connection.update.params.index`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.upsert` exceptions + + .. _update: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/update/ + """ return self._send(mode, 'update', space_name, key, - op_list, **kwargs) + op_list, index=index) + + def ping(self, notime=False, *, mode=None): + """ + Execute a PING request on the pool server: send an empty request + and receive an empty response from the server. Refer to + :meth:`~tarantool.Connection.ping`. + + :param notime: Refer to + :paramref:`~tarantool.Connection.ping.params.notime`. - def ping(self, *, mode=None, **kwargs): - ''' - :param tarantool.Mode mode: Request mode. - ''' + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + + :return: Refer to :meth:`~tarantool.Connection.ping`. + + :raise: :exc:`~ValueError`, + :meth:`~tarantool.Connection.ping` exceptions + """ if mode is None: raise ValueError("Please, specify 'mode' keyword argument") - return self._send(mode, 'ping', **kwargs) + return self._send(mode, 'ping', notime) + + def select(self, space_name, key, *, offset=0, limit=0xffffffff, + index=0, iterator=None, mode=Mode.ANY): + """ + Execute a SELECT request on the pool server: `update`_ a tuple + from the space. Refer to :meth:`~tarantool.Connection.select`. + + :param space_name: Refer to + :paramref:`~tarantool.Connection.select.params.space_name`. + + :param key: Refer to + :paramref:`~tarantool.Connection.select.params.key`. + + :param offset: Refer to + :paramref:`~tarantool.Connection.select.params.offset`. - def select(self, space_name, key, *, mode=Mode.ANY, **kwargs): - ''' - :param tarantool.Mode mode: Request mode (default is - ANY). - ''' + :param limit: Refer to + :paramref:`~tarantool.Connection.select.params.limit`. - return self._send(mode, 'select', space_name, key, **kwargs) + :param index: Refer to + :paramref:`~tarantool.Connection.select.params.index`. + + :param iterator: Refer to + :paramref:`~tarantool.Connection.select.params.iterator`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode`, optional + + :rtype: :class:`~tarantool.response.Response` + + :raise: :meth:`~tarantool.Connection.select` exceptions + + .. _select: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_space/select/ + """ + + return self._send(mode, 'select', space_name, key, offset=offset, limit=limit, + index=index, iterator=iterator) def execute(self, query, params=None, *, mode=None): - ''' - :param tarantool.Mode mode: Request mode (default is RW). - ''' + """ + Execute an SQL request on the pool server. Refer to + :meth:`~tarantool.Connection.execute`. + + :param query: Refer to + :paramref:`~tarantool.Connection.execute.params.query`. + + :param params: Refer to + :paramref:`~tarantool.Connection.execute.params.params`. + + :param mode: Request mode. + :type mode: :class:`~tarantool.Mode` + + :rtype: :class:`~tarantool.response.Response` + + :raise: :exc:`~ValueError`, + :meth:`~tarantool.Connection.execute` exceptions + """ if mode is None: raise ValueError("Please, specify 'mode' keyword argument") diff --git a/tarantool/dbapi.py b/tarantool/dbapi.py index 515e4689..f6f5d2ab 100644 --- a/tarantool/dbapi.py +++ b/tarantool/dbapi.py @@ -1,3 +1,9 @@ +""" +Python DB API implementation, refer to `PEP-249`_. + +.. _PEP-249: http://www.python.org/dev/peps/pep-0249/ +""" + from tarantool.connection import Connection as BaseConnection from tarantool.error import * @@ -8,8 +14,19 @@ class Cursor: + """ + Represent a database `cursor`_, which is used to manage the context + of a fetch operation. + + .. _cursor: https://peps.python.org/pep-0249/#cursor-objects + """ def __init__(self, conn): + """ + :param conn: Connection to a Tarantool server. + :type conn: :class:`~tarantool.Connection` + """ + self._c = conn self._lastrowid = None self._rowcount = None @@ -18,12 +35,16 @@ def __init__(self, conn): def callproc(self, procname, *params): """ - Call a stored database procedure with the given name. The sequence of - parameters must contain one entry for each argument that the - procedure expects. The result of the call is returned as a modified - copy of the input sequence. The input parameters are left untouched, - the output and input/output parameters replaced with - possibly new values. + **Not supported** + + Call a stored database procedure with the given name. The + sequence of parameters must contain one entry for each argument + that the procedure expects. The result of the call is returned + as a modified copy of the input sequence. The input parameters + are left untouched, the output and input/output parameters + replaced with possibly new values. + + :raises: :exc:`~tarantool.error.NotSupportedError` """ raise NotSupportedError("callproc() method is not supported") @@ -33,22 +54,45 @@ def rows(self): @property def description(self): + """ + **Not implemented** + + Call a stored database procedure with the given name. The + sequence of parameters must contain one entry for each argument + that the procedure expects. The result of the call is returned + as a modified copy of the input sequence. The input parameters + are left untouched, the output and input/output parameters + replaced with possibly new values. + + :raises: :exc:`~NotImplementedError` + """ + # FIXME Implement this method please raise NotImplementedError("description() property is not implemented") def close(self): """ Close the cursor now (rather than whenever __del__ is called). - The cursor will be unusable from this point forward; DatabaseError - exception will be raised if any operation is attempted with - the cursor. + The cursor will be unusable from this point forward; + :exc:`~tarantool.error.InterfaceError` exception will be + raised if any operation is attempted with the cursor. """ + self._c = None self._rows = None self._lastrowid = None self._rowcount = None def _check_not_closed(self, error=None): + """ + Check that cursor is not closed. Raise + :exc:`~tarantool.error.InterfaceError` otherwise. + + :param error: Custom error to be raised if cursor is closed. + :type error: optional + + :raises: :exc:`~tarantool.error.InterfaceError` + """ if self._c is None: raise InterfaceError(error or "Can not operate on a closed cursor") if self._c.is_closed(): @@ -57,8 +101,19 @@ def _check_not_closed(self, error=None): def execute(self, query, params=None): """ - Prepare and execute a database operation (query or command). + Execute an SQL request. Refer to + :meth:`~tarantool.Connection.execute`. + + :param query: Refer to + :paramref:`~tarantool.Connection.execute.params.query` + + :param params: Refer to + :paramref:`~tarantool.Connection.execute.params.params` + + :raises: :exc:`~tarantool.error.InterfaceError`, + :meth:`~tarantool.Connection.execute` exceptions """ + self._check_not_closed("Can not execute on closed cursor.") response = self._c.execute(query, params) @@ -71,6 +126,22 @@ def execute(self, query, params=None): self._lastrowid = None def executemany(self, query, param_sets): + """ + Execute several SQL requests with same query and different + parameters. Refer to :meth:`~tarantool.dbapi.Cursor.execute`. + + :param query: Refer to + :paramref:`~tarantool.dbapi.Cursor.execute.params.query`. + + :param param_sets: Set of parameters for execution. Refer to + :paramref:`~tarantool.dbapi.Cursor.execute.params.params` + for item description. + :type param sets: :obj:`list` or :obj:`tuple` + + :raises: :exc:`~tarantool.error.InterfaceError`, + :meth:`~tarantool.dbapi.Cursor.execute` exceptions + """ + self._check_not_closed("Can not execute on closed cursor.") rowcount = 0 for params in param_sets: @@ -84,25 +155,39 @@ def executemany(self, query, param_sets): @property def lastrowid(self): """ - This read-only attribute provides the rowid of the last modified row - (most databases return a rowid only when a single INSERT operation is - performed). + This read-only attribute provides the rowid of the last modified + row (most databases return a rowid only when a single INSERT + operation is performed). + + :type: :obj:`int` """ + return self._lastrowid @property def rowcount(self): """ - This read-only attribute specifies the number of rows that the last - .execute*() produced (for DQL statements like SELECT) or affected ( - for DML statements like UPDATE or INSERT). + This read-only attribute specifies the number of rows that the + last ``.execute*()`` produced (for DQL statements like SELECT) + or affected (for DML statements like UPDATE or INSERT). + + :type: :obj:`int` """ + return self._rowcount def _check_result_set(self, error=None): """ - Non-public method for raising an error when Cursor object does not have - any row to fetch. Useful for checking access after DQL requests. + Non-public method for raising an error when Cursor object does + not have any row to fetch. Useful for checking access after DQL + requests. + + :param error: Error to raise in case of fail. + :type error: optional + + :raise: :exc:`~tarantool.error.InterfaceError` + + :meta private: """ if self._rows is None: raise InterfaceError(error or "No result set to fetch from") @@ -111,16 +196,25 @@ def fetchone(self): """ Fetch the next row of a query result set, returning a single sequence, or None when no more data is available. + + :raise: :exc:`~tarantool.error.InterfaceError` """ + self._check_result_set() return self.fetchmany(1)[0] if self._rows else None def fetchmany(self, size=None): """ - Fetch the next set of rows of a query result, returning a sequence of - sequences (e.g. a list of tuples). An empty sequence is returned when - no more rows are available. + Fetch the next set of rows of a query result, returning a + sequence of sequences (e.g. a list of tuples). An empty sequence + is returned when no more rows are available. + + :param size: Count of rows to fetch. If ``None``, fetch all. + :type size: :obj:`int` or :obj:`None`, optional + + :raise: :exc:`~tarantool.error.InterfaceError` """ + self._check_result_set() size = size or self.arraysize @@ -134,10 +228,15 @@ def fetchmany(self, size=None): return items def fetchall(self): - """Fetch all (remaining) rows of a query result, returning them as a - sequence of sequences (e.g. a list of tuples). Note that the cursor's - arraysize attribute can affect the performance of this operation. """ + Fetch all (remaining) rows of a query result, returning them as + a sequence of sequences (e.g. a list of tuples). Note that + the cursor's arraysize attribute can affect the performance of + this operation. + + :raise: :exc:`~tarantool.error.InterfaceError` + """ + self._check_result_set() items = self._rows @@ -145,22 +244,51 @@ def fetchall(self): return items def setinputsizes(self, sizes): - """PEP-249 allows to not implement this method and do nothing.""" + """ + **Not implemented** (optional, refer to `PEP-249`_) + + Do nothing. + """ def setoutputsize(self, size, column=None): - """PEP-249 allows to not implement this method and do nothing.""" + """ + **Not implemented** (optional, refer to `PEP-249`_) + + Do nothing. + """ class Connection(BaseConnection): + """ + `PEP-249`_ compatible :class:`~tarantool.Connection` class wrapper. + """ def __init__(self, *args, **kwargs): + """ + :param args: :class:`~tarantool.Connection` args. + :type args: :obj:`tuple` + + :param kwargs: :class:`~tarantool.Connection` kwargs. + :type kwargs: :obj:`dict` + + :param autocommit: Enable or disable autocommit. Defaults to + ``True``. + :type autocommit: :obj:`bool`, optional + + :raise: :class:`~tarantool.Connection` exceptions + """ + super(Connection, self).__init__(*args, **kwargs) self._set_autocommit(kwargs.get('autocommit', True)) def _set_autocommit(self, autocommit): - """Autocommit is True by default and the default will be changed - to False. Set the autocommit property explicitly to True or verify - it when lean on autocommit behaviour.""" + """ + Autocommit setter. ``False`` is not supported. + + :raise: :exc:`~tarantool.error.InterfaceError`, + :exc:`~tarantool.error.NotSupportedError` + """ + if not isinstance(autocommit, bool): raise InterfaceError("autocommit parameter must be boolean, " "not %s" % autocommit.__class__.__name__) @@ -171,46 +299,79 @@ def _set_autocommit(self, autocommit): @property def autocommit(self): - """Autocommit state""" + """ + Autocommit state. + """ + return self._autocommit @autocommit.setter def autocommit(self, autocommit): - """Set autocommit state""" + """ + Set autocommit state. ``False`` is not supported. + + :raise: :exc:`~tarantool.error.InterfaceError`, + :exc:`~tarantool.error.NotSupportedError` + """ + self._set_autocommit(autocommit) def _check_not_closed(self, error=None): """ - Checks if the connection is not closed and rises an error if it is. + Checks if the connection is not closed and raises an error if it + is. + + :param error: Error to raise in case of fail. + :type error: optional + + :raise: :exc:`~tarantool.error.InterfaceError` """ if self.is_closed(): raise InterfaceError(error or "The connector is closed") def close(self): """ - Closes the connection + Close the connection. + + :raise: :exc:`~tarantool.error.InterfaceError` """ + self._check_not_closed("The closed connector can not be closed again.") super(Connection, self).close() def commit(self): """ Commit any pending transaction to the database. + + :raise: :exc:`~tarantool.error.InterfaceError` """ + self._check_not_closed("Can not commit on the closed connection") def rollback(self): """ - Roll back pending transaction + **Not supported** + + Roll back pending transaction. + + :raise: :exc:`~tarantool.error.InterfaceError`, + :exc:`~tarantool.error.NotSupportedError` """ + self._check_not_closed("Can not roll back on a closed connection") raise NotSupportedError("Transactions are not supported in this" "version of connector") def cursor(self): """ - Return a new Cursor Object using the connection. + Return a new Cursor object using the connection. + + :rtype: :class:`~tarantool.dbapi.Cursor` + + :raise: :exc:`~tarantool.error.InterfaceError`, + :class:`~tarantool.dbapi.Cursor` exceptions """ + self._check_not_closed("Cursor creation is not allowed on a closed " "connection") return Cursor(self) @@ -221,13 +382,23 @@ def connect(dsn=None, host=None, port=None, """ Constructor for creating a connection to the database. - :param str dsn: Data source name (Tarantool URI) - ([[[username[:password]@]host:]port) - :param str host: Server hostname or IP-address - :param int port: Server port - :param str user: Tarantool user - :param str password: User password - :rtype: Connection + :param dsn: **Not implemented**. Tarantool server URI: + ``[[[username[:password]@]host:]port``. + :type dsn: :obj:`str` + + :param host: Refer to :paramref:`~tarantool.Connection.params.host`. + + :param port: Refer to :paramref:`~tarantool.Connection.params.port`. + + :param user: Refer to :paramref:`~tarantool.Connection.params.user`. + + :param password: Refer to + :paramref:`~tarantool.Connection.params.password`. + + :rtype: :class:`~tarantool.Connection` + + :raise: :exc:`~NotImplementedError`, + :class:`~tarantool.Connection` exceptions """ if dsn: diff --git a/tarantool/error.py b/tarantool/error.py index 6bbe012a..b2da32e7 100644 --- a/tarantool/error.py +++ b/tarantool/error.py @@ -1,22 +1,9 @@ # pylint: disable=C0301,W0105,W0401,W0614 -''' -Python DB API compatible exceptions -http://www.python.org/dev/peps/pep-0249/ - -The PEP-249 says that database related exceptions must be inherited as follows: - - Exception - |__Warning - |__Error - |__InterfaceError - |__DatabaseError - |__DataError - |__OperationalError - |__IntegrityError - |__InternalError - |__ProgrammingError - |__NotSupportedError -''' +""" +Python DB API compatible exceptions, see `PEP-249`_. + +.. _PEP-249: http://www.python.org/dev/peps/pep-0249/ +""" import os import socket @@ -31,89 +18,96 @@ class Warning(Exception): - '''Exception raised for important warnings - like data truncations while inserting, etc. ''' + """ + Exception raised for important warnings + like data truncations while inserting, etc. + """ class Error(Exception): - '''Base class for error exceptions''' + """ + Base class for error exceptions. + """ class InterfaceError(Error): - ''' - Exception raised for errors that are related to the database interface - rather than the database itself. - ''' + """ + Exception raised for errors that are related to the database + interface rather than the database itself. + """ class DatabaseError(Error): - '''Exception raised for errors that are related to the database.''' + """ + Exception raised for errors that are related to the database. + """ class DataError(DatabaseError): - ''' - Exception raised for errors that are due to problems with the processed - data like division by zero, numeric value out of range, etc. - ''' + """ + Exception raised for errors that are due to problems with the + processed data like division by zero, numeric value out of range, + etc. + """ class OperationalError(DatabaseError): - ''' - Exception raised for errors that are related to the database's operation - and not necessarily under the control of the programmer, e.g. an - unexpected disconnect occurs, the data source name is not found, - a transaction could not be processed, a memory allocation error occurred - during processing, etc. - ''' + """ + Exception raised for errors that are related to the database's + operation and not necessarily under the control of the programmer, + e.g. an unexpected disconnect occurs, the data source name is not + found, a transaction could not be processed, a memory allocation + error occurred during processing, etc. + """ class IntegrityError(DatabaseError): - ''' - Exception raised when the relational integrity of the database is affected, - e.g. a foreign key check fails. - ''' + """ + Exception raised when the relational integrity of the database is + affected, e.g. a foreign key check fails. + """ class InternalError(DatabaseError): - ''' - Exception raised when the database encounters an internal error, e.g. the - cursor is not valid anymore, the transaction is out of sync, etc. - ''' + """ + Exception raised when the database encounters an internal error, + e.g. the cursor is not valid anymore, the transaction is out of + sync, etc. + """ class ProgrammingError(DatabaseError): - ''' - Exception raised when the database encounters an internal error, e.g. the - cursor is not valid anymore, the transaction is out of sync, etc. - ''' + """ + Exception raised when the database encounters an internal error, + e.g. the cursor is not valid anymore, the transaction is out of + sync, etc. + """ class NotSupportedError(DatabaseError): - ''' - Exception raised in case a method or database API was used which is not - supported by the database, e.g. requesting a .rollback() on a connection - that does not support transaction or has transactions turned off. - ''' + """ + Exception raised in case a method or database API was used which is + not supported by the database, e.g. requesting a .rollback() on a + connection that does not support transactions or has transactions + turned off. + """ class ConfigurationError(Error): - ''' + """ Error of initialization with a user-provided configuration. - ''' + """ class MsgpackError(Error): - ''' - Error with encoding or decoding of MP_EXT types - ''' + """ + Error with encoding or decoding of `MP_EXT`_ types. -class MsgpackWarning(UserWarning): - ''' - Warning with encoding or decoding of MP_EXT types - ''' + .. _MP_EXT: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ + """ -__all__ = ("Warning", "Error", "InterfaceError", "DatabaseError", "DataError", - "OperationalError", "IntegrityError", "InternalError", - "ProgrammingError", "NotSupportedError", "MsgpackError", - "MsgpackWarning") +class MsgpackWarning(UserWarning): + """ + Warning with encoding or decoding of `MP_EXT`_ types. + """ # Monkey patch os.strerror for win32 if sys.platform == "win32": @@ -163,13 +157,13 @@ class MsgpackWarning(UserWarning): os_strerror_orig = os.strerror def os_strerror_patched(code): - ''' + """ Return cross-platform message about socket-related errors This function exists because under Windows os.strerror returns 'Unknown error' on all socket-related errors. And socket-related exception contain broken non-ascii encoded messages. - ''' + """ message = os_strerror_orig(code) if not message.startswith("Unknown"): return message @@ -181,7 +175,15 @@ def os_strerror_patched(code): class SchemaError(DatabaseError): + """ + Error related to extracting space and index schema. + """ + def __init__(self, value): + """ + :param value: Error value. + """ + super(SchemaError, self).__init__(0, value) self.value = value @@ -190,7 +192,19 @@ def __str__(self): class SchemaReloadException(DatabaseError): + """ + Error related to outdated space and index schema. + """ + def __init__(self, message, schema_version): + """ + :param message: Error message. + :type message: :obj:`str` + + :param schema_version: Response schema version. + :type schema_version: :obj:`int` + """ + super(SchemaReloadException, self).__init__(109, message) self.code = 109 self.message = message @@ -201,9 +215,19 @@ def __str__(self): class NetworkError(DatabaseError): - '''Error related to network''' + """ + Error related to network. + """ def __init__(self, orig_exception=None, *args): + """ + :param orig_exception: Exception to wrap. + :type orig_exception: optional + + :param args: Wrapped exception arguments. + :type args: :obj:`tuple` + """ + self.errno = 0 if hasattr(orig_exception, 'errno'): self.errno = orig_exception.errno @@ -220,13 +244,25 @@ def __init__(self, orig_exception=None, *args): class NetworkWarning(UserWarning): - '''Warning related to network''' + """ + Warning related to network. + """ pass class SslError(DatabaseError): - '''Error related to SSL''' + """ + Error related to SSL. + """ def __init__(self, orig_exception=None, *args): + """ + :param orig_exception: Exception to wrap. + :type orig_exception: optional + + :param args: Wrapped exception arguments. + :type args: :obj:`tuple` + """ + self.errno = 0 if hasattr(orig_exception, 'errno'): self.errno = orig_exception.errno @@ -238,22 +274,34 @@ def __init__(self, orig_exception=None, *args): class ClusterDiscoveryWarning(UserWarning): - '''Warning related to cluster discovery''' + """ + Warning related to cluster discovery. + """ pass class ClusterConnectWarning(UserWarning): - '''Warning related to cluster pool connection''' + """ + Warning related to cluster pool connection. + """ pass class PoolTolopogyWarning(UserWarning): - '''Warning related to ro/rw cluster pool topology''' + """ + Warning related to unsatisfying `box.info.ro`_ state of + pool instances. + """ pass class PoolTolopogyError(DatabaseError): - '''Exception raised due to unsatisfying ro/rw cluster pool topology''' + """ + Exception raised due to unsatisfying `box.info.ro`_ state of + pool instances. + + .. _box.info.ro: https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_info/ + """ pass @@ -262,10 +310,17 @@ class PoolTolopogyError(DatabaseError): def warn(message, warning_class): - ''' - Emit warinig message. + """ + Emit a warning message. Just like standard warnings.warn() but don't output full filename. - ''' + + :param message: Warning message. + :type message: :obj:`str` + + :param warning_class: Warning class. + :type warning_class: :class:`~tarantool.error.Warning` + """ + frame = sys._getframe(2) # pylint: disable=W0212 module_name = frame.f_globals.get("__name__") line_no = frame.f_lineno @@ -449,6 +504,16 @@ def warn(message, warning_class): def tnt_strerror(num): + """ + Parse Tarantool error to string data. + + :param num: Tarantool error code. + :type num: :obj:`int` + + :return: Tuple ``(ER_NAME, message)`` or ``"UNDEFINED"``. + :rtype: :obj:`tuple` or :obj:`str` + """ + if num in _strerror: return _strerror[num] return "UNDEFINED" diff --git a/tarantool/mesh_connection.py b/tarantool/mesh_connection.py index 33387fd2..7ce89570 100644 --- a/tarantool/mesh_connection.py +++ b/tarantool/mesh_connection.py @@ -1,7 +1,6 @@ -''' -This module provides the MeshConnection class with automatic switch -between Tarantool instances by the basic round-robin strategy. -''' +""" +This module provides API for interaction with Tarantool servers cluster. +""" import time @@ -42,6 +41,19 @@ def parse_uri(uri): + """ + Parse URI received from cluster discovery function. + + :param uri: URI received from cluster discovery function + :type uri: :obj:`str` + + :return: First value: `{"host": host, "port": port}` or ``None`` in + case of fail, second value: ``None`` or error message in case of + fail. + :rtype: first value: :obj:`dict` or ``None``, + second value: ``None`` or :obj:`str` + """ + # TODO: Support Unix sockets. def parse_error(uri, msg): msg = 'URI "%s": %s' % (uri, msg) @@ -87,6 +99,20 @@ def parse_error(uri, msg): def prepare_address(address): + """ + Validate address dictionary, fill with default values. + For format refer to + :paramref:`~tarantool.ConnectionPool.params.addrs`. + + :param address: Address dictionary. + :type address: :obj:`dict` + + :return: Address dictionary or ``None`` in case of failure, second + value: ``None`` or error message in case of failure. + :rtype: first value: :obj:`dict` or ``None``, + second value: ``None`` or :obj:`str` + """ + def format_error(address, err): return None, 'Address %s: %s' % (str(address), err) @@ -145,6 +171,16 @@ def format_error(address, err): def update_connection(conn, address): + """ + Update connection info after rotation. + + :param conn: Connection mesh to update. + :type conn: :class:`~tarantool.MeshConnection` + + :param address: New active connection address. + :type address: :obj:`dict` + """ + conn.host = address["host"] conn.port = address["port"] conn.transport = address['transport'] @@ -156,12 +192,25 @@ def update_connection(conn, address): class RoundRobinStrategy(object): """ - Simple round-robin address rotation + Defines strategy to choose next pool server after fail. """ + def __init__(self, addrs): + """ + :param addrs: Server addresses list, refer to + :paramref:`~tarantool.ConnectionPool.params.addrs`. + :type addrs: :obj:`list` of :obj:`dict` + """ self.update(addrs) def update(self, new_addrs): + """ + Refresh the strategy state with new addresses. + + :param new_addrs: Updated server addresses list. + :type addrs: :obj:`list` of :obj:`dict` + """ + # Verify new_addrs is a non-empty list. assert new_addrs and isinstance(new_addrs, list) @@ -189,64 +238,21 @@ def update(self, new_addrs): self.pos = new_pos def getnext(self): + """ + Get next cluster server. + + :return: Server address. + :rtype: :obj:`dict` + """ + self.pos = (self.pos + 1) % len(self.addrs) return self.addrs[self.pos] class MeshConnection(Connection): - ''' - Represents a connection to a cluster of Tarantool servers. - - This class uses Connection to connect to one of the nodes of the cluster. - The initial list of nodes is passed to the constructor in the - 'addrs' parameter. The class set in the 'strategy_class' parameter - is used to select a node from the list and switch nodes in case the - current node is unavailable. - - The 'cluster_discovery_function' param of the constructor sets the name - of the stored Lua function used to refresh the list of available nodes. - The function takes no parameters and returns a list of strings in the - format 'host:port'. A generic function for getting the list of nodes - looks like this: - - .. code-block:: lua - - function get_cluster_nodes() - return { - '192.168.0.1:3301', - '192.168.0.2:3302?transport=ssl&ssl_ca_file=/path/to/ca.cert', - -- ... - } - end - - You can put in this list whatever you need, depending on your - cluster topology. Chances are you'll want to derive the list of nodes - from the nodes' replication configuration. Here is an example: - - .. code-block:: lua - - local uri_lib = require('uri') - - function get_cluster_nodes() - local nodes = {} - - local replicas = box.cfg.replication - - for i = 1, #replicas do - local uri = uri_lib.parse(replicas[i]) - - if uri.host and uri.service then - table.insert(nodes, uri.host .. ':' .. uri.service) - end - end - - -- if your replication config doesn't contain the current node, - -- you have to add it manually like this: - table.insert(nodes, '192.168.0.1:3301') - - return nodes - end - ''' + """ + Represents a connection to a cluster of Tarantool servers. + """ def __init__(self, host=None, port=None, user=None, @@ -267,6 +273,147 @@ def __init__(self, host=None, port=None, strategy_class=RoundRobinStrategy, cluster_discovery_function=None, cluster_discovery_delay=CLUSTER_DISCOVERY_DELAY): + """ + :param host: Refer to + :paramref:`~tarantool.Connection.params.host`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param port: Refer to + :paramref:`~tarantool.Connection.params.host`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param user: Refer to + :paramref:`~tarantool.Connection.params.user`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param password: Refer to + :paramref:`~tarantool.Connection.params.password`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param socket_timeout: Refer to + :paramref:`~tarantool.Connection.params.socket_timeout`. + Value would be used for the current active connection. + + :param reconnect_max_attempts: Refer to + :paramref:`~tarantool.Connection.params.reconnect_max_attempts`. + Value would be used for the current active connection. + + :param reconnect_delay: Refer to + :paramref:`~tarantool.Connection.params.reconnect_delay`. + Value would be used for the current active connection. + + :param connect_now: If ``True``, connect to server on + initialization. Otherwise, you have to call + :meth:`~tarantool.MeshConnection.connect` manually after + initialization. + :type connect_now: :obj:`bool`, optional + + :param encoding: Refer to + :paramref:`~tarantool.Connection.params.encoding`. + Value would be used for the current active connection. + + :param call_16: Refer to + :paramref:`~tarantool.Connection.params.call_16`. + Value would be used for the current active connection. + + :param connection_timeout: Refer to + :paramref:`~tarantool.Connection.params.connection_timeout`. + Value would be used for the current active connection. + + :param transport: Refer to + :paramref:`~tarantool.Connection.params.transport`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param ssl_key_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_key_file`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param ssl_cert_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_cert_file`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param ssl_ca_file: Refer to + :paramref:`~tarantool.Connection.params.ssl_ca_file`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param ssl_ciphers: Refer to + :paramref:`~tarantool.Connection.params.ssl_ciphers`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param addrs: Cluster servers addresses list. Refer to + :paramref:`~tarantool.ConnectionPool.params.addrs`. + + :param strategy_class: Strategy for choosing a server after + the current server fails. Defaults to the round-robin + strategy. + :type strategy_class: :obj:`object`, optional + + :param cluster_discovery_function: sets the name of the stored + Lua function used to refresh the list of available nodes. + The function takes no parameters and returns a list of + strings in the format ``'host:port'``. A generic function + for getting the list of nodes looks like this: + + .. code-block:: lua + + function get_cluster_nodes() + return { + '192.168.0.1:3301', + '192.168.0.2:3302?transport=ssl&ssl_ca_file=/path/to/ca.cert', + -- ... + } + end + + You can put in this list whatever you need, depending on + your cluster topology. Chances are you'll want to derive + the list of nodes from the nodes' replication configuration. + Here is an example: + + .. code-block:: lua + + local uri_lib = require('uri') + + function get_cluster_nodes() + local nodes = {} + + local replicas = box.cfg.replication + + for i = 1, #replicas do + local uri = uri_lib.parse(replicas[i]) + + if uri.host and uri.service then + table.insert(nodes, uri.host .. ':' .. uri.service) + end + end + + -- if your replication config doesn't contain the current node, + -- you have to add it manually like this: + table.insert(nodes, '192.168.0.1:3301') + + return nodes + end + + :type cluster_discovery_function: :obj:`str` or :obj:`None`, + optional + + :param cluster_discovery_delay: Minimal time between address + list refresh. + :type cluster_discovery_delay: :obj:`float`, optional + + :raises: :exc:`~tarantool.error.ConfigurationError`, + :class:`~tarantool.Connection` exceptions, + :class:`~tarantool.MeshConnection.connect` exceptions + """ + if addrs is None: addrs = [] else: @@ -323,15 +470,29 @@ def __init__(self, host=None, port=None, ssl_ciphers=addr['ssl_ciphers']) def connect(self): + """ + Create a connection to some server in the cluster. Refresh + addresses info after success. There is no need to call this + method explicitly until you have set ``connect_now=False`` on + initialization. + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :class:`~tarantool.Connection.connect` exceptions + """ super(MeshConnection, self).connect() if self.connected and self.cluster_discovery_function: self._opt_refresh_instances() def _opt_reconnect(self): - ''' - Attempt to connect "reconnect_max_attempts" times to each - available address. - ''' + """ + Attempt to connect + :paramref:`~tarantool.MeshConnection.reconnect_max_attempts` + times to each available address. + + :raise: :class:`~tarantool.Connection.connect` exceptions + """ last_error = None for _ in range(len(self.strategy.addrs)): @@ -348,10 +509,16 @@ def _opt_reconnect(self): raise last_error def _opt_refresh_instances(self): - ''' - Refresh the list of tarantool instances in a cluster. + """ + Refresh the list of Tarantool instances in a cluster. Reconnect if the current instance has disappeared from the list. - ''' + + :raise: :exc:`~AssertionError`, + :exc:`~tarantool.error.SchemaError`, + :exc:`~tarantool.error.NetworkError`, + :class:`~tarantool.MeshConnection._opt_reconnect` exceptions + """ + now = time.time() if not self.connected or not self.cluster_discovery_function or \ @@ -415,18 +582,18 @@ def _opt_refresh_instances(self): self._opt_reconnect() def _send_request(self, request): - ''' - Update the instances list if `cluster_discovery_function` - is provided and the last update was more than - `cluster_discovery_delay` seconds ago. + """ + Send a request to a Tarantool server. If required, refresh + addresses list before sending a request. + + :param request: Request to send. + :type request: :class:`~tarantool.request.Request` - After that, perform a request as usual and return an instance of - the `Response` class. + :rtype: :class:`~tarantool.response.Response` - :param request: object representing a request - :type request: `Request` instance + :raise: :class:`~tarantool.MeshConnection._opt_reconnect` exceptions, + :class:`~tarantool.Connection._send_request` exceptions + """ - :rtype: `Response` instance - ''' self._opt_refresh_instances() return super(MeshConnection, self)._send_request(request) diff --git a/tarantool/msgpack_ext/datetime.py b/tarantool/msgpack_ext/datetime.py index 70f56dc9..e47f162e 100644 --- a/tarantool/msgpack_ext/datetime.py +++ b/tarantool/msgpack_ext/datetime.py @@ -1,9 +1,44 @@ +""" +Tarantool `datetime`_ extension type support module. + +Refer to :mod:`~tarantool.msgpack_ext.types.datetime`. + +.. _datetime: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type +""" + from tarantool.msgpack_ext.types.datetime import Datetime EXT_ID = 4 +""" +`datetime`_ type id. +""" def encode(obj): + """ + Encode a datetime object. + + :param obj: Datetime to encode. + :type: :obj: :class:`tarantool.Datetime` + + :return: Encoded datetime. + :rtype: :obj:`bytes` + + :raise: :exc:`tarantool.Datetime.msgpack_encode` exceptions + """ + return obj.msgpack_encode() def decode(data): + """ + Decode a datetime object. + + :param obj: Datetime to decode. + :type obj: :obj:`bytes` + + :return: Decoded datetime. + :rtype: :class:`tarantool.Datetime` + + :raise: :exc:`tarantool.Datetime` exceptions + """ + return Datetime(data) diff --git a/tarantool/msgpack_ext/decimal.py b/tarantool/msgpack_ext/decimal.py index 616024b1..80e40051 100644 --- a/tarantool/msgpack_ext/decimal.py +++ b/tarantool/msgpack_ext/decimal.py @@ -1,53 +1,85 @@ +""" +Tarantool `decimal`_ extension type support module. + +The decimal MessagePack representation looks like this: + +.. code-block:: text + + +--------+-------------------+------------+===============+ + | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | + +--------+-------------------+------------+===============+ + +``PackedDecimal`` has the following structure: + +.. code-block:: text + + <--- length bytes --> + +-------+=============+ + | scale | BCD | + +-------+=============+ + +Here the scale is either `mp_int`_ or `mp_uint`_. Scale is the number +of digits after the decimal point + +BCD is a sequence of bytes representing decimal digits of the encoded +number (each byte has two decimal digits each encoded using 4-bit +nibbles), so ``byte >> 4`` is the first digit and ``byte & 0x0f`` is +the second digit. The leftmost digit in the array is the most +significant. The rightmost digit in the array is the least significant. + +The first byte of the ``BCD`` array contains the first digit of the number, +represented as follows: + +.. code-block:: text + + | 4 bits | 4 bits | + = 0x = the 1st digit + +(The first nibble contains ``0`` if the decimal number has an even +number of digits.) The last byte of the ``BCD`` array contains the last +digit of the number and the final nibble, represented as follows: + +.. code-block:: text + + | 4 bits | 4 bits | + = the last digit = nibble + +The final nibble represents the number’s sign: + +* ``0x0a``, ``0x0c``, ``0x0e``, ``0x0f`` stand for plus, +* ``0x0b`` and ``0x0d`` stand for minus. + +.. _decimal: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type +.. _mp_int: +.. _mp_uint: https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family +""" + from decimal import Decimal from tarantool.error import MsgpackError, MsgpackWarning, warn -# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type -# -# The decimal MessagePack representation looks like this: -# +--------+-------------------+------------+===============+ -# | MP_EXT | length (optional) | MP_DECIMAL | PackedDecimal | -# +--------+-------------------+------------+===============+ -# -# PackedDecimal has the following structure: -# -# <--- length bytes --> -# +-------+=============+ -# | scale | BCD | -# +-------+=============+ -# -# Here scale is either MP_INT or MP_UINT. -# scale = number of digits after the decimal point -# -# BCD is a sequence of bytes representing decimal digits of the encoded number -# (each byte has two decimal digits each encoded using 4-bit nibbles), so -# byte >> 4 is the first digit and byte & 0x0f is the second digit. The -# leftmost digit in the array is the most significant. The rightmost digit in -# the array is the least significant. -# -# The first byte of the BCD array contains the first digit of the number, -# represented as follows: -# -# | 4 bits | 4 bits | -# = 0x = the 1st digit -# -# (The first nibble contains 0 if the decimal number has an even number of -# digits.) The last byte of the BCD array contains the last digit of the number -# and the final nibble, represented as follows: -# -# | 4 bits | 4 bits | -# = the last digit = nibble -# -# The final nibble represents the number’s sign: -# -# 0x0a, 0x0c, 0x0e, 0x0f stand for plus, -# 0x0b and 0x0d stand for minus. - EXT_ID = 1 +""" +`decimal`_ type id. +""" TARANTOOL_DECIMAL_MAX_DIGITS = 38 def get_mp_sign(sign): + """ + Parse decimal sign to a nibble. + + :param sign: ``'+`` or ``'-'`` symbol. + :type sign: :obj:`str` + + :return: Decimal sigh nibble. + :rtype: :obj:`int` + + :raise: :exc:`RuntimeError` + + :meta private: + """ + if sign == '+': return 0x0c @@ -57,51 +89,91 @@ def get_mp_sign(sign): raise RuntimeError def add_mp_digit(digit, bytes_reverted, digit_count): + """ + Append decimal digit to a binary data array. + + :param digit: Digit to add. + :type digit: :obj:`int` + + :param bytes_reverted: Reverted array with binary data. + :type bytes_reverted: :obj:`bytearray` + + :param digit_count: Current digit count. + :type digit_count: :obj:`int` + + :meta private: + """ + if digit_count % 2 == 0: bytes_reverted[-1] = bytes_reverted[-1] | (digit << 4) else: bytes_reverted.append(digit) def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): -# Decimal numbers have 38 digits of precision, that is, the total number of -# digits before and after the decimal point can be 38. If there are more -# digits arter the decimal point, the precision is lost. If there are more -# digits before the decimal point, error is thrown. -# -# Tarantool 2.10.1-0-g482d91c66 -# -# tarantool> decimal.new('10000000000000000000000000000000000000') -# --- -# - 10000000000000000000000000000000000000 -# ... -# -# tarantool> decimal.new('100000000000000000000000000000000000000') -# --- -# - error: '[string "return VERSION"]:1: variable ''VERSION'' is not declared' -# ... -# -# tarantool> decimal.new('1.0000000000000000000000000000000000001') -# --- -# - 1.0000000000000000000000000000000000001 -# ... -# -# tarantool> decimal.new('1.00000000000000000000000000000000000001') -# --- -# - 1.0000000000000000000000000000000000000 -# ... -# -# In fact, there is also an exceptional case: if decimal starts with `0.`, -# 38 digits after the decimal point are supported without the loss of precision. -# -# tarantool> decimal.new('0.00000000000000000000000000000000000001') -# --- -# - 0.00000000000000000000000000000000000001 -# ... -# -# tarantool> decimal.new('0.000000000000000000000000000000000000001') -# --- -# - 0.00000000000000000000000000000000000000 -# ... + """ + Decimal numbers have 38 digits of precision, that is, the total + number of digits before and after the decimal point can be 38. If + there are more digits arter the decimal point, the precision is + lost. If there are more digits before the decimal point, error is + thrown (Tarantool 2.10.1-0-g482d91c66). + + .. code-block:: lua + + tarantool> decimal.new('10000000000000000000000000000000000000') + --- + - 10000000000000000000000000000000000000 + ... + + tarantool> decimal.new('100000000000000000000000000000000000000') + --- + - error: incorrect value to convert to decimal as 1 argument + ... + + tarantool> decimal.new('1.0000000000000000000000000000000000001') + --- + - 1.0000000000000000000000000000000000001 + ... + + tarantool> decimal.new('1.00000000000000000000000000000000000001') + --- + - 1.0000000000000000000000000000000000000 + ... + + In fact, there is also an exceptional case: if decimal starts with + ``0.``, 38 digits after the decimal point are supported without the + loss of precision. + + .. code-block:: lua + + tarantool> decimal.new('0.00000000000000000000000000000000000001') + --- + - 0.00000000000000000000000000000000000001 + ... + + tarantool> decimal.new('0.000000000000000000000000000000000000001') + --- + - 0.00000000000000000000000000000000000000 + ... + + :param str_repr: Decimal string representation. + :type str_repr: :obj:`str` + + :param scale: Decimal scale. + :type scale: :obj:`int` + + :param first_digit_ind: Index of the first digit in decimal string + representation. + :type first_digit_ind: :obj:`int` + + :return: ``True``, if decimal can be encoded to Tarantool decimal + without precision loss. ``False`` otherwise. + :rtype: :obj:`bool` + + :raise: :exc:`~tarantool.error.MsgpackError` + + :meta private: + """ + if scale > 0: digit_count = len(str_repr) - 1 - first_digit_ind else: @@ -127,6 +199,23 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): return True def strip_decimal_str(str_repr, scale, first_digit_ind): + """ + Strip decimal digits after the decimal point if decimal cannot be + represented as Tarantool decimal without precision loss. + + :param str_repr: Decimal string representation. + :type str_repr: :obj:`str` + + :param scale: Decimal scale. + :type scale: :obj:`int` + + :param first_digit_ind: Index of the first digit in decimal string + representation. + :type first_digit_ind: :obj:`int` + + :meta private: + """ + assert scale > 0 # Strip extra bytes str_repr = str_repr[:TARANTOOL_DECIMAL_MAX_DIGITS + 1 + first_digit_ind] @@ -137,6 +226,18 @@ def strip_decimal_str(str_repr, scale, first_digit_ind): return str_repr def encode(obj): + """ + Encode a decimal object. + + :param obj: Decimal to encode. + :type obj: :obj:`decimal.Decimal` + + :return: Encoded decimal. + :rtype: :obj:`bytes` + + :raise: :exc:`~tarantool.error.MsgpackError` + """ + # Non-scientific string with trailing zeroes removed str_repr = format(obj, 'f') @@ -186,6 +287,20 @@ def encode(obj): def get_str_sign(nibble): + """ + Parse decimal sign nibble to a symbol. + + :param nibble: Decimal sign nibble. + :type nibble: :obj:`int` + + :return: ``'+`` or ``'-'`` symbol. + :rtype: :obj:`str` + + :raise: :exc:`MsgpackError` + + :meta private: + """ + if nibble == 0x0a or nibble == 0x0c or nibble == 0x0e or nibble == 0x0f: return '+' @@ -195,6 +310,23 @@ def get_str_sign(nibble): raise MsgpackError('Unexpected MP_DECIMAL sign nibble') def add_str_digit(digit, digits_reverted, scale): + """ + Append decimal digit to a binary data array. + + :param digit: Digit to add. + :type digit: :obj:`int` + + :param digits_reverted: Reverted decimal string. + :type digits_reverted: :obj:`str` + + :param scale: Decimal scale. + :type scale: :obj:`int` + + :raise: :exc:`~tarantool.error.MsgpackError` + + :meta private: + """ + if not (0 <= digit <= 9): raise MsgpackError('Unexpected MP_DECIMAL digit nibble') @@ -204,6 +336,18 @@ def add_str_digit(digit, digits_reverted, scale): digits_reverted.append(str(digit)) def decode(data): + """ + Decode a decimal object. + + :param obj: Decimal to decode. + :type obj: :obj:`bytes` + + :return: Decoded decimal. + :rtype: :obj:`decimal.Decimal` + + :raise: :exc:`~tarantool.error.MsgpackError` + """ + scale = data[0] sign = get_str_sign(data[-1] & 0x0f) diff --git a/tarantool/msgpack_ext/interval.py b/tarantool/msgpack_ext/interval.py index 79b5a8de..20a791ef 100644 --- a/tarantool/msgpack_ext/interval.py +++ b/tarantool/msgpack_ext/interval.py @@ -1,9 +1,44 @@ +""" +Tarantool `datetime.interval`_ extension type support module. + +Refer to :mod:`~tarantool.msgpack_ext.types.interval`. + +.. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type +""" + from tarantool.msgpack_ext.types.interval import Interval EXT_ID = 6 +""" +`datetime.interval`_ type id. +""" def encode(obj): + """ + Encode an interval object. + + :param obj: Interval to encode. + :type: :obj: :class:`tarantool.Interval` + + :return: Encoded interval. + :rtype: :obj:`bytes` + + :raise: :exc:`tarantool.Interval.msgpack_encode` exceptions + """ + return obj.msgpack_encode() def decode(data): + """ + Decode an interval object. + + :param obj: Interval to decode. + :type obj: :obj:`bytes` + + :return: Decoded interval. + :rtype: :class:`tarantool.Interval` + + :raise: :exc:`tarantool.Interval` exceptions + """ + return Interval(data) diff --git a/tarantool/msgpack_ext/packer.py b/tarantool/msgpack_ext/packer.py index d41c411d..4706496f 100644 --- a/tarantool/msgpack_ext/packer.py +++ b/tarantool/msgpack_ext/packer.py @@ -1,3 +1,9 @@ +""" +Tarantool `extension`_ types encoding support. + +.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +""" + from decimal import Decimal from uuid import UUID from msgpack import ExtType @@ -18,6 +24,19 @@ ] def default(obj): + """ + :class:`msgpack.Packer` encoder. + + :param obj: Object to encode. + :type obj: :class:`decimal.Decimal` or :class:`uuid.UUID` or + :class:`tarantool.Datetime` or :class:`tarantool.Interval` + + :return: Encoded value. + :rtype: :class:`msgpack.ExtType` + + :raise: :exc:`~TypeError` + """ + for encoder in encoders: if isinstance(obj, encoder['type']): return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj)) diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py index d89c1cae..d84352dd 100644 --- a/tarantool/msgpack_ext/types/datetime.py +++ b/tarantool/msgpack_ext/types/datetime.py @@ -1,3 +1,43 @@ +""" +Tarantool `datetime`_ extension type support module. + +The datetime MessagePack representation looks like this: + +.. code-block:: text + + +---------+----------------+==========+-----------------+ + | MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; | + | = d7/d8 | = 4 | | tzindex; | + +---------+----------------+==========+-----------------+ + +MessagePack data contains: + +* Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the + little-endian order. +* The optional fields (8 bytes), if any of them have a non-zero value. + The fields include nsec (4 bytes), tzoffset (2 bytes), and + tzindex (2 bytes) packed in the little-endian order. + +``seconds`` is seconds since Epoch, where the epoch is the point where +the time starts, and is platform dependent. For Unix, the epoch is +January 1, 1970, 00:00:00 (UTC). Tarantool uses a ``double`` type, see a +structure definition in src/lib/core/datetime.h and reasons in +`datetime RFC`_. + +``nsec`` is nanoseconds, fractional part of seconds. Tarantool uses +``int32_t``, see a definition in src/lib/core/datetime.h. + +``tzoffset`` is timezone offset in minutes from UTC. Tarantool uses +``int16_t`` type, see a structure definition in src/lib/core/datetime.h. + +``tzindex`` is Olson timezone id. Tarantool uses ``int16_t`` type, see +a structure definition in src/lib/core/datetime.h. If both +``tzoffset`` and ``tzindex`` are specified, ``tzindex`` has the +preference and the ``tzoffset`` value is ignored. + +.. _datetime RFC: https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c +""" + from copy import deepcopy import pandas @@ -8,37 +48,6 @@ from tarantool.msgpack_ext.types.interval import Interval, Adjust -# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type -# -# The datetime MessagePack representation looks like this: -# +---------+----------------+==========+-----------------+ -# | MP_EXT | MP_DATETIME | seconds | nsec; tzoffset; | -# | = d7/d8 | = 4 | | tzindex; | -# +---------+----------------+==========+-----------------+ -# MessagePack data contains: -# -# * Seconds (8 bytes) as an unencoded 64-bit signed integer stored in the -# little-endian order. -# * The optional fields (8 bytes), if any of them have a non-zero value. -# The fields include nsec (4 bytes), tzoffset (2 bytes), and -# tzindex (2 bytes) packed in the little-endian order. -# -# seconds is seconds since Epoch, where the epoch is the point where the time -# starts, and is platform dependent. For Unix, the epoch is January 1, -# 1970, 00:00:00 (UTC). Tarantool uses a double type, see a structure -# definition in src/lib/core/datetime.h and reasons in -# https://github.com/tarantool/tarantool/wiki/Datetime-internals#intervals-in-c -# -# nsec is nanoseconds, fractional part of seconds. Tarantool uses int32_t, see -# a definition in src/lib/core/datetime.h. -# -# tzoffset is timezone offset in minutes from UTC. Tarantool uses a int16_t type, -# see a structure definition in src/lib/core/datetime.h. -# -# tzindex is Olson timezone id. Tarantool uses a int16_t type, see a structure -# definition in src/lib/core/datetime.h. If both tzoffset and tzindex are -# specified, tzindex has the preference and the tzoffset value is ignored. - SECONDS_SIZE_BYTES = 8 NSEC_SIZE_BYTES = 4 TZOFFSET_SIZE_BYTES = 2 @@ -52,13 +61,61 @@ MONTH_IN_YEAR = 12 def get_bytes_as_int(data, cursor, size): + """ + Get integer value from binary data. + + :param data: MessagePack binary data. + :type data: :obj:`bytes` + + :param cursor: Index after last parsed byte. + :type cursor: :obj:`int` + + :param size: Integer size, in bytes. + :type size: :obj:`int` + + :return: First value: parsed integer, second value: new cursor + position. + :rtype: first value: :obj:`int`, second value: :obj:`int` + + :meta private: + """ + part = data[cursor:cursor + size] return int.from_bytes(part, BYTEORDER, signed=True), cursor + size def get_int_as_bytes(data, size): + """ + Get binary representation of integer value. + + :param data: Integer value. + :type data: :obj:`int` + + :param size: Integer size, in bytes. + :type size: :obj:`int` + + :return: Encoded integer. + :rtype: :obj:`bytes` + + :meta private: + """ + return data.to_bytes(size, byteorder=BYTEORDER, signed=True) def compute_offset(timestamp): + """ + Compute timezone offset. Offset is computed each time and not stored + since it could depend on current datetime value. It is expected that + timestamp offset is not ``None``. + + :param timestamp: Timestamp data. + :type timestamp: :class:`pandas.Timestamp` + + :return: Timezone offset, in minutes. + :rtype: :obj:`int` + + :meta private: + """ + utc_offset = timestamp.tzinfo.utcoffset(timestamp) # `None` offset is a valid utcoffset implementation, @@ -70,6 +127,28 @@ def compute_offset(timestamp): return int(utc_offset.total_seconds()) // SEC_IN_MIN def get_python_tzinfo(tz, error_class): + """ + All non-abbreviated Tarantool timezones are represented as pytz + timezones (from :func:`pytz.timezone`). All non-ambiguous + abbreviated Tarantool timezones are represented as + :class:`pytz.FixedOffset` timezones. Attempt to build timezone + info for ambiguous timezone results in raising the exception, same + as in Tarantool. + + :param tz: Tarantool timezone name. + :type tz: :obj:`str` + + :param error_class: Error class to raise in case of fail. + :type error_class: :obj:`Exception` + + :return: Timezone object. + :rtype: :func:`pytz.timezone` result or :class:`pytz.FixedOffset` + + :raise: :exc:`~tarantool.msgpack_ext.types.datetime.get_python_tzinfo.params.error_class` + + :meta private: + """ + if tz in pytz.all_timezones: return pytz.timezone(tz) @@ -81,6 +160,23 @@ def get_python_tzinfo(tz, error_class): return pytz.FixedOffset(tt_tzinfo['offset']) def msgpack_decode(data): + """ + Decode MsgPack binary data to useful timestamp and timezone data. + For internal use of :class:`~tarantool.Datetime`. + + :param data: MessagePack binary data to decode. + :type data: :obj:`bytes` + + :return: First value: timestamp data with timezone info, second + value: Tarantool timezone name. + :rtype: first value: :class:`pandas.Timestamp`, second value: + :obj:`str` + + :raises: :exc:`~tarantool.error.MsgpackError` + + :meta private: + """ + cursor = 0 seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES) @@ -113,9 +209,145 @@ def msgpack_decode(data): return datetime, '' class Datetime(): + """ + Class representing Tarantool `datetime`_ info. Internals are based + on :class:`pandas.Timestamp`. + + You can create :class:`~tarantool.Datetime` objects either from + MessagePack data or by using the same API as in Tarantool: + + .. code-block:: python + + dt1 = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321) + + dt2 = tarantool.Datetime(timestamp=1661969274) + + dt3 = tarantool.Datetime(timestamp=1661969274, nsec=308543321) + + :class:`~tarantool.Datetime` exposes + :attr:`~tarantool.Datetime.year`, + :attr:`~tarantool.Datetime.month`, + :attr:`~tarantool.Datetime.day`, + :attr:`~tarantool.Datetime.hour`, + :attr:`~tarantool.Datetime.minute`, + :attr:`~tarantool.Datetime.sec`, + :attr:`~tarantool.Datetime.nsec`, + :attr:`~tarantool.Datetime.timestamp` and + :attr:`~tarantool.Datetime.value` (integer epoch time with + nanoseconds precision) properties if you need to convert + :class:`~tarantool.Datetime` to any other kind of datetime object: + + .. code-block:: python + + pdt = pandas.Timestamp(year=dt.year, month=dt.month, day=dt.day, + hour=dt.hour, minute=dt.minute, second=dt.sec, + microsecond=(dt.nsec // 1000), + nanosecond=(dt.nsec % 1000)) + + Use :paramref:`~tarantool.Datetime.params.tzoffset` parameter to set + up offset timezone: + + .. code-block:: python + + dt = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321, tzoffset=180) + + You may use the :attr:`~tarantool.Datetime.tzoffset` property to + get the timezone offset of a datetime object. + + Use :paramref:`~tarantool.Datetime.params.tz` parameter to set up + timezone name: + + .. code-block:: python + + dt = tarantool.Datetime(year=2022, month=8, day=31, + hour=18, minute=7, sec=54, + nsec=308543321, tz='Europe/Moscow') + + If both :paramref:`~tarantool.Datetime.params.tz` and + :paramref:`~tarantool.Datetime.params.tzoffset` are specified, + :paramref:`~tarantool.Datetime.params.tz` is used. + + You may use the :attr:`~tarantool.Datetime.tz` property to get + the timezone name of a datetime object. + + .. _datetime: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type + """ + def __init__(self, data=None, *, timestamp=None, year=None, month=None, day=None, hour=None, minute=None, sec=None, nsec=None, tzoffset=0, tz=''): + """ + :param data: MessagePack binary data to decode. If provided, + all other parameters are ignored. + :type data: :obj:`bytes`, optional + + :param timestamp: Timestamp since epoch. Cannot be provided + together with + :paramref:`~tarantool.Datetime.params.year`, + :paramref:`~tarantool.Datetime.params.month`, + :paramref:`~tarantool.Datetime.params.day`, + :paramref:`~tarantool.Datetime.params.hour`, + :paramref:`~tarantool.Datetime.params.minute`, + :paramref:`~tarantool.Datetime.params.sec`. + If :paramref:`~tarantool.Datetime.params.nsec` is provided, + it must be :obj:`int`. + :type timestamp: :obj:`float` or :obj:`int`, optional + + :param year: Datetime year value. Must be a valid + :class:`pandas.Timestamp` ``year`` parameter. + Must be provided unless the object is built with + :paramref:`~tarantool.Datetime.params.data` or + :paramref:`~tarantool.Datetime.params.timestamp`. + :type year: :obj:`int`, optional + + :param month: Datetime month value. Must be a valid + :class:`pandas.Timestamp` ``month`` parameter. + Must be provided unless the object is built with + :paramref:`~tarantool.Datetime.params.data` or + :paramref:`~tarantool.Datetime.params.timestamp`. + :type month: :obj:`int`, optional + + :param day: Datetime day value. Must be a valid + :class:`pandas.Timestamp` ``day`` parameter. + Must be provided unless the object is built with + :paramref:`~tarantool.Datetime.params.data` or + :paramref:`~tarantool.Datetime.params.timestamp`. + :type day: :obj:`int`, optional + + :param hour: Datetime hour value. Must be a valid + :class:`pandas.Timestamp` ``hour`` parameter. + :type hour: :obj:`int`, optional + + :param minute: Datetime minute value. Must be a valid + :class:`pandas.Timestamp` ``minute`` parameter. + :type minute: :obj:`int`, optional + + :param sec: Datetime seconds value. Must be a valid + :class:`pandas.Timestamp` ``second`` parameter. + :type sec: :obj:`int`, optional + + :param nsec: Datetime nanoseconds value. Quotient of a division + by 1000 (nanoseconds in microseconds) must be a valid + :class:`pandas.Timestamp` ``microsecond`` parameter, + remainder of a division by 1000 must be a valid + :class:`pandas.Timestamp` ``nanosecond`` parameter. + :type sec: :obj:`int`, optional + + :param tzoffset: Timezone offset. Ignored, if provided together + with :paramref:`~tarantool.Datetime.params.tz`. + :type tzoffset: :obj:`int`, optional + + :param tz: Timezone name from Olson timezone database. + :type tz: :obj:`str`, optional + + :raise: :exc:`ValueError`, :exc:`~tarantool.error.MsgpackError`, + :class:`pandas.Timestamp` exceptions + """ + if data is not None: if not isinstance(data, bytes): raise ValueError('data argument (first positional argument) ' + @@ -172,6 +404,22 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None, self._tz = '' def _interval_operation(self, other, sign=1): + """ + Implementation of :class:`~tarantool.Interval` addition and + subtraction. + + :param other: Interval to add or subtract. + :type other: :class:`~tarantool.Interval` + + :param sign: Right operand multiplier: ``1`` for addition, + ``-1`` for subtractiom. + :type sign: :obj:`int` + + :rtype: :class:`~tarantool.Datetime` + + :meta private: + """ + self_dt = self._datetime # https://github.com/tarantool/tarantool/wiki/Datetime-Internals#date-adjustions-and-leap-years @@ -206,12 +454,94 @@ def _interval_operation(self, other, sign=1): tzoffset=tzoffset, tz=self.tz) def __add__(self, other): + """ + Valid operations: + + * :class:`~tarantool.Datetime` + :class:`~tarantool.Interval` + = :class:`~tarantool.Datetime` + + Since :class:`~tarantool.Interval` could contain + :paramref:`~tarantool.Interval.params.month` and + :paramref:`~tarantool.Interval.params.year` fields and such + operations could be ambiguous, you can use the + :paramref:`~tarantool.Interval.params.adjust` field to tune the + logic. The behavior is the same as in Tarantool, see + `Interval arithmetic RFC`_. + + * :attr:`tarantool.IntervalAdjust.NONE ` + -- only truncation toward the end of month is performed (default + mode). + + .. code-block:: python + + >>> dt = tarantool.Datetime(year=2022, month=3, day=31) + datetime: Timestamp('2022-03-31 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.NONE) + >>> dt + di + datetime: Timestamp('2022-04-30 00:00:00'), tz: "" + + * :attr:`tarantool.IntervalAdjust.EXCESS ` + -- overflow mode, + without any snap or truncation to the end of month, straight + addition of days in month, stopping over month boundaries if + there is less number of days. + + .. code-block:: python + + >>> dt = tarantool.Datetime(year=2022, month=1, day=31) + datetime: Timestamp('2022-01-31 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.EXCESS) + >>> dt + di + datetime: Timestamp('2022-03-02 00:00:00'), tz: "" + + * :attr:`tarantool.IntervalAdjust.LAST ` + -- mode when day snaps to the end of month, if it happens. + + .. code-block:: python + + >>> dt = tarantool.Datetime(year=2022, month=2, day=28) + datetime: Timestamp('2022-02-28 00:00:00'), tz: "" + >>> di = tarantool.Interval(month=1, adjust=tarantool.IntervalAdjust.LAST) + >>> dt + di + datetime: Timestamp('2022-03-31 00:00:00'), tz: "" + + :param other: Second operand. + :type other: :class:`~tarantool.Interval` + + :rtype: :class:`~tarantool.Datetime` + + :raise: :exc:`TypeError` + + .. _Interval arithmetic RFC: https://github.com/tarantool/tarantool/wiki/Datetime-Internals#interval-arithmetic + """ + if not isinstance(other, Interval): raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") return self._interval_operation(other, sign=1) def __sub__(self, other): + """ + Valid operations: + + * :class:`~tarantool.Datetime` - :class:`~tarantool.Interval` + = :class:`~tarantool.Datetime` + * :class:`~tarantool.Datetime` - :class:`~tarantool.Datetime` + = :class:`~tarantool.Interval` + + Refer to :meth:`~tarantool.Datetime.__add__` for interval + adjustment rules. + + :param other: Second operand. + :type other: :class:`~tarantool.Interval` or + :class:`~tarantool.Datetime` + + :rtype: :class:`~tarantool.Datetime` or + :class:`~tarantool.Interval` + + :raise: :exc:`TypeError` + """ + if isinstance(other, Datetime): self_dt = self._datetime other_dt = other._datetime @@ -249,6 +579,16 @@ def __sub__(self, other): raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'") def __eq__(self, other): + """ + Datetimes are equal when underlying datetime infos are equal. + + :param other: Second operand. + :type other: :class:`~tarantool.Datetime` or + :class:`~pandas.Timestamp` + + :rtype: :obj:`bool` + """ + if isinstance(other, Datetime): return self._datetime == other._datetime elif isinstance(other, pandas.Timestamp): @@ -278,52 +618,123 @@ def __deepcopy__(self, memo): @property def year(self): + """ + Datetime year. + + :rtype: :obj:`int` + """ + return self._datetime.year @property def month(self): + """ + Datetime month. + + :rtype: :obj:`int` + """ + return self._datetime.month @property def day(self): + """ + Datetime day. + + :rtype: :obj:`int` + """ + return self._datetime.day @property def hour(self): + """ + Datetime day. + + :rtype: :obj:`int` + """ + return self._datetime.hour @property def minute(self): + """ + Datetime minute. + + :rtype: :obj:`int` + """ + return self._datetime.minute @property def sec(self): + """ + Datetime seconds. + + :rtype: :obj:`int` + """ + return self._datetime.second @property def nsec(self): - # microseconds + nanoseconds + """ + Datetime nanoseconds (everything less than seconds is included). + + :rtype: :obj:`int` + """ + return self._datetime.value % NSEC_IN_SEC @property def timestamp(self): + """ + Datetime time since epoch, in seconds. + + :rtype: :obj:`float` + """ + return self._datetime.timestamp() @property def tzoffset(self): + """ + Datetime current timezone offset. + + :rtype: :obj:`int` + """ + if self._datetime.tzinfo is not None: return compute_offset(self._datetime) return 0 @property def tz(self): + """ + Datetime timezone name. + + :rtype: :obj:`str` + """ + return self._tz @property def value(self): + """ + Datetime time since epoch, in nanoseconds. + + :rtype: :obj:`int` + """ + return self._datetime.value def msgpack_encode(self): + """ + Encode a datetime object. + + :rtype: :obj:`bytes` + """ + seconds = self.value // NSEC_IN_SEC nsec = self.nsec tzoffset = self.tzoffset diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index d7caeb9f..62d98145 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -1,37 +1,48 @@ +""" +Tarantool `datetime.interval`_ extension type support module. + +The interval MessagePack representation looks like this: + +.. code-block:: text + + +--------+-------------------------+-------------+----------------+ + | MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval | + +--------+-------------------------+-------------+----------------+ + +Packed interval consists of: + +* Packed number of non-zero fields. +* Packed non-null fields. + +Each packed field has the following structure: + +.. code-block:: text + + +----------+=====================+ + | field ID | field value | + +----------+=====================+ + +The number of defined (non-null) fields can be zero. In this case, +the packed interval will be encoded as integer 0. + +List of the field IDs: + +* 0 – year +* 1 – month +* 2 – week +* 3 – day +* 4 – hour +* 5 – minute +* 6 – second +* 7 – nanosecond +* 8 – adjust +""" + import msgpack from enum import Enum from tarantool.error import MsgpackError -# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type -# -# The interval MessagePack representation looks like this: -# +--------+-------------------------+-------------+----------------+ -# | MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval | -# +--------+-------------------------+-------------+----------------+ -# Packed interval consists of: -# - Packed number of non-zero fields. -# - Packed non-null fields. -# -# Each packed field has the following structure: -# +----------+=====================+ -# | field ID | field value | -# +----------+=====================+ -# -# The number of defined (non-null) fields can be zero. In this case, -# the packed interval will be encoded as integer 0. -# -# List of the field IDs: -# - 0 – year -# - 1 – month -# - 2 – week -# - 3 – day -# - 4 – hour -# - 5 – minute -# - 6 – second -# - 7 – nanosecond -# - 8 – adjust - id_map = { 0: 'year', 1: 'month', @@ -46,16 +57,87 @@ # https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.h#L34 class Adjust(Enum): - EXCESS = 0 # DT_EXCESS in c-dt, "excess" in Tarantool - NONE = 1 # DT_LIMIT in c-dt, "none" in Tarantool - LAST = 2 # DT_SNAP in c-dt, "last" in Tarantool + """ + Interval adjustment mode for year and month arithmetic. Refer to + :meth:`~tarantool.Datetime.__add__`. + """ + + EXCESS = 0 + """ + Overflow mode. + """ + + NONE = 1 + """ + Only truncation toward the end of month is performed. + """ + + LAST = 2 + """ + Mode when day snaps to the end of month, if it happens. + """ class Interval(): + """ + Class representing Tarantool `datetime.interval`_ info. + + You can create :class:`~tarantool.Interval` objects either + from MessagePack data or by using the same API as in Tarantool: + + .. code-block:: python + + di = tarantool.Interval(year=-1, month=2, day=3, + hour=4, minute=-5, sec=6, + nsec=308543321, + adjust=tarantool.IntervalAdjust.NONE) + + Its attributes (same as in init API) are exposed, so you can + use them if needed. + + .. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type + """ + def __init__(self, data=None, *, year=0, month=0, week=0, day=0, hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE): - # If msgpack data does not contain a field value, it is zero. - # If built not from msgpack data, set argument values later. + """ + :param data: MessagePack binary data to decode. If provided, + all other parameters are ignored. + :type data: :obj:`bytes`, optional + + :param year: Interval year value. + :type year: :obj:`int`, optional + + :param month: Interval month value. + :type month: :obj:`int`, optional + + :param week: Interval week value. + :type week: :obj:`int`, optional + + :param day: Interval day value. + :type day: :obj:`int`, optional + + :param hour: Interval hour value. + :type hour: :obj:`int`, optional + + :param minute: Interval minute value. + :type minute: :obj:`int`, optional + + :param sec: Interval seconds value. + :type sec: :obj:`int`, optional + + :param nsec: Interval nanoseconds value. + :type nsec: :obj:`int`, optional + + :param adjust: Interval adjustment rule. Refer to + :meth:`~tarantool.Datetime.__add__`. + :type adjust: :class:`~tarantool.IntervalAdjust`, optional + + :raise: :exc:`ValueError` + """ + + # If MessagePack data does not contain a field value, it is zero. + # If built not from MessagePack data, set argument values later. self.year = 0 self.month = 0 self.week = 0 @@ -107,6 +189,22 @@ def __init__(self, data=None, *, year=0, month=0, week=0, self.adjust = adjust def __add__(self, other): + """ + Valid operations: + + * :class:`~tarantool.Interval` + :class:`~tarantool.Interval` + = :class:`~tarantool.Interval` + + Adjust of the first operand is used in result. + + :param other: Second operand. + :type other: :class:`~tarantool.Interval` + + :rtype: :class:`~tarantool.Interval` + + :raise: :exc:`TypeError` + """ + if not isinstance(other, Interval): raise TypeError(f"unsupported operand type(s) for +: '{type(self)}' and '{type(other)}'") @@ -139,6 +237,22 @@ def __add__(self, other): ) def __sub__(self, other): + """ + Valid operations: + + * :class:`~tarantool.Interval` - :class:`~tarantool.Interval` + = :class:`~tarantool.Interval` + + Adjust of the first operand is used in result. + + :param other: Second operand. + :type other: :class:`~tarantool.Interval` + + :rtype: :class:`~tarantool.Interval` + + :raise: :exc:`TypeError` + """ + if not isinstance(other, Interval): raise TypeError(f"unsupported operand type(s) for -: '{type(self)}' and '{type(other)}'") @@ -171,6 +285,15 @@ def __sub__(self, other): ) def __eq__(self, other): + """ + Compare equality of each field, no casts. + + :param other: Second operand. + :type other: :class:`~tarantool.Interval` + + :rtype: :obj:`bool` + """ + if not isinstance(other, Interval): return False @@ -198,6 +321,12 @@ def __repr__(self): __str__ = __repr__ def msgpack_encode(self): + """ + Encode an interval object. + + :rtype: :obj:`bytes` + """ + buf = bytes() count = 0 diff --git a/tarantool/msgpack_ext/types/timezones/__init__.py b/tarantool/msgpack_ext/types/timezones/__init__.py index b5f2faf1..c0c4ce7e 100644 --- a/tarantool/msgpack_ext/types/timezones/__init__.py +++ b/tarantool/msgpack_ext/types/timezones/__init__.py @@ -1,3 +1,7 @@ +""" +Tarantool timezones module. +""" + from tarantool.msgpack_ext.types.timezones.timezones import ( TZ_AMBIGUOUS, indexToTimezone, diff --git a/tarantool/msgpack_ext/types/timezones/gen-timezones.sh b/tarantool/msgpack_ext/types/timezones/gen-timezones.sh index 66610c4c..5de5a51a 100755 --- a/tarantool/msgpack_ext/types/timezones/gen-timezones.sh +++ b/tarantool/msgpack_ext/types/timezones/gen-timezones.sh @@ -22,7 +22,10 @@ wget -O ${SRC_FILE} \ # So we can do the same and don't worry, be happy. cat < ${DST_FILE} -# Automatically generated by gen-timezones.sh +""" +Tarantool timezone info. Automatically generated by +\`\`gen-timezones.sh\`\`. +""" TZ_UTC = 0x01 TZ_RFC = 0x02 diff --git a/tarantool/msgpack_ext/types/timezones/timezones.py b/tarantool/msgpack_ext/types/timezones/timezones.py index bbb5df5c..0e3ff770 100644 --- a/tarantool/msgpack_ext/types/timezones/timezones.py +++ b/tarantool/msgpack_ext/types/timezones/timezones.py @@ -1,4 +1,7 @@ -# Automatically generated by gen-timezones.sh +""" +Tarantool timezone info. Automatically generated by +``gen-timezones.sh``. +""" TZ_UTC = 0x01 TZ_RFC = 0x02 diff --git a/tarantool/msgpack_ext/types/timezones/validate_timezones.py b/tarantool/msgpack_ext/types/timezones/validate_timezones.py index 0626d8c3..943437f8 100644 --- a/tarantool/msgpack_ext/types/timezones/validate_timezones.py +++ b/tarantool/msgpack_ext/types/timezones/validate_timezones.py @@ -1,3 +1,8 @@ +""" +Script to validate that each Tarantool timezone is either a valid pytz +timezone or an addreviated timezone with explicit offset provided. +""" + import pytz from timezones import timezoneToIndex, timezoneAbbrevInfo diff --git a/tarantool/msgpack_ext/unpacker.py b/tarantool/msgpack_ext/unpacker.py index ff3bdcb8..bc1fb0a0 100644 --- a/tarantool/msgpack_ext/unpacker.py +++ b/tarantool/msgpack_ext/unpacker.py @@ -1,3 +1,9 @@ +""" +Tarantool `extension`_ types decoding support. + +.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +""" + import tarantool.msgpack_ext.decimal as ext_decimal import tarantool.msgpack_ext.uuid as ext_uuid import tarantool.msgpack_ext.datetime as ext_datetime @@ -11,6 +17,22 @@ } def ext_hook(code, data): + """ + :class:`msgpack.Unpacker` decoder. + + :param code: MessagePack extension type code. + :type code: :obj:`int` + + :param data: MessagePack extension type data. + :type data: :obj:`bytes` + + :return: Decoded value. + :rtype: :class:`decimal.Decimal` or :class:`uuid.UUID` or + :class:`tarantool.Datetime` or :class:`tarantool.Interval` + + :raise: :exc:`NotImplementedError` + """ + if code in decoders: return decoders[code](data) raise NotImplementedError("Unknown msgpack type: %d" % (code,)) diff --git a/tarantool/msgpack_ext/uuid.py b/tarantool/msgpack_ext/uuid.py index c489a3fc..8a1951d0 100644 --- a/tarantool/msgpack_ext/uuid.py +++ b/tarantool/msgpack_ext/uuid.py @@ -1,17 +1,47 @@ -from uuid import UUID +""" +Tarantool `uuid`_ extension type support module. + +The UUID MessagePack representation looks like this: + +.. code-block:: text -# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type -# -# The UUID MessagePack representation looks like this: -# +--------+------------+-----------------+ -# | MP_EXT | MP_UUID | UuidValue | -# | = d8 | = 2 | = 16-byte value | -# +--------+------------+-----------------+ + +--------+------------+-----------------+ + | MP_EXT | MP_UUID | UuidValue | + | = d8 | = 2 | = 16-byte value | + +--------+------------+-----------------+ + +.. _uuid: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type +""" + +from uuid import UUID EXT_ID = 2 +""" +`uuid`_ type id. +""" def encode(obj): + """ + Encode an UUID object. + + :param obj: UUID to encode. + :type obj: :obj:`uuid.UUID` + + :return: Encoded UUID. + :rtype: :obj:`bytes` + """ + return obj.bytes def decode(data): + """ + Decode an UUID object. + + :param data: UUID to decode. + :type data: :obj:`bytes` + + :return: Decoded UUID. + :rtype: :obj:`uuid.UUID` + """ + return UUID(bytes=data) diff --git a/tarantool/request.py b/tarantool/request.py index 419f7832..68d49714 100644 --- a/tarantool/request.py +++ b/tarantool/request.py @@ -1,7 +1,8 @@ # pylint: disable=C0301,W0105,W0401,W0614 -''' -Request types definitions -''' +""" +Request types definitions. For internal use only, there is no API to +send pre-build request objects. +""" import sys import msgpack @@ -56,18 +57,23 @@ from tarantool.msgpack_ext.packer import default as packer_default class Request(object): - ''' + """ Represents a single request to the server in compliance with the - Tarantool protocol. - Responsible for data encapsulation and builds binary packet - to be sent to the server. + Tarantool protocol. Responsible for data encapsulation and building + the binary packet to be sent to the server. + + This is the abstract base class. Specific request types are + implemented in the inherited classes. + """ - This is the abstract base class. Specific request types - are implemented by the inherited classes. - ''' request_type = None def __init__(self, conn): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + """ + self._bytes = None self.conn = conn self._sync = None @@ -120,6 +126,10 @@ def __init__(self, conn): self.packer = msgpack.Packer(**packer_kwargs) def _dumps(self, src): + """ + Encode MsgPack data. + """ + return self.packer.pack(src) def __bytes__(self): @@ -129,15 +139,27 @@ def __bytes__(self): @property def sync(self): - ''' - :type: int + """ + :type: :obj:`int` - Required field in the server request. Contains request header IPROTO_SYNC. - ''' + """ + return self._sync def header(self, length): + """ + Pack total (header + payload) length info together with header + itself. + + :param length: Payload length. + :type: :obj:`int` + + :return: MsgPack data with encoded total (header + payload) + length info and header. + :rtype: :obj:`bytes` + """ + self._sync = self.conn.generate_sync() header = self._dumps({IPROTO_CODE: self.request_type, IPROTO_SYNC: self._sync, @@ -147,15 +169,27 @@ def header(self, length): class RequestInsert(Request): - ''' - Represents INSERT request - ''' + """ + Represents INSERT request. + """ + request_type = REQUEST_TYPE_INSERT # pylint: disable=W0231 def __init__(self, conn, space_no, values): - ''' - ''' + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param values: Record to be inserted. + :type values: :obj:`tuple` or :obj:`list` + + :raise: :exc:`~AssertionError` + """ + super(RequestInsert, self).__init__(conn) assert isinstance(values, (tuple, list)) @@ -166,12 +200,29 @@ def __init__(self, conn, space_no, values): class RequestAuthenticate(Request): - ''' - Represents AUTHENTICATE request - ''' + """ + Represents AUTHENTICATE request. + """ + request_type = REQUEST_TYPE_AUTHENTICATE def __init__(self, conn, salt, user, password): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param salt: base64-encoded session salt. + :type salt: :obj:`str` + + :param user: User name for authentication on the Tarantool + server. + :type user: :obj:`str` + + :param password: User password for authentication on the + Tarantool server. + :type password: :obj:`str` + """ + super(RequestAuthenticate, self).__init__(conn) def sha1(values): @@ -193,6 +244,18 @@ def sha1(values): self._body = request_body def header(self, length): + """ + Pack total (header + payload) length info together with header + itself. + + :param length: Payload length. + :type: :obj:`int` + + :return: MsgPack data with encoded total (header + payload) + length info and header. + :rtype: :obj:`bytes` + """ + self._sync = self.conn.generate_sync() # Set IPROTO_SCHEMA_ID: 0 to avoid SchemaReloadException # It is ok to use 0 in auth every time. @@ -204,15 +267,27 @@ def header(self, length): class RequestReplace(Request): - ''' - Represents REPLACE request - ''' + """ + Represents REPLACE request. + """ + request_type = REQUEST_TYPE_REPLACE # pylint: disable=W0231 def __init__(self, conn, space_no, values): - ''' - ''' + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param values: Record to be replaced. + :type values: :obj:`tuple` or :obj:`list` + + :raise: :exc:`~AssertionError` + """ + super(RequestReplace, self).__init__(conn) assert isinstance(values, (tuple, list)) @@ -223,15 +298,30 @@ def __init__(self, conn, space_no, values): class RequestDelete(Request): - ''' - Represents DELETE request - ''' + """ + Represents DELETE request. + """ + request_type = REQUEST_TYPE_DELETE # pylint: disable=W0231 def __init__(self, conn, space_no, index_no, key): - ''' - ''' + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param index_no: Index id. + :type index_no: :obj:`int` + + :param key: Key of a tuple to be deleted. + :type key: :obj:`list` + + :raise: :exc:`~AssertionError` + """ + super(RequestDelete, self).__init__(conn) request_body = self._dumps({IPROTO_SPACE_ID: space_no, @@ -242,13 +332,40 @@ def __init__(self, conn, space_no, index_no, key): class RequestSelect(Request): - ''' - Represents SELECT request - ''' + """ + Represents SELECT request. + """ + request_type = REQUEST_TYPE_SELECT # pylint: disable=W0231 def __init__(self, conn, space_no, index_no, key, offset, limit, iterator): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param index_no: Index id. + :type index_no: :obj:`int` + + :param key: Key of a tuple to be selected. + :type key: :obj:`list` + + :param offset: Number of tuples to skip. + :type offset: :obj:`int` + + :param limit: Maximum number of tuples to select. + :type limit: :obj:`int` + + :param iterator: Index iterator type, see + :paramref:`~tarantool.Connection.select.params.iterator`. + :type iterator: :obj:`str` + + :raise: :exc:`~AssertionError` + """ + super(RequestSelect, self).__init__(conn) request_body = self._dumps({IPROTO_SPACE_ID: space_no, IPROTO_INDEX_ID: index_no, @@ -261,14 +378,35 @@ def __init__(self, conn, space_no, index_no, key, offset, limit, iterator): class RequestUpdate(Request): - ''' - Represents UPDATE request - ''' + """ + Represents UPDATE request. + """ request_type = REQUEST_TYPE_UPDATE # pylint: disable=W0231 def __init__(self, conn, space_no, index_no, key, op_list): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param index_no: Index id. + :type index_no: :obj:`int` + + :param key: Key of a tuple to be updated. + :type key: :obj:`list` + + :param op_list: The list of operations to update individual + fields, refer to + :paramref:`~tarantool.Connection.update.params.op_list`. + :type op_list: :obj:`tuple` or :obj:`list` + + :raise: :exc:`~AssertionError` + """ + super(RequestUpdate, self).__init__(conn) request_body = self._dumps({IPROTO_SPACE_ID: space_no, @@ -280,13 +418,31 @@ def __init__(self, conn, space_no, index_no, key, op_list): class RequestCall(Request): - ''' - Represents CALL request - ''' + """ + Represents CALL request. + """ + request_type = REQUEST_TYPE_CALL # pylint: disable=W0231 def __init__(self, conn, name, args, call_16): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param name: Stored Lua function name. + :type func_name: :obj:`str` + + :param args: Stored Lua function arguments. + :type args: :obj:`tuple` + + :param call_16: If ``True``, use compatibility mode with + Tarantool 1.6 or older. + :type call_16: :obj:`bool` + + :raise: :exc:`~AssertionError` + """ + if call_16: self.request_type = REQUEST_TYPE_CALL16 super(RequestCall, self).__init__(conn) @@ -299,13 +455,27 @@ def __init__(self, conn, name, args, call_16): class RequestEval(Request): - ''' - Represents EVAL request - ''' + """ + Represents EVAL request. + """ + request_type = REQUEST_TYPE_EVAL # pylint: disable=W0231 def __init__(self, conn, name, args): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param name: Lua expression. + :type func_name: :obj:`str` + + :param args: Lua expression arguments. + :type args: :obj:`tuple` + + :raise: :exc:`~AssertionError` + """ + super(RequestEval, self).__init__(conn) assert isinstance(args, (list, tuple)) @@ -316,25 +486,52 @@ def __init__(self, conn, name, args): class RequestPing(Request): - ''' - Ping body is empty, so body_length == 0 and there's no body - ''' + """ + Represents a ping request with the empty body. + """ + request_type = REQUEST_TYPE_PING def __init__(self, conn): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + """ + super(RequestPing, self).__init__(conn) self._body = b'' class RequestUpsert(Request): - ''' - Represents UPSERT request - ''' + """ + Represents UPSERT request. + """ request_type = REQUEST_TYPE_UPSERT # pylint: disable=W0231 def __init__(self, conn, space_no, index_no, tuple_value, op_list): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param space_no: Space id. + :type space_no: :obj:`int` + + :param index_no: Index id. + :type index_no: :obj:`int` + + :param tuple_value: Tuple to be upserted. + :type tuple_value: :obj:`tuple` or :obj:`list` + + :param op_list: The list of operations to update individual + fields, refer to + :paramref:`~tarantool.Connection.update.params.op_list`. + :type op_list: :obj:`tuple` or :obj:`list` + + :raise: :exc:`~AssertionError` + """ + super(RequestUpsert, self).__init__(conn) request_body = self._dumps({IPROTO_SPACE_ID: space_no, @@ -346,26 +543,52 @@ def __init__(self, conn, space_no, index_no, tuple_value, op_list): class RequestJoin(Request): - ''' - Represents JOIN request - ''' + """ + Represents JOIN request. + """ + request_type = REQUEST_TYPE_JOIN # pylint: disable=W0231 def __init__(self, conn, server_uuid): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param server_uuid: UUID of connector "server". + :type server_uuid: :obj:`str` + """ + super(RequestJoin, self).__init__(conn) request_body = self._dumps({IPROTO_SERVER_UUID: server_uuid}) self._body = request_body class RequestSubscribe(Request): - ''' - Represents SUBSCRIBE request - ''' + """ + Represents SUBSCRIBE request. + """ + request_type = REQUEST_TYPE_SUBSCRIBE # pylint: disable=W0231 def __init__(self, conn, cluster_uuid, server_uuid, vclock): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param server_uuid: UUID of connector "server". + :type server_uuid: :obj:`str` + + :param server_uuid: UUID of connector "server". + :type server_uuid: :obj:`str` + + :param vclock: Connector "server" vclock. + :type vclock: :obj:`dict` + + :raise: :exc:`~AssertionError` + """ + super(RequestSubscribe, self).__init__(conn) assert isinstance(vclock, dict) @@ -378,13 +601,22 @@ def __init__(self, conn, cluster_uuid, server_uuid, vclock): class RequestOK(Request): - ''' - Represents OK acknowledgement - ''' + """ + Represents OK acknowledgement. + """ + request_type = REQUEST_TYPE_OK # pylint: disable=W0231 def __init__(self, conn, sync): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param sync: Previous request sync id. + :type sync: :obj:`int` + """ + super(RequestOK, self).__init__(conn) request_body = self._dumps({IPROTO_CODE: self.request_type, IPROTO_SYNC: sync}) @@ -392,12 +624,26 @@ def __init__(self, conn, sync): class RequestExecute(Request): - ''' - Represents EXECUTE SQL request - ''' + """ + Represents EXECUTE SQL request. + """ + request_type = REQUEST_TYPE_EXECUTE def __init__(self, conn, sql, args): + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` + + :param sql: SQL query. + :type sql: :obj:`str` + + :param args: SQL query bind values. + :type args: :obj:`dict` or :obj:`list` + + :raise: :exc:`~TypeError` + """ + super(RequestExecute, self).__init__(conn) if isinstance(args, Mapping): args = [{":%s" % name: value} for name, value in args.items()] diff --git a/tarantool/response.py b/tarantool/response.py index 4566acd5..2832fba9 100644 --- a/tarantool/response.py +++ b/tarantool/response.py @@ -1,4 +1,7 @@ # pylint: disable=C0301,W0105,W0401,W0614 +""" +Request response types definitions. +""" from collections.abc import Sequence @@ -26,24 +29,24 @@ from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook class Response(Sequence): - ''' + """ Represents a single response from the server in compliance with the - Tarantool protocol. - Responsible for data encapsulation (i.e. received list of tuples) - and parsing of binary packets received from the server. - ''' + Tarantool protocol. Responsible for data encapsulation (i.e. + received list of tuples) and parsing of binary packets received from + the server. + """ def __init__(self, conn, response): - ''' - Create an instance of `Response` - using the data received from the server. + """ + :param conn: Request sender. + :type conn: :class:`~tarantool.Connection` - __init__() reads data from the socket, parses the response body, and - sets the appropriate instance attributes. + :param response: Response binary data. + :type response: :obj:`bytes` - :param body: body of the response - :type body: array of bytes - ''' + :raise: :exc:`~tarantool.error.DatabaseError`, + :exc:`~tarantool.error.SchemaReloadException` + """ # This is not necessary, because underlying list data structures are # created in the __new__(). @@ -51,7 +54,7 @@ def __init__(self, conn, response): unpacker_kwargs = dict() - # Decode msgpack arrays into Python lists by default (not tuples). + # Decode MsgPack arrays into Python lists by default (not tuples). # Can be configured in the Connection init unpacker_kwargs['use_list'] = conn.use_list @@ -147,115 +150,120 @@ def __reversed__(self): return reversed(self._data) def index(self, *args): + """ + Refer to :class:`collections.abc.Sequence`. + + :raises: :exc:`~tarantool.error.InterfaceError.` + """ + if self._data is None: raise InterfaceError("Trying to access data when there's no data") return self._data.index(*args) def count(self, item): + """ + Refer to :class:`collections.abc.Sequence`. + + :raises: :exc:`~tarantool.error.InterfaceError` + """ + if self._data is None: raise InterfaceError("Trying to access data when there's no data") return self._data.count(item) @property def rowcount(self): - ''' - :type: int + """ + :type: :obj:`int` Number of rows affected or returned by a query. - ''' + """ + return len(self) @property def body(self): - ''' - :type: dict + """ + :type: :obj:`dict` + + Raw response body. + """ - Required field in the server response. - Contains the raw response body. - ''' return self._body @property def code(self): - ''' - :type: int + """ + :type: :obj:`int` + + Response type id. + """ - Required field in the server response. - Contains the response type id. - ''' return self._code @property def sync(self): - ''' - :type: int + """ + :type: :obj:`int` + + Response header IPROTO_SYNC. + """ - Required field in the server response. - Contains the response header IPROTO_SYNC. - ''' return self._sync @property def return_code(self): - ''' - :type: int - - Required field in the server response. - If the request was successful, - the value of :attr:`return_code` is ``0``. - Otherwise, :attr:`return_code` contains an error code. - If :attr:`return_code` is non-zero, :attr:`return_message` - contains an error message. - ''' + """ + :type: :obj:`int` + + If the request was successful, the value of is ``0``. + Otherwise, it contains an error code. If the value is non-zero, + :attr:`return_message` contains an error message. + """ + return self._return_code @property def data(self): - ''' - :type: object + """ + :type: :obj:`object` + + Contains the list of tuples for SELECT, REPLACE and DELETE + requests and arbitrary data for CALL. + """ - Required field in the server response. - Contains the list of tuples for SELECT, REPLACE and DELETE requests - and arbitrary data for CALL. - ''' return self._data @property def strerror(self): - ''' - :type: str + """ + Refer to :func:`~tarantool.error.tnt_strerror`. + """ - Contains ER_OK if the request was successful, - or contains an error code string. - ''' return tnt_strerror(self._return_code) @property def return_message(self): - ''' - :type: str + """ + :type: :obj:`str` + + The error message returned by the server in case of non-zero + :attr:`return_code` (empty string otherwise). + """ - The error message returned by the server in case - :attr:`return_code` is non-zero. - ''' return self._return_message @property def schema_version(self): - ''' - :type: int + """ + :type: :obj:`int` + + Request current schema version. + """ - Current schema version of request. - ''' return self._schema_version def __str__(self): - ''' - Return a user-friendy string representation of the object. - Useful for interactive sessions and debuging. - - :rtype: str or None - ''' if self.return_code: return json.dumps({ 'error': { @@ -274,16 +282,20 @@ def __str__(self): class ResponseExecute(Response): + """ + Represents an SQL EXECUTE request response. + """ + @property def autoincrement_ids(self): """ - Returns a list with the new primary-key value - (or values) for an INSERT in a table defined with - PRIMARY KEY AUTOINCREMENT - (NOT result set size). + A list with the new primary-key value (or values) for an + INSERT in a table defined with PRIMARY KEY AUTOINCREMENT (NOT + result set size). - :rtype: list or None + :rtype: :obj:`list` or :obj:`None` """ + if self._return_code != 0: return None info = self._body.get(IPROTO_SQL_INFO) @@ -298,11 +310,12 @@ def autoincrement_ids(self): @property def affected_row_count(self): """ - Returns the number of changed rows for responses - to DML requests and None for DQL requests. + The number of changed rows for responses to DML requests and + ``None`` for DQL requests. - :rtype: int + :rtype: :obj:`int` or :obj:`None` """ + if self._return_code != 0: return None info = self._body.get(IPROTO_SQL_INFO) diff --git a/tarantool/schema.py b/tarantool/schema.py index 3c06963d..d4f13f6b 100644 --- a/tarantool/schema.py +++ b/tarantool/schema.py @@ -1,8 +1,8 @@ # pylint: disable=R0903 -''' -This module provides the :class:`~tarantool.schema.Schema` class. -It is a Tarantool schema description. -''' +""" +Schema types definitions. For internal use only, there is no API to use +pre-build schema objects. +""" from tarantool.error import ( Error, @@ -13,28 +13,50 @@ class RecursionError(Error): - """Report the situation when max recursion depth is reached. + """ + Report the situation when max recursion depth is reached. - This is an internal error of caller, - and it should be re-raised properly be the caller. + This is an internal error of + :func:`~tarantool.schema.to_unicode_recursive` caller and it should + be re-raised properly by the caller. """ def to_unicode(s): + """ + Decode :obj:`bytes` to unicode :obj:`str`. + + :param s: Value to convert. + + :return: Decoded unicode :obj:`str`, if value is :obj:`bytes`. + Otherwise, it returns the original value. + + :meta private: + """ + if isinstance(s, bytes): return s.decode(encoding='utf-8') return s def to_unicode_recursive(x, max_depth): - """Same as to_unicode(), but traverses recursively over dictionaries, - lists and tuples. + """ + Recursively decode :obj:`bytes` to unicode :obj:`str` over + :obj:`dict`, :obj:`list` and :obj:`tuple`. - x: value to convert + :param x: Value to convert. - max_depth: 1 accepts a scalar, 2 accepts a list of scalars, - etc. + :param max_depth: Maximum depth recursion. + :type max_depth: :obj:`int` + + :return: The same structure where all :obj:`bytes` are replaced + with unicode :obj:`str`. + + :raise: :exc:`~tarantool.schema.RecursionError` + + :meta private: """ + if max_depth <= 0: raise RecursionError('Max recursion depth is reached') @@ -59,7 +81,21 @@ def to_unicode_recursive(x, max_depth): class SchemaIndex(object): + """ + Contains schema for a space index. + """ + def __init__(self, index_row, space): + """ + :param index_row: Index format data received from Tarantool. + :type index_row: :obj:`list` or :obj:`tuple` + + :param space: Related space schema. + :type space: :class:`~tarantool.schema.SchemaSpace` + + :raise: :exc:`~tarantool.error.SchemaError` + """ + self.iid = index_row[1] self.name = index_row[2] self.name = to_unicode(index_row[2]) @@ -89,13 +125,31 @@ def __init__(self, index_row, space): self.space.indexes[self.name] = self def flush(self): + """ + Clean existing index data. + """ + del self.space.indexes[self.iid] if self.name: del self.space.indexes[self.name] class SchemaSpace(object): + """ + Contains schema for a space. + """ + def __init__(self, space_row, schema): + """ + :param space_row: Space format data received from Tarantool. + :type space_row: :obj:`list` or :obj:`tuple` + + :param schema: Related server schema. + :type schema: :class:`~tarantool.schema.Schema` + + :raise: :exc:`~tarantool.error.SchemaError` + """ + self.sid = space_row[0] self.arity = space_row[1] self.name = to_unicode(space_row[2]) @@ -116,17 +170,42 @@ def __init__(self, space_row, schema): self.format[part_id ] = part def flush(self): + """ + Clean existing space data. + """ + del self.schema[self.sid] if self.name: del self.schema[self.name] class Schema(object): + """ + Contains Tarantool server spaces schema. + """ + def __init__(self, con): + """ + :param con: Related Tarantool server connection. + :type con: :class:`~tarantool.Connection` + """ + self.schema = {} self.con = con def get_space(self, space): + """ + Get space schema. If it exists in the local schema, return local + data, otherwise fetch data from the Tarantool server. + + :param space: Space name or space id. + :type space: :obj:`str` or :obj:`int` + + :rtype: :class:`~tarantool.schema.SchemaSpace` + + :raises: :meth:`~tarantool.schema.Schema.fetch_space` exceptions + """ + space = to_unicode(space) try: @@ -137,6 +216,19 @@ def get_space(self, space): return self.fetch_space(space) def fetch_space(self, space): + """ + Fetch a single space schema from the Tarantool server and build + a schema object. + + :param space: Space name or space id to fetch. + :type space: :obj:`str` or :obj:`int` + + :rtype: :class:`~tarantool.schema.SchemaSpace` + + :raises: :exc:`~tarantool.error.SchemaError`, + :meth:`~tarantool.schema.Schema.fetch_space_from` exceptions + """ + space_row = self.fetch_space_from(space) if len(space_row) > 1: @@ -155,6 +247,19 @@ def fetch_space(self, space): return SchemaSpace(space_row, self.schema) def fetch_space_from(self, space): + """ + Fetch space schema from the Tarantool server. + + :param space: Space name or space id to fetch. If ``None``, + fetch all spaces. + :type space: :obj:`str` or :obj:`int` or :obj:`None` + + :return: Space format data received from Tarantool. + :rtype: :obj:`list` or :obj:`tuple` + + :raises: :meth:`~tarantool.Connection.select` exceptions + """ + _index = None if isinstance(space, str): _index = const.INDEX_SPACE_NAME @@ -181,11 +286,33 @@ def fetch_space_from(self, space): return space_row def fetch_space_all(self): + """ + Fetch all spaces schema from the Tarantool server and build + corresponding schema objects. + + :raises: :meth:`~tarantool.schema.Schema.fetch_space_from` + exceptions + """ + space_rows = self.fetch_space_from(None) for row in space_rows: SchemaSpace(row, self.schema) def get_index(self, space, index): + """ + Get space index schema. If it exists in the local schema, return + local data, otherwise fetch data from the Tarantool server. + + :param space: Space id or space name. + :type space: :obj:`str` or :obj:`int` + + :param index: Index id or index name. + :type index: :obj:`str` or :obj:`int` + + :rtype: :class:`~tarantool.schema.SchemaIndex` + + :raises: :meth:`~tarantool.schema.Schema.fetch_index` exceptions + """ space = to_unicode(space) index = to_unicode(index) @@ -198,6 +325,22 @@ def get_index(self, space, index): return self.fetch_index(_space, index) def fetch_index(self, space_object, index): + """ + Fetch a single index space schema from the Tarantool server and + build a schema object. + + :param space: Space schema. + :type space: :class:`~tarantool.schema.SchemaSpace` + + :param index: Index name or id. + :type index: :obj:`str` or :obj:`int` + + :rtype: :class:`~tarantool.schema.SchemaIndex` + + :raises: :exc:`~tarantool.error.SchemaError`, + :meth:`~tarantool.schema.Schema.fetch_index_from` exceptions + """ + index_row = self.fetch_index_from(space_object.sid, index) if len(index_row) > 1: @@ -218,11 +361,35 @@ def fetch_index(self, space_object, index): return SchemaIndex(index_row, space_object) def fetch_index_all(self): + """ + Fetch all spaces indexes schema from the Tarantool server and + build corresponding schema objects. + + :raises: :meth:`~tarantool.schema.Schema.fetch_index_from` + exceptions + """ index_rows = self.fetch_index_from(None, None) for row in index_rows: SchemaIndex(row, self.schema[row[0]]) def fetch_index_from(self, space, index): + """ + Fetch space index schema from the Tarantool server. + + :param space: Space id. If ``None``, fetch all spaces + index schema. + :type space: :obj:`int` or :obj:`None` + + :param index: Index name or id. If ``None``, fetch all space + indexes schema. + :type index: :obj:`str` or :obj:`int` or :obj:`None` + + :return: Space index format data received from Tarantool. + :rtype: :obj:`list` or :obj:`tuple` + + :raises: :meth:`~tarantool.Connection.select` exceptions + """ + _index = None if isinstance(index, str): _index = const.INDEX_INDEX_NAME @@ -257,6 +424,21 @@ def fetch_index_from(self, space, index): return index_row def get_field(self, space, field): + """ + Get space field format info. + + :param space: Space name or space id. + :type space: :obj:`str` or :obj:`int` + + :param field: Field name or field id. + :type field: :obj:`str` or :obj:`int` + + :return: Field format info. + :rtype: :obj:`dict` + + :raises: :exc:`~tarantool.error.SchemaError`, + :meth:`~tarantool.schema.Schema.fetch_space` exceptions + """ space = to_unicode(space) field = to_unicode(field) @@ -273,4 +455,8 @@ def get_field(self, space, field): return field def flush(self): + """ + Clean existing schema data. + """ + self.schema.clear() diff --git a/tarantool/space.py b/tarantool/space.py index 5ebe297c..0fa61198 100644 --- a/tarantool/space.py +++ b/tarantool/space.py @@ -1,82 +1,76 @@ # pylint: disable=C0301,W0105,W0401,W0614 -''' -This module provides the :class:`~tarantool.space.Space` class. -It is an object-oriented wrapper for requests to a Tarantool space. -''' +""" +Space type definition. It is an object-oriented wrapper for requests to +a Tarantool server space. +""" class Space(object): - ''' + """ Object-oriented wrapper for accessing a particular space. - Encapsulates the identifier of the space and provides a more convenient - syntax for database operations. - ''' + Encapsulates the identifier of the space and provides a more + convenient syntax for database operations. + """ def __init__(self, connection, space_name): - ''' - Create Space instance. + """ + :param connection: Connection to the server. + :type connection: :class:`~tarantool.Connection` - :param connection: object representing connection to the server - :type connection: :class:`~tarantool.connection.Connection` instance - :param int space_name: space no or name to insert a record - :type space_name: int or str - ''' + :param space_name: Space name or space id to bind. + :type space_name: :obj:`str` or :obj:`int` + + :raises: :meth:`~tarantool.schema.Schema.get_space` exceptions + """ self.connection = connection self.space_no = self.connection.schema.get_space(space_name).sid def insert(self, *args, **kwargs): - ''' - Execute an INSERT request. + """ + Refer to :meth:`~tarantool.Connection.insert`. + """ - See `~tarantool.connection.insert` for more information. - ''' return self.connection.insert(self.space_no, *args, **kwargs) def replace(self, *args, **kwargs): - ''' - Execute a REPLACE request. + """ + Refer to :meth:`~tarantool.Connection.replace`. + """ - See `~tarantool.connection.replace` for more information. - ''' return self.connection.replace(self.space_no, *args, **kwargs) def delete(self, *args, **kwargs): - ''' - Execute a DELETE request. + """ + Refer to :meth:`~tarantool.Connection.delete`. + """ - See `~tarantool.connection.delete` for more information. - ''' return self.connection.delete(self.space_no, *args, **kwargs) def update(self, *args, **kwargs): - ''' - Execute an UPDATE request. + """ + Refer to :meth:`~tarantool.Connection.update`. + """ - See `~tarantool.connection.update` for more information. - ''' return self.connection.update(self.space_no, *args, **kwargs) def upsert(self, *args, **kwargs): - ''' - Execute an UPDATE request. + """ + Refer to :meth:`~tarantool.Connection.upsert`. + """ - See `~tarantool.connection.upsert` for more information. - ''' return self.connection.upsert(self.space_no, *args, **kwargs) def select(self, *args, **kwargs): - ''' - Execute a SELECT request. + """ + Refer to :meth:`~tarantool.Connection.select`. + """ - See `~tarantool.connection.select` for more information. - ''' return self.connection.select(self.space_no, *args, **kwargs) def call(self, func_name, *args, **kwargs): - ''' - Execute a CALL request. Call a stored Lua function. + """ + **Deprecated**, use :meth:`~tarantool.Connection.call` instead. + """ - Deprecated, use `~tarantool.connection.call` instead. - ''' return self.connection.call(func_name, *args, **kwargs) diff --git a/tarantool/utils.py b/tarantool/utils.py index 1427d9d0..3f8275ba 100644 --- a/tarantool/utils.py +++ b/tarantool/utils.py @@ -8,9 +8,33 @@ from base64 import decodebytes as base64_decode def strxor(rhs, lhs): + """ + XOR two strings. + + :param rhs: String to XOR. + :type rhs: :obj:`str` or :obj:`bytes` + + :param lhs: Another string to XOR. + :type lhs: :obj:`str` or :obj:`bytes` + + :rtype: :obj:`bytes` + """ + return bytes([x ^ y for x, y in zip(rhs, lhs)]) def check_key(*args, **kwargs): + """ + Validate request key types and map. + + :param args: Method args. + :type args: :obj:`tuple` + + :param kwargs: Method kwargs. + :type kwargs: :obj:`dict` + + :rtype: :obj:`list` + """ + if 'first' not in kwargs: kwargs['first'] = True if 'select' not in kwargs: @@ -29,9 +53,36 @@ def check_key(*args, **kwargs): def version_id(major, minor, patch): + """ + :param major: Version major number. + :type major: :obj:`int` + + :param minor: Version minor number. + :type minor: :obj:`int` + + :param patch: Version patch number. + :type patch: :obj:`int` + + :return: Unique version identificator for 8-bytes major, minor, + patch numbers. + :rtype: :obj:`int` + """ + return (((major << 8) | minor) << 8) | patch def greeting_decode(greeting_buf): + """ + Decode Tarantool server greeting. + + :param greeting_buf: Binary greetings data. + :type greeting_buf: :obj:`bytes` + + :rtype: ``Greeting`` dataclass with ``version_id``, ``protocol``, + ``uuid``, ``salt`` fields + + :raise: :exc:`~Exception` + """ + class Greeting: version_id = 0 protocol = None