├── asynctnt ├── iproto │ ├── __init__.py │ ├── requests │ │ ├── id.pxd │ │ ├── ping.pxd │ │ ├── insert.pxd │ │ ├── call.pxd │ │ ├── ping.pyx │ │ ├── eval.pxd │ │ ├── upsert.pxd │ │ ├── delete.pxd │ │ ├── prepare.pxd │ │ ├── auth.pxd │ │ ├── execute.pxd │ │ ├── select.pxd │ │ ├── streams.pxd │ │ ├── upsert.pyx │ │ ├── update.pxd │ │ ├── base.pxd │ │ ├── insert.pyx │ │ ├── streams.pyx │ │ ├── id.pyx │ │ ├── delete.pyx │ │ ├── eval.pyx │ │ ├── call.pyx │ │ ├── base.pyx │ │ ├── prepare.pyx │ │ ├── execute.pyx │ │ ├── select.pyx │ │ ├── auth.pyx │ │ └── update.pyx │ ├── ttuple.pyx │ ├── xd.pxd │ ├── ext │ │ ├── uuid.pxd │ │ ├── uuid.pyx │ │ ├── error.pxd │ │ ├── decimal.pxd │ │ ├── datetime.pxd │ │ ├── interval.pxd │ │ ├── datetime.pyx │ │ ├── error.pyx │ │ ├── decimal.pyx │ │ └── interval.pyx │ ├── unicodeutil.pxd │ ├── push.pxd │ ├── tupleobj │ │ ├── __init__.pxd │ │ └── tupleobj.h │ ├── unicodeutil.pyx │ ├── const.pxi │ ├── bit.pxd │ ├── coreproto.pxd │ ├── rbuffer.pxd │ ├── schema.pxd │ ├── response.pxd │ ├── buffer.pxd │ ├── protocol.pxd │ ├── db.pxd │ ├── tarantool.pxd │ ├── cmsgpuck.pxd │ ├── python.pxd │ ├── push.pyx │ ├── rbuffer.pyx │ ├── coreproto.pyx │ └── protocol.pyi ├── log.py ├── types.py ├── __init__.py ├── stream.py └── prepared.py ├── docs ├── overview.md ├── CHANGELOG.md ├── ttuple.md ├── index.md ├── installation.md ├── metadata.md ├── Makefile ├── make.bat ├── examples.md ├── pushes.md ├── streams.md ├── mpext.md ├── sql.md └── conf.py ├── scripts ├── run_until_error.sh └── run_until_success.sh ├── .gitmodules ├── MANIFEST.in ├── .editorconfig ├── tests ├── util │ └── __init__.py ├── __init__.py ├── test_op_ping.py ├── test_op_upsert.py ├── test_op_eval.py ├── test_op_sql_prepared.py ├── test_op_delete.py ├── test_op_insert.py ├── test_stream.py ├── files │ └── app.lua └── test_op_call.py ├── Makefile ├── .gitignore ├── bench ├── init.lua └── benchmark.py ├── pyproject.toml ├── third_party └── xd.h ├── setup.py ├── .github └── workflows │ └── actions.yaml └── README.md /asynctnt/iproto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/ttuple.md: -------------------------------------------------------------------------------- 1 | # Tarantool Tuple 2 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/id.pxd: -------------------------------------------------------------------------------- 1 | cdef class IDRequest(BaseRequest): 2 | pass 3 | -------------------------------------------------------------------------------- /asynctnt/iproto/ttuple.pyx: -------------------------------------------------------------------------------- 1 | TarantoolTuple = tupleobj.AtntTuple_InitTypes() 2 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/ping.pxd: -------------------------------------------------------------------------------- 1 | cdef class PingRequest(BaseRequest): 2 | pass 3 | 4 | -------------------------------------------------------------------------------- /scripts/run_until_error.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $@; while [ $? -eq 0 ]; do $@; done 4 | -------------------------------------------------------------------------------- /scripts/run_until_success.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $@; while [ $? -ne 0 ]; do $@; done 4 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/insert.pxd: -------------------------------------------------------------------------------- 1 | cdef class InsertRequest(BaseRequest): 2 | cdef: 3 | object t 4 | -------------------------------------------------------------------------------- /asynctnt/iproto/xd.pxd: -------------------------------------------------------------------------------- 1 | cdef extern from "../../third_party/xd.h": 2 | char * xd(char *data, size_t size) 3 | -------------------------------------------------------------------------------- /asynctnt/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | __all__ = ("logger",) 4 | 5 | logger = logging.getLogger("asynctnt") 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/msgpuck"] 2 | path = third_party/msgpuck 3 | url = https://github.com/tarantool/msgpuck.git 4 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/uuid.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uint32_t 2 | 3 | 4 | cdef object uuid_decode(const char ** p, uint32_t length) 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/call.pxd: -------------------------------------------------------------------------------- 1 | cdef class CallRequest(BaseRequest): 2 | cdef: 3 | str func_name 4 | object args 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/ping.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class PingRequest(BaseRequest): 6 | pass 7 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/eval.pxd: -------------------------------------------------------------------------------- 1 | cdef class EvalRequest(BaseRequest): 2 | cdef: 3 | str expression 4 | object args 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/upsert.pxd: -------------------------------------------------------------------------------- 1 | cdef class UpsertRequest(BaseRequest): 2 | cdef: 3 | object t 4 | list operations 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/delete.pxd: -------------------------------------------------------------------------------- 1 | cdef class DeleteRequest(BaseRequest): 2 | cdef: 3 | SchemaIndex index 4 | object key 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/unicodeutil.pxd: -------------------------------------------------------------------------------- 1 | cdef bytes encode_unicode_string(object s, bytes encoding) 2 | cdef str decode_string(bytes b, bytes encoding) 3 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/prepare.pxd: -------------------------------------------------------------------------------- 1 | cdef class PrepareRequest(BaseRequest): 2 | cdef: 3 | str query 4 | uint64_t statement_id 5 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/auth.pxd: -------------------------------------------------------------------------------- 1 | cdef class AuthRequest(BaseRequest): 2 | cdef: 3 | bytes salt 4 | str username 5 | str password 6 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/execute.pxd: -------------------------------------------------------------------------------- 1 | cdef class ExecuteRequest(BaseRequest): 2 | cdef: 3 | str query 4 | uint64_t statement_id 5 | object args 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tests *.py 2 | recursive-include asynctnt *.pyx *.pxd *.pxi *.py *.c *.h 3 | recursive-include third_party * 4 | include LICENSE.txt README.md Makefile 5 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # asynctnt 2 | 3 | ```{toctree} 4 | :maxdepth: 2 5 | 6 | overview 7 | installation 8 | examples 9 | sql 10 | metadata 11 | pushes 12 | streams 13 | mpext 14 | CHANGELOG 15 | ``` 16 | -------------------------------------------------------------------------------- /asynctnt/iproto/push.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class PushIterator: 6 | cdef: 7 | object _fut 8 | BaseRequest _request 9 | Response _response 10 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/select.pxd: -------------------------------------------------------------------------------- 1 | cdef class SelectRequest(BaseRequest): 2 | cdef: 3 | SchemaIndex index 4 | object key 5 | uint64_t offset 6 | uint64_t limit 7 | uint32_t iterator 8 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To install asynctnt through **pip** run: 4 | 5 | ```bash 6 | $ pip install asynctnt 7 | ``` 8 | 9 | 10 | # Running tests 11 | 12 | To execute the testsuite simply run: 13 | 14 | ```bash 15 | $ make test 16 | ``` 17 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/streams.pxd: -------------------------------------------------------------------------------- 1 | cdef class BeginRequest(BaseRequest): 2 | cdef: 3 | uint32_t isolation 4 | double tx_timeout 5 | 6 | cdef class CommitRequest(BaseRequest): 7 | pass 8 | 9 | cdef class RollbackRequest(BaseRequest): 10 | pass 11 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/uuid.pyx: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uint32_t 2 | 3 | from uuid import UUID 4 | 5 | 6 | cdef object uuid_decode(const char ** p, uint32_t length): 7 | data = cpython.bytes.PyBytes_FromStringAndSize(p[0], length) 8 | p[0] += length 9 | return UUID(bytes=data) 10 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/upsert.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class UpsertRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | return encode_request_update(buffer, self.space, self.space.get_index(0), 8 | self.t, self.operations, True) 9 | -------------------------------------------------------------------------------- /asynctnt/iproto/tupleobj/__init__.pxd: -------------------------------------------------------------------------------- 1 | cimport cpython 2 | 3 | 4 | cdef extern from "tupleobj/tupleobj.h": 5 | 6 | cpython.PyTypeObject *AtntTuple_InitTypes() except NULL 7 | 8 | int AtntTuple_CheckExact(object) 9 | object AtntTuple_New(object, int) 10 | void AtntTuple_SET_ITEM(object, int, object) 11 | 12 | object AtntTupleDesc_New(object, object) 13 | -------------------------------------------------------------------------------- /asynctnt/types.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Awaitable, Dict, List, Tuple, Union 3 | 4 | from asynctnt.iproto import protocol 5 | 6 | MethodRet = Union[Awaitable[protocol.Response], asyncio.Future] 7 | SpaceType = Union[str, int] 8 | IndexType = Union[str, int] 9 | KeyType = Union[List[Any], Tuple] 10 | TupleType = Union[List[Any], Tuple, Dict[str, Any]] 11 | -------------------------------------------------------------------------------- /asynctnt/__init__.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: F401 2 | 3 | from .connection import Connection, connect 4 | from .iproto.protocol import ( 5 | Adjust, 6 | Db, 7 | Field, 8 | IProtoError, 9 | IProtoErrorStackFrame, 10 | Iterator, 11 | Metadata, 12 | MPInterval, 13 | PushIterator, 14 | Response, 15 | Schema, 16 | SchemaIndex, 17 | SchemaSpace, 18 | TarantoolTuple, 19 | ) 20 | 21 | __version__ = "2.4.0" 22 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/error.pxd: -------------------------------------------------------------------------------- 1 | cdef class IProtoErrorStackFrame: 2 | cdef: 3 | readonly str error_type 4 | readonly str file 5 | readonly int line 6 | readonly str message 7 | readonly int err_no 8 | readonly int code 9 | readonly dict fields 10 | 11 | cdef class IProtoError: 12 | cdef: 13 | readonly list trace 14 | 15 | cdef IProtoError iproto_error_decode(const char ** b, bytes encoding) 16 | -------------------------------------------------------------------------------- /asynctnt/iproto/unicodeutil.pyx: -------------------------------------------------------------------------------- 1 | cimport cpython.unicode 2 | from cpython.ref cimport PyObject 3 | 4 | 5 | cdef bytes encode_unicode_string(object s, bytes encoding): 6 | cdef: 7 | bytes b 8 | PyObject *p 9 | 10 | b = cpython.unicode.PyUnicode_AsEncodedString( 11 | s, encoding, b'strict' 12 | ) 13 | return b 14 | 15 | 16 | cdef str decode_string(bytes b, bytes encoding): 17 | return cpython.unicode.PyUnicode_FromEncodedObject( 18 | b, encoding, b'strict' 19 | ) 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.{py, lua}] 9 | charset = utf-8 10 | 11 | [*.{py, lua, sh}] 12 | indent_style = space 13 | indent_size = 4 14 | trim_trailing_whitespace = true 15 | 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [{package.json,.travis.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [{env*}] 24 | indent_style = none 25 | indent_size = none 26 | end_of_line = none 27 | trim_trailing_whitespace = none 28 | charset = none 29 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/decimal.pxd: -------------------------------------------------------------------------------- 1 | from libc cimport math 2 | from libc.stdint cimport uint8_t, uint32_t 3 | 4 | 5 | cdef inline uint32_t bcd_len(uint32_t digits_len): 6 | return math.floor(digits_len / 2) + 1 7 | 8 | cdef uint32_t decimal_len(int exponent, uint32_t digits_count) 9 | cdef char *decimal_encode(char *p, 10 | uint32_t digits_count, 11 | uint8_t sign, 12 | tuple digits, 13 | int exponent) except NULL 14 | cdef object decimal_decode(const char ** p, uint32_t length) 15 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/update.pxd: -------------------------------------------------------------------------------- 1 | cdef class UpdateRequest(BaseRequest): 2 | cdef: 3 | SchemaIndex index 4 | object key 5 | list operations 6 | 7 | 8 | cdef char *encode_update_ops(WriteBuffer buffer, 9 | char *p, list operations, 10 | SchemaSpace space) except NULL 11 | cdef int encode_request_update(WriteBuffer buffer, 12 | SchemaSpace space, SchemaIndex index, 13 | key_tuple, list operations, 14 | bint is_upsert) except -1 15 | -------------------------------------------------------------------------------- /docs/metadata.md: -------------------------------------------------------------------------------- 1 | # Metadata 2 | 3 | `asynctnt` fetches schema from Tarantool server in order to provide ability to use 4 | space and index names in the CRUD requests, as well as field names of the tuples. 5 | You can access this metadata in the `Connection` object directly. This schema may be 6 | refreshed if schema is changed in Tarantool. 7 | 8 | ```python 9 | import asynctnt 10 | 11 | conn = await asynctnt.connect() 12 | 13 | print('space id', conn.schema.spaces['_space'].sid) 14 | print('space engine', conn.schema.spaces['_space'].engine) 15 | print('space format fields', conn.schema.spaces['_space'].metadata.fields) 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = asynctnt 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /asynctnt/iproto/ext/datetime.pxd: -------------------------------------------------------------------------------- 1 | from cpython.datetime cimport datetime 2 | from libc.stdint cimport int16_t, int32_t, int64_t, uint32_t 3 | 4 | 5 | cdef struct IProtoDateTime: 6 | int64_t seconds 7 | int32_t nsec 8 | int16_t tzoffset 9 | int16_t tzindex 10 | 11 | cdef void datetime_zero(IProtoDateTime *dt) 12 | cdef uint32_t datetime_len(IProtoDateTime *dt) 13 | cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL 14 | cdef int datetime_decode(const char ** p, 15 | uint32_t length, 16 | IProtoDateTime *dt) except -1 17 | cdef void datetime_from_py(datetime ob, IProtoDateTime *dt) 18 | cdef object datetime_to_py(IProtoDateTime *dt) 19 | -------------------------------------------------------------------------------- /asynctnt/iproto/const.pxi: -------------------------------------------------------------------------------- 1 | DEF _BUFFER_FREELIST_SIZE = 256 2 | DEF _BUFFER_INITIAL_SIZE = 1024 3 | DEF _BUFFER_MAX_GROW = 65536 4 | 5 | DEF _DEALLOCATE_RATIO = 4 6 | 7 | DEF METADATA_FREELIST_SIZE = 128 8 | DEF REQUEST_FREELIST = 256 9 | 10 | # Header length description: 11 | # pkt_len + 12 | # mp_sizeof_map(2) + 13 | # mp_sizeof_uint(TP_CODE) + 14 | # mp_sizeof_uint(TP COMMAND) + 15 | # mp_sizeof_uint(TP_SYNC) + 16 | # sync len + 17 | # mp_sizeof_uint(TP_SCHEMA_ID) + 18 | # mp_sizeof_uint(STREAM_ID) + 19 | # schema_id len 20 | DEF HEADER_CONST_LEN = 5 + 1 + 1 + 1 + 1 + 5 + 1 + 9 + 9 21 | 22 | DEF IPROTO_GREETING_SIZE = 128 23 | DEF TARANTOOL_VERSION_LENGTH = 64 24 | DEF SALT_LENGTH = 44 25 | DEF SCRAMBLE_SIZE = 20 26 | 27 | 28 | DEF SPACE_VSPACE = 281 29 | DEF SPACE_VINDEX = 289 30 | 31 | DEF DATETIME_TAIL_SZ = 4 + 2 + 2 32 | -------------------------------------------------------------------------------- /asynctnt/iproto/bit.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uint16_t, uint32_t, uint64_t 2 | from libc.string cimport memcpy 3 | 4 | 5 | cdef inline uint64_t load_u64(const void * p): 6 | cdef: 7 | uint64_t res 8 | 9 | res = 0 10 | memcpy(&res, p, sizeof(res)) 11 | return res 12 | 13 | cdef inline uint64_t load_u32(const void * p): 14 | cdef: 15 | uint32_t res 16 | 17 | res = 0 18 | memcpy(&res, p, sizeof(res)) 19 | return res 20 | 21 | cdef inline uint64_t load_u16(const void * p): 22 | cdef: 23 | uint16_t res 24 | 25 | res = 0 26 | memcpy(&res, p, sizeof(res)) 27 | return res 28 | 29 | cdef inline void store_u64(void * p, uint64_t v): 30 | memcpy(p, &v, sizeof(v)) 31 | 32 | cdef inline void store_u32(void * p, uint32_t v): 33 | memcpy(p, &v, sizeof(v)) 34 | 35 | cdef inline void store_u16(void * p, uint16_t v): 36 | memcpy(p, &v, sizeof(v)) 37 | -------------------------------------------------------------------------------- /asynctnt/stream.py: -------------------------------------------------------------------------------- 1 | from .api import Api 2 | 3 | 4 | class Stream(Api): 5 | def __init__(self): 6 | super().__init__() 7 | 8 | @property 9 | def stream_id(self) -> int: 10 | """ 11 | Current stream is 12 | """ 13 | return self._db.stream_id 14 | 15 | async def __aenter__(self): 16 | """ 17 | If used as Context Manager `begin()` and `commit()`/`rollback()` 18 | are called automatically 19 | :return: 20 | """ 21 | await self.begin() 22 | return self 23 | 24 | async def __aexit__(self, exc_type, exc_val, exc_tb): 25 | """ 26 | Normally `commit()` is called on context manager exit, but 27 | in the case of exception `rollback()` is called 28 | """ 29 | if exc_type and exc_val: 30 | await self.rollback() 31 | else: 32 | await self.commit() 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=asynctnt 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/interval.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uint32_t 2 | 3 | 4 | cdef class MPInterval: 5 | cdef: 6 | public int year 7 | public int month 8 | public int week 9 | public int day 10 | public int hour 11 | public int min 12 | public int sec 13 | public int nsec 14 | public object adjust 15 | 16 | cdef enum mp_interval_fields: 17 | MP_INTERVAL_FIELD_YEAR = 0 18 | MP_INTERVAL_FIELD_MONTH = 1 19 | MP_INTERVAL_FIELD_WEEK = 2 20 | MP_INTERVAL_FIELD_DAY = 3 21 | MP_INTERVAL_FIELD_HOUR = 4 22 | MP_INTERVAL_FIELD_MINUTE = 5 23 | MP_INTERVAL_FIELD_SECOND = 6 24 | MP_INTERVAL_FIELD_NANOSECOND = 7 25 | MP_INTERVAL_FIELD_ADJUST = 8 26 | 27 | cdef uint32_t interval_len(MPInterval interval) 28 | cdef char *interval_encode(char *p, MPInterval interval) except NULL 29 | cdef MPInterval interval_decode(const char ** p, uint32_t length) except * 30 | -------------------------------------------------------------------------------- /tests/util/__init__.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | 4 | def get_complex_param(replace_bin=True, encoding="utf-8"): 5 | p = { 6 | "a": 1, 7 | "b": 2.5, 8 | "c": [1, 2, [4, 5], {"3": 17}], 9 | "d": {"k": 1, "l": (1, 2)}, 10 | "e": b"1234567890", 11 | "f": [], 12 | "g": {}, 13 | "h": "some long text", 14 | "i": "русский текст", 15 | "j": False, 16 | "k": True, 17 | "l": None, 18 | } 19 | p_copy = copy.deepcopy(p) 20 | # tuples return as lists 21 | p_copy["d"]["l"] = list(p_copy["d"]["l"]) 22 | 23 | if replace_bin: 24 | # For some reason Tarantool in call returns MP_STR instead of MP_BIN 25 | p_copy["e"] = p_copy["e"].decode(encoding) 26 | 27 | return p, p_copy 28 | 29 | 30 | def get_big_param(size=1024, parts=3): 31 | assert parts > 0 32 | d = {} 33 | for p in range(parts): 34 | d["a" + str(p)] = "x" * (size // parts) 35 | return d 36 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/base.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport int64_t, uint64_t 2 | 3 | 4 | cdef class BaseRequest: 5 | cdef: 6 | tarantool.iproto_type op 7 | uint64_t sync 8 | int64_t schema_id 9 | uint64_t stream_id 10 | SchemaSpace space 11 | object waiter 12 | object timeout_handle 13 | bint parse_metadata 14 | bint parse_as_tuples 15 | bint push_subscribe 16 | bint check_schema_change 17 | 18 | cdef inline Metadata metadata(self): 19 | if self.space is None: 20 | return None 21 | return self.space.metadata 22 | 23 | cdef inline WriteBuffer encode(self, bytes encoding) 24 | cdef int encode_body(self, WriteBuffer buffer) except -1 25 | 26 | 27 | cdef char *encode_key_sequence(WriteBuffer buffer, 28 | char *p, object t, 29 | Metadata metadata, 30 | bint default_none) except NULL 31 | -------------------------------------------------------------------------------- /asynctnt/iproto/coreproto.pxd: -------------------------------------------------------------------------------- 1 | cdef enum ProtocolState: 2 | PROTOCOL_IDLE = 0 3 | PROTOCOL_GREETING = 1 4 | PROTOCOL_NORMAL = 2 5 | 6 | 7 | cdef enum ConnectionState: 8 | CONNECTION_BAD = 0 9 | CONNECTION_CONNECTED = 1 10 | CONNECTION_FULL = 2 11 | 12 | 13 | cdef class CoreProtocol: 14 | cdef: 15 | object host 16 | object port 17 | 18 | bytes encoding 19 | 20 | ProtocolState state 21 | ConnectionState con_state 22 | 23 | ReadBuffer rbuf 24 | tuple version 25 | bytes salt 26 | 27 | cdef bint _is_connected(self) 28 | cdef bint _is_fully_connected(self) 29 | 30 | cdef void _write(self, buf) except * 31 | cdef void _on_data_received(self, data) 32 | cdef void _process__greeting(self) 33 | cdef void _on_greeting_received(self) 34 | cdef void _on_response_received(self, const char *buf, uint32_t buf_len) 35 | cdef void _on_connection_made(self) 36 | cdef void _on_connection_lost(self, exc) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import unittest 5 | 6 | from ._testbase import TarantoolTestCase 7 | 8 | CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | class BaseTarantoolTestCase(TarantoolTestCase): 12 | DO_CONNECT = True 13 | LOGGING_LEVEL = getattr(logging, os.getenv("LOG", "CRITICAL").upper()) 14 | LOGGING_STREAM = sys.stdout 15 | TNT_APP_LUA_PATH = os.path.join(CURRENT_DIR, "files", "app.lua") 16 | 17 | TESTER_SPACE_ID = 512 18 | TESTER_SPACE_NAME = "tester" 19 | 20 | async def truncate(self): 21 | if self.conn and self.conn.is_connected: 22 | await self.conn.call("truncate", timeout=5) 23 | 24 | def tearDown(self): 25 | if hasattr(self, "conn"): 26 | self.loop.run_until_complete(self.truncate()) 27 | super().tearDown() 28 | 29 | 30 | def suite(): 31 | loader = unittest.TestLoader() 32 | return loader.discover(CURRENT_DIR, pattern="test_*.py") 33 | 34 | 35 | if __name__ == "__main__": 36 | runner = unittest.TextTestRunner(verbosity=2) 37 | result = runner.run(suite()) 38 | sys.exit(not result.wasSuccessful()) 39 | -------------------------------------------------------------------------------- /asynctnt/iproto/rbuffer.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libc.stdint cimport uint32_t 3 | 4 | 5 | cdef inline size_t size_t_max(size_t a, size_t b): 6 | if a > b: 7 | return a 8 | return b 9 | 10 | cdef inline uint32_t nearest_power_of_2(uint32_t v): 11 | v -= 1 12 | v |= v >> 1 13 | v |= v >> 2 14 | v |= v >> 4 15 | v |= v >> 8 16 | v |= v >> 16 17 | v += 1 18 | return v 19 | 20 | @cython.final 21 | cdef class ReadBuffer: 22 | cdef: 23 | char *buf 24 | size_t initial_buffer_size # Initial buffer size, obviously 25 | size_t len # Allocated size 26 | size_t use # Used size 27 | 28 | str encoding 29 | 30 | @staticmethod 31 | cdef ReadBuffer create(str encoding, size_t initial_buffer_size= *) 32 | 33 | cdef void _reallocate(self, size_t new_size) except * 34 | cdef int extend(self, const char *data, size_t len) except -1 35 | cdef void move(self, size_t pos) 36 | cdef void move_offset(self, ssize_t offset, size_t size) except * 37 | cdef bytes get_slice(self, size_t begin, size_t end) 38 | cdef bytes get_slice_begin(self, size_t begin) 39 | cdef bytes get_slice_end(self, size_t end) 40 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/insert.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class InsertRequest(BaseRequest): 6 | 7 | cdef int encode_body(self, WriteBuffer buffer) except -1: 8 | cdef: 9 | char *begin 10 | char *p 11 | uint32_t body_map_sz 12 | uint32_t max_body_len 13 | uint32_t space_id 14 | 15 | space_id = self.space.sid 16 | 17 | body_map_sz = 2 18 | # Size description: 19 | # mp_sizeof_map(body_map_sz) 20 | # + mp_sizeof_uint(TP_SPACE) 21 | # + mp_sizeof_uint(space) 22 | # + mp_sizeof_uint(TP_TUPLE) 23 | max_body_len = 1 \ 24 | + 1 \ 25 | + 9 \ 26 | + 1 27 | 28 | buffer.ensure_allocated(max_body_len) 29 | 30 | p = begin = &buffer._buf[buffer._length] 31 | p = mp_encode_map(p, body_map_sz) 32 | p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID) 33 | p = mp_encode_uint(p, space_id) 34 | 35 | p = mp_encode_uint(p, tarantool.IPROTO_TUPLE) 36 | buffer._length += (p - begin) 37 | p = encode_key_sequence(buffer, p, self.t, self.space.metadata, True) 38 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/streams.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class BeginRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | cdef: 8 | char *p 9 | char *begin 10 | uint32_t body_map_sz 11 | uint32_t max_body_len 12 | 13 | body_map_sz = 2 14 | # Size description: 15 | max_body_len = mp_sizeof_map(body_map_sz) \ 16 | + mp_sizeof_uint(tarantool.IPROTO_TXN_ISOLATION) \ 17 | + mp_sizeof_uint(self.isolation) \ 18 | + mp_sizeof_uint(tarantool.IPROTO_TIMEOUT) \ 19 | + mp_sizeof_double(self.tx_timeout) 20 | 21 | buffer.ensure_allocated(max_body_len) 22 | 23 | p = begin = &buffer._buf[buffer._length] 24 | p = mp_encode_map(p, body_map_sz) 25 | p = mp_encode_uint(p, tarantool.IPROTO_TXN_ISOLATION) 26 | p = mp_encode_uint(p, self.isolation) 27 | p = mp_encode_uint(p, tarantool.IPROTO_TIMEOUT) 28 | p = mp_encode_double(p, self.tx_timeout) 29 | 30 | buffer._length += (p - begin) 31 | 32 | @cython.final 33 | cdef class CommitRequest(BaseRequest): 34 | pass 35 | 36 | @cython.final 37 | cdef class RollbackRequest(BaseRequest): 38 | pass 39 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/id.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | DEF IPROTO_VERSION = 3 4 | 5 | @cython.final 6 | cdef class IDRequest(BaseRequest): 7 | cdef int encode_body(self, WriteBuffer buffer) except -1: 8 | cdef: 9 | char *p 10 | char *begin 11 | uint32_t body_map_sz 12 | uint32_t max_body_len 13 | 14 | body_map_sz = 2 15 | # Size description: 16 | # mp_sizeof_map(body_map_sz) 17 | # + mp_sizeof_uint(tarantool.IPROTO_VERSION) 18 | # + mp_sizeof_uint(IPROTO_VERSION) 19 | # + mp_sizeof_uint(tarantool.IPROTO_FEATURES) 20 | # + mp_sizeof_array(0) 21 | # + max arr size 22 | max_body_len = 1 \ 23 | + 1 \ 24 | + 1 \ 25 | + 1 \ 26 | + 1 \ 27 | + 4 # Note: maximum 4 elements in array 28 | 29 | buffer.ensure_allocated(max_body_len) 30 | 31 | p = begin = &buffer._buf[buffer._length] 32 | p = mp_encode_map(p, body_map_sz) 33 | p = mp_encode_uint(p, tarantool.IPROTO_VERSION) 34 | p = mp_encode_uint(p, IPROTO_VERSION) 35 | p = mp_encode_uint(p, tarantool.IPROTO_FEATURES) 36 | p = mp_encode_array(p, 3) 37 | p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_STREAMS) 38 | p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_TRANSACTIONS) 39 | p = mp_encode_uint(p, tarantool.IPROTO_FEATURE_ERROR_EXTENSION) 40 | 41 | buffer._length += (p - begin) 42 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/delete.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class DeleteRequest(BaseRequest): 6 | 7 | cdef int encode_body(self, WriteBuffer buffer) except -1: 8 | cdef: 9 | char *p 10 | char *begin 11 | uint32_t body_map_sz 12 | uint32_t max_body_len 13 | uint32_t space_id, index_id 14 | 15 | space_id = self.space.sid 16 | index_id = self.index.iid 17 | 18 | body_map_sz = 2 \ 19 | + (index_id > 0) 20 | # Size description: 21 | # mp_sizeof_map(body_map_sz) 22 | # + mp_sizeof_uint(TP_SPACE) 23 | # + mp_sizeof_uint(space) 24 | max_body_len = 1 \ 25 | + 1 \ 26 | + 9 27 | 28 | if index_id > 0: 29 | # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index) 30 | max_body_len += 1 + 9 31 | 32 | max_body_len += 1 # mp_sizeof_uint(TP_KEY); 33 | 34 | buffer.ensure_allocated(max_body_len) 35 | 36 | p = begin = &buffer._buf[buffer._length] 37 | p = mp_encode_map(p, body_map_sz) 38 | p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID) 39 | p = mp_encode_uint(p, space_id) 40 | 41 | if index_id > 0: 42 | p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID) 43 | p = mp_encode_uint(p, index_id) 44 | 45 | p = mp_encode_uint(p, tarantool.IPROTO_KEY) 46 | buffer._length += (p - begin) 47 | p = encode_key_sequence(buffer, p, self.key, self.index.metadata, False) 48 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Basic Usage 4 | 5 | ```lua 6 | box.cfg { 7 | listen = '127.0.0.1:3301' 8 | } 9 | 10 | box.once('v1', function() 11 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 12 | 13 | local s = box.schema.create_space('tester') 14 | s:create_index('primary') 15 | s:format({ 16 | { name = 'id', type = 'unsigned' }, 17 | { name = 'name', type = 'string' }, 18 | }) 19 | end) 20 | ``` 21 | 22 | Python code: 23 | 24 | ```python 25 | import asyncio 26 | import asynctnt 27 | 28 | 29 | async def main(): 30 | conn = asynctnt.Connection(host='127.0.0.1', port=3301) 31 | await conn.connect() 32 | 33 | for i in range(1, 11): 34 | await conn.insert('tester', [i, 'hello{}'.format(i)]) 35 | 36 | data = await conn.select('tester', []) 37 | first_tuple = data[0] 38 | print('tuple:', first_tuple) 39 | print(f'tuple[0]: {first_tuple[0]}; tuple["id"]: {first_tuple["id"]}') 40 | print(f'tuple[1]: {first_tuple[1]}; tuple["name"]: {first_tuple["name"]}') 41 | 42 | await conn.disconnect() 43 | 44 | asyncio.run(main()) 45 | ``` 46 | 47 | Stdout: 48 | ``` 49 | tuple: 50 | tuple[0]: 1; tuple["id"]: 1 51 | tuple[1]: hello1; tuple["name"]: hello1 52 | ``` 53 | 54 | ## Using Connection context manager 55 | 56 | ```python 57 | import asyncio 58 | import asynctnt 59 | 60 | 61 | async def main(): 62 | async with asynctnt.Connection(port=3301) as conn: 63 | res = await conn.call('box.info') 64 | print(res.body) 65 | 66 | asyncio.run(main()) 67 | ``` 68 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/eval.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class EvalRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | cdef: 8 | char *begin 9 | char *p 10 | uint32_t body_map_sz 11 | uint32_t max_body_len 12 | 13 | bytes expression_temp 14 | char *expression_str 15 | ssize_t expression_len 16 | 17 | expression_str = NULL 18 | expression_len = 0 19 | 20 | expression_temp = encode_unicode_string(self.expression, buffer._encoding) 21 | cpython.bytes.PyBytes_AsStringAndSize(expression_temp, 22 | &expression_str, 23 | &expression_len) 24 | body_map_sz = 2 25 | # Size description: 26 | # mp_sizeof_map() 27 | # + mp_sizeof_uint(TP_EXPRESSION) 28 | # + mp_sizeof_str(expression) 29 | # + mp_sizeof_uint(TP_TUPLE) 30 | max_body_len = 1 \ 31 | + 1 \ 32 | + mp_sizeof_str( expression_len) \ 33 | + 1 34 | 35 | buffer.ensure_allocated(max_body_len) 36 | 37 | p = begin = &buffer._buf[buffer._length] 38 | p = mp_encode_map(p, body_map_sz) 39 | p = mp_encode_uint(p, tarantool.IPROTO_EXPR) 40 | p = mp_encode_str(p, expression_str, expression_len) 41 | 42 | p = mp_encode_uint(p, tarantool.IPROTO_TUPLE) 43 | buffer._length += (p - begin) 44 | p = encode_key_sequence(buffer, p, self.args, None, False) 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build local debug annotate dist docs style mypy ruff style-check lint test quicktest coverage 2 | 3 | PYTHON?=python 4 | 5 | all: local 6 | 7 | clean: 8 | pip uninstall -y asynctnt 9 | rm -rf asynctnt/*.c asynctnt/*.h asynctnt/*.cpp 10 | rm -rf asynctnt/*.so asynctnt/*.html 11 | rm -rf asynctnt/iproto/*.c asynctnt/iproto/*.h 12 | rm -rf asynctnt/iproto/*.so asynctnt/iproto/*.html asynctnt/iproto/requests/*.html 13 | rm -rf build *.egg-info .eggs dist 14 | find . -name '__pycache__' | xargs rm -rf 15 | rm -rf htmlcov 16 | rm -rf __tnt* 17 | rm -rf tests/__tnt* 18 | 19 | 20 | build: 21 | $(PYTHON) -m pip install -e '.[test,docs]' 22 | 23 | local: 24 | $(PYTHON) -m pip install -e . 25 | 26 | 27 | debug: clean 28 | ASYNCTNT_DEBUG=1 $(PYTHON) -m pip install -e '.[test]' 29 | 30 | 31 | annotate: 32 | cython -3 -a asynctnt/iproto/protocol.pyx 33 | 34 | dist: 35 | $(PYTHON) -m build . 36 | 37 | docs: build 38 | $(MAKE) -C docs html 39 | 40 | style: 41 | $(PYTHON) -m black . 42 | $(PYTHON) -m isort . 43 | 44 | mypy: 45 | $(PYTHON) -m mypy --enable-error-code ignore-without-code . 46 | 47 | ruff: 48 | $(PYTHON) -m ruff check . 49 | 50 | style-check: 51 | $(PYTHON) -m black --check --diff . 52 | $(PYTHON) -m isort --check --diff . 53 | 54 | lint: style-check ruff 55 | 56 | test: lint 57 | PYTHONASYNCIODEBUG=1 $(PYTHON) -m pytest 58 | $(PYTHON) -m pytest 59 | USE_UVLOOP=1 $(PYTHON) -m pytest 60 | 61 | quicktest: 62 | $(PYTHON) -m pytest 63 | 64 | coverage: 65 | $(PYTHON) -m pytest --cov 66 | ./scripts/run_until_success.sh $(PYTHON) -m coverage report -m 67 | ./scripts/run_until_success.sh $(PYTHON) -m coverage html 68 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/call.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | cimport asynctnt.iproto.tarantool as tarantool 4 | 5 | 6 | @cython.final 7 | cdef class CallRequest(BaseRequest): 8 | cdef int encode_body(self, WriteBuffer buffer) except -1: 9 | cdef: 10 | char *begin 11 | char *p 12 | uint32_t body_map_sz 13 | uint32_t max_body_len 14 | 15 | bytes func_name_temp 16 | char *func_name_str 17 | ssize_t func_name_len 18 | 19 | func_name_str = NULL 20 | func_name_len = 0 21 | 22 | func_name_temp = encode_unicode_string(self.func_name, buffer._encoding) 23 | cpython.bytes.PyBytes_AsStringAndSize(func_name_temp, 24 | &func_name_str, 25 | &func_name_len) 26 | body_map_sz = 2 27 | # Size description: 28 | # mp_sizeof_map() 29 | # + mp_sizeof_uint(TP_FUNCTION) 30 | # + mp_sizeof_str(func_name) 31 | # + mp_sizeof_uint(TP_TUPLE) 32 | max_body_len = 1 \ 33 | + 1 \ 34 | + mp_sizeof_str( func_name_len) \ 35 | + 1 36 | 37 | buffer.ensure_allocated(max_body_len) 38 | 39 | p = begin = &buffer._buf[buffer._length] 40 | p = mp_encode_map(p, body_map_sz) 41 | p = mp_encode_uint(p, tarantool.IPROTO_FUNCTION_NAME) 42 | p = mp_encode_str(p, func_name_str, func_name_len) 43 | 44 | p = mp_encode_uint(p, tarantool.IPROTO_TUPLE) 45 | buffer._length += (p - begin) 46 | p = encode_key_sequence(buffer, p, self.args, None, False) 47 | -------------------------------------------------------------------------------- /docs/pushes.md: -------------------------------------------------------------------------------- 1 | # Session Push 2 | 3 | Tarantool 1.10 introduced session pushes which gives an ability to receive 4 | out of bound notifications from Tarantool server 5 | 6 | For example let's consider this simple example: 7 | 8 | ```lua 9 | function sub(n) 10 | for i=1,n do 11 | box.session.push(i, i * i) 12 | end 13 | 14 | return 'done' 15 | end 16 | ``` 17 | 18 | 19 | This function will yield `n` push messages to the client before returning. 20 | To receive such notification in Python using `asynctnt` we need to subscribe 21 | first for these notifications and then use `PushIterator` to iterate over 22 | all the messages from Tarantool: 23 | 24 | ```python 25 | import asyncio 26 | import asynctnt 27 | 28 | 29 | async def main(): 30 | async with asynctnt.Connection(port=3301) as conn: 31 | fut = conn.call('sub', [10], push_subscribe=True) 32 | it = asynctnt.PushIterator(fut) 33 | 34 | async for value in it: 35 | print(value) 36 | 37 | asyncio.run(main()) 38 | ``` 39 | 40 | This will produce the following output: 41 | 42 | ```bash 43 | $ python example.py 44 | [1, 1] 45 | [2, 4] 46 | [3, 9] 47 | [4, 16] 48 | [5, 25] 49 | ``` 50 | 51 | 52 | In order to receive a return value you can simply `await` on the initially 53 | returned future from the `call()` method: 54 | 55 | ```python 56 | import asyncio 57 | import asynctnt 58 | 59 | 60 | async def main(): 61 | async with asynctnt.Connection(port=3301) as conn: 62 | fut = conn.call('sub', [10], push_subscribe=True) 63 | it = asynctnt.PushIterator(fut) 64 | 65 | async for value in it: 66 | print(value) 67 | 68 | print(await fut) # receive the response 69 | 70 | asyncio.run(main()) 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/streams.md: -------------------------------------------------------------------------------- 1 | # Streams and Transactions 2 | 3 | ## Basic usage 4 | Interactive transactions are available in Tarantool 2.10+ and are implemented on 5 | top of streams. 6 | 7 | You can easily use streams in `asynctnt`: 8 | 9 | ```python 10 | import asynctnt 11 | 12 | conn = await asynctnt.connect() 13 | 14 | async with conn.stream() as s: 15 | data = [1, 'Peter Parker'] 16 | await s.insert('heroes', data) 17 | await s.update('heroes', [1], ['=', 'name', 'Spider-Man']) 18 | 19 | res = await conn.select('heroes') 20 | print(res) 21 | ``` 22 | 23 | This syntax will call `begin()` and `commit()` methods behind the scenes and a `rollback()` 24 | method if any exception will happen inside the context manager. 25 | 26 | ## Isolation 27 | Everything happening inside in the transaction (a.k.a. stream) is visible only 28 | to the current stream. 29 | You may also control the isolation level, but you have to call `begin()` method manually: 30 | 31 | ```python 32 | import asynctnt 33 | from asynctnt.api import Isolation 34 | 35 | conn = await asynctnt.connect() 36 | 37 | s = conn.stream() 38 | await s.begin(Isolation.READ_COMMITTED) 39 | 40 | data = [1, 'Peter Parker'] 41 | await s.insert('heroes', data) 42 | await s.update('heroes', [1], ['=', 'name', 'Spider-Man']) 43 | 44 | await s.commit() 45 | 46 | res = await conn.select('heroes') 47 | print(res) 48 | ``` 49 | 50 | ## Flexibility 51 | Tarantool allows to start/end transaction with any way (of course the native functions are the fastest): 52 | ```python 53 | # begin() variants 54 | await conn.begin() 55 | await conn.call('box.begin') 56 | await conn.execute('START TRANSACTION') 57 | 58 | # commit() variants 59 | await conn.commit() 60 | await conn.call('box.commit') 61 | await conn.execute('COMMIT') 62 | 63 | # rollback() variants 64 | await conn.rollback() 65 | await conn.call('box.rollback') 66 | await conn.execute('ROLLBACK') 67 | ``` 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env*/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | .venv/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # IDE 95 | .idea 96 | .vscode 97 | 98 | # Custom 99 | *.snap 100 | *.xlog 101 | *.xctl 102 | protocol.c 103 | protocol.cpp 104 | protocol.h 105 | asynctnt/**/*.html 106 | __tnt* 107 | !third_party/**/* 108 | vinyl.meta 109 | *.vymeta 110 | deploy_key* 111 | !.ci/deploy_key.enc 112 | /core 113 | cython_debug 114 | 115 | temp 116 | -------------------------------------------------------------------------------- /docs/mpext.md: -------------------------------------------------------------------------------- 1 | # Type Extensions 2 | 3 | Tarantool supports natively Decimal, uuid, Datetime and Interval types. `asynctnt` also supports 4 | encoding/decoding of such types to Python native `Decimal`, `UUID` and `datetime` types respectively. 5 | 6 | Some examples: 7 | 8 | ```lua 9 | local s = box.schema.create_space('wallets') 10 | s:format({ 11 | { type = 'unsigned', name = 'id' }, 12 | { type = 'uuid', name = 'uuid' }, 13 | { type = 'decimal', name = 'money' }, 14 | { type = 'datetime', name = 'created_at' }, 15 | }) 16 | s:create_index('primary') 17 | ``` 18 | 19 | And some python usage: 20 | 21 | ```python 22 | import pytz 23 | import datetime 24 | import uuid 25 | import asynctnt 26 | 27 | from decimal import Decimal 28 | 29 | Moscow = pytz.timezone('Europe/Moscow') 30 | 31 | conn = await asynctnt.connect() 32 | 33 | await conn.insert('wallets', { 34 | 'id': 1, 35 | 'uuid': uuid.uuid4(), 36 | 'money': Decimal('42.17'), 37 | 'created_at': datetime.datetime.now(tz=Moscow) 38 | }) 39 | ``` 40 | 41 | ## Interval types 42 | 43 | Tarantool has support for an interval type. `asynctnt` also has a support for this type which can be used as follows: 44 | 45 | ```python 46 | import asynctnt 47 | 48 | async with asynctnt.Connection() as conn: 49 | resp = await conn.eval(""" 50 | local datetime = require('datetime') 51 | return datetime.interval.new({ 52 | year=1, 53 | month=2, 54 | week=3, 55 | day=4, 56 | hour=5, 57 | min=6, 58 | sec=7, 59 | nsec=8, 60 | }) 61 | """) 62 | 63 | assert resp[0] == asynctnt.MPInterval( 64 | year=1, 65 | month=2, 66 | week=3, 67 | day=4, 68 | hour=5, 69 | min=6, 70 | sec=7, 71 | nsec=8, 72 | ) 73 | ``` 74 | 75 | You may use `asynctnt.MPInterval` type also as parameters to Tarantool methods (like call, insert, and others). 76 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/base.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libc.stdint cimport int64_t, uint64_t 3 | 4 | 5 | @cython.freelist(REQUEST_FREELIST) 6 | cdef class BaseRequest: 7 | 8 | # def __cinit__(self): 9 | # self.sync = 0 10 | # self.schema_id = -1 11 | # self.parse_as_tuples = False 12 | # self.parse_metadata = True 13 | # self.push_subscribe = False 14 | 15 | cdef inline WriteBuffer encode(self, bytes encoding): 16 | cdef WriteBuffer buffer = WriteBuffer.create(encoding) 17 | buffer.write_header(self.sync, self.op, self.schema_id, self.stream_id) 18 | self.encode_body(buffer) 19 | buffer.write_length() 20 | return buffer 21 | 22 | cdef int encode_body(self, WriteBuffer buffer) except -1: 23 | return 0 24 | 25 | def __repr__(self): # pragma: nocover 26 | return \ 27 | ''.format( 28 | self.op, 29 | self.sync, 30 | self.schema_id, 31 | self.push_subscribe 32 | ) 33 | 34 | 35 | cdef char *encode_key_sequence(WriteBuffer buffer, 36 | char *p, object t, 37 | Metadata metadata, 38 | bint default_none) except NULL: 39 | if isinstance(t, list) or t is None: 40 | return buffer.mp_encode_list(p, t) 41 | elif isinstance(t, tuple): 42 | return buffer.mp_encode_tuple(p, t) 43 | elif isinstance(t, dict) and metadata is not None: 44 | return buffer.mp_encode_list( 45 | p, dict_to_list_fields( t, metadata, default_none) 46 | ) 47 | else: 48 | if metadata is not None: 49 | msg = 'sequence must be either list, tuple or dict' 50 | else: 51 | msg = 'sequence must be either list or tuple' 52 | raise TypeError( 53 | '{}, got: {}'.format(msg, type(t)) 54 | ) 55 | -------------------------------------------------------------------------------- /asynctnt/iproto/tupleobj/tupleobj.h: -------------------------------------------------------------------------------- 1 | #ifndef ATNT_TUPLEOBJ_H 2 | #define ATNT_TUPLEOBJ_H 3 | 4 | #include "Python.h" 5 | #include "protocol.h" 6 | 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | 11 | #if defined(PYPY_VERSION) 12 | # define CPy_TRASHCAN_BEGIN(op, dealloc) do {} while(0); 13 | # define CPy_TRASHCAN_END(op) do {} while(0); 14 | #else 15 | 16 | #if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 8 17 | # define CPy_TRASHCAN_BEGIN(op, dealloc) Py_TRASHCAN_BEGIN(op, dealloc) 18 | # define CPy_TRASHCAN_END(op) Py_TRASHCAN_END 19 | #else 20 | # define CPy_TRASHCAN_BEGIN(op, dealloc) Py_TRASHCAN_SAFE_BEGIN(op) 21 | # define CPy_TRASHCAN_END(op) Py_TRASHCAN_SAFE_END(op) 22 | #endif 23 | 24 | #endif 25 | 26 | /* Largest ttuple to save on free list */ 27 | #define AtntTuple_MAXSAVESIZE 20 28 | 29 | /* Maximum number of ttuples of each size to save */ 30 | #define AtntTuple_MAXFREELIST 2000 31 | 32 | 33 | typedef struct { 34 | PyObject_HEAD 35 | PyObject *mapping; 36 | PyObject *keys; 37 | } AtntTupleDescObject; 38 | 39 | 40 | typedef struct { 41 | PyObject_VAR_HEAD 42 | Py_hash_t self_hash; 43 | struct C_Metadata *metadata; 44 | PyObject *ob_item[1]; 45 | 46 | /* ob_item contains space for 'ob_size' elements. 47 | * Items must normally not be NULL, except during construction when 48 | * the ttuple is not yet visible outside the function that builds it. 49 | */ 50 | } AtntTupleObject; 51 | 52 | 53 | extern PyTypeObject AtntTuple_Type; 54 | extern PyTypeObject AtntTupleIter_Type; 55 | extern PyTypeObject AtntTupleItems_Type; 56 | 57 | extern PyTypeObject AtntTupleDesc_Type; 58 | 59 | #define AtntTuple_CheckExact(o) (Py_TYPE(o) == &AtntTuple_Type) 60 | #define C_Metadata_CheckExact(o) (Py_TYPE(o) == &C_Metadata_Type) 61 | 62 | #define AtntTuple_SET_ITEM(op, i, v) \ 63 | (((AtntTupleObject *)(op))->ob_item[i] = v) 64 | #define AtntTuple_GET_ITEM(op, i) \ 65 | (((AtntTupleObject *)(op))->ob_item[i]) 66 | 67 | PyTypeObject *AtntTuple_InitTypes(void); 68 | PyObject *AtntTuple_New(PyObject *, Py_ssize_t); 69 | 70 | #ifdef __cplusplus 71 | } 72 | #endif 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /tests/test_op_ping.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | from asynctnt import Response 5 | from asynctnt.exceptions import TarantoolNotConnectedError 6 | from tests import BaseTarantoolTestCase 7 | 8 | 9 | class PingTestCase(BaseTarantoolTestCase): 10 | SMALL_TIMEOUT = 0.00000000001 11 | 12 | async def test__ping_basic(self): 13 | res = await self.conn.ping() 14 | self.assertIsNotNone(res) 15 | self.assertIsInstance(res, Response) 16 | self.assertGreater(res.sync, 0, "Sync is not 0") 17 | self.assertEqual(res.code, 0, "Code is 0") 18 | self.assertEqual(res.return_code, 0, "Return code is 0") 19 | 20 | with warnings.catch_warnings(): 21 | warnings.simplefilter("ignore", DeprecationWarning) 22 | self.assertIsNone(res.body, "No body for ping") 23 | 24 | async def test__ping_timeout_on_conn(self): 25 | await self.tnt_reconnect(request_timeout=self.SMALL_TIMEOUT) 26 | self.assertEqual(self.conn.request_timeout, self.SMALL_TIMEOUT) 27 | 28 | try: 29 | await self.conn.ping(timeout=1) 30 | except Exception as e: 31 | self.fail("Should not fail on timeout 1: {}".format(e)) 32 | 33 | async def test__ping_connection_lost(self): 34 | self.tnt.stop() 35 | await self.sleep(0) 36 | 37 | try: 38 | os.kill(self.tnt.pid, 0) 39 | running = True 40 | except Exception: 41 | running = False 42 | 43 | with self.assertRaises(TarantoolNotConnectedError): 44 | res = await self.conn.ping() 45 | print(res) 46 | print("running", running) 47 | print(os.system("ps aux | grep tarantool")) 48 | 49 | self.tnt.start() 50 | await self.sleep(1) 51 | 52 | try: 53 | await self.conn.ping() 54 | except Exception as e: 55 | self.fail("Should not fail on timeout 1: {}".format(e)) 56 | 57 | async def test__ping_with_reconnect(self): 58 | await self.conn.reconnect() 59 | res = await self.conn.ping() 60 | self.assertIsInstance(res, Response, "Ping result") 61 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/prepare.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class PrepareRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | cdef: 8 | char *begin 9 | char *p 10 | uint32_t body_map_sz 11 | uint32_t max_body_len 12 | 13 | bytes query_temp 14 | char *query_str 15 | ssize_t query_len 16 | uint32_t kind 17 | 18 | body_map_sz = 1 19 | max_body_len = 0 20 | 21 | query_str = NULL 22 | query_len = 0 23 | 24 | if self.query is not None: 25 | query_temp = encode_unicode_string(self.query, buffer._encoding) 26 | cpython.bytes.PyBytes_AsStringAndSize(query_temp, 27 | &query_str, 28 | &query_len) 29 | # Size description: 30 | # mp_sizeof_map() 31 | # + mp_sizeof_uint(TP_SQL_TEXT) 32 | # + mp_sizeof_str(query) 33 | # + mp_sizeof_uint(TP_SQL_BIND) 34 | max_body_len = 1 \ 35 | + 1 \ 36 | + mp_sizeof_str( query_len) \ 37 | + 1 38 | kind = tarantool.IPROTO_SQL_TEXT 39 | else: 40 | # Size description: 41 | # mp_sizeof_map() 42 | # + mp_sizeof_uint(IPROTO_STMT_ID) 43 | # + mp_sizeof_int(self.statement_id) 44 | # + mp_sizeof_uint(TP_SQL_BIND) 45 | max_body_len = 1 \ 46 | + 1 \ 47 | + 9 \ 48 | + 1 49 | kind = tarantool.IPROTO_STMT_ID 50 | 51 | buffer.ensure_allocated(max_body_len) 52 | 53 | p = begin = &buffer._buf[buffer._length] 54 | p = mp_encode_map(p, body_map_sz) 55 | p = mp_encode_uint(p, kind) 56 | if query_str != NULL: 57 | p = mp_encode_str(p, query_str, query_len) 58 | else: 59 | p = mp_encode_uint(p, self.statement_id) 60 | 61 | buffer._length += (p - begin) 62 | -------------------------------------------------------------------------------- /asynctnt/iproto/schema.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport int64_t 2 | 3 | 4 | cdef class Field: 5 | cdef: 6 | readonly str name 7 | readonly str type 8 | readonly str collation 9 | readonly object is_nullable 10 | readonly object is_autoincrement 11 | readonly str span 12 | 13 | 14 | cdef public class Metadata [object C_Metadata, type C_Metadata_Type]: 15 | cdef: 16 | readonly list fields 17 | readonly dict name_id_map 18 | list names 19 | 20 | cdef inline int len(self) 21 | cdef inline void add(self, int id, Field field) 22 | cdef inline str name_by_id(self, int i) 23 | cdef inline int id_by_name(self, str name) except * 24 | cdef inline int id_by_name_safe(self, str name) except* 25 | 26 | 27 | cdef class SchemaIndex: 28 | cdef: 29 | readonly int sid 30 | readonly int iid 31 | readonly str name 32 | readonly str index_type 33 | readonly object unique 34 | readonly Metadata metadata 35 | 36 | 37 | cdef class SchemaDummyIndex(SchemaIndex): 38 | pass 39 | 40 | 41 | cdef class SchemaSpace: 42 | cdef: 43 | readonly int sid 44 | readonly int owner 45 | readonly str name 46 | readonly str engine 47 | readonly int field_count 48 | readonly object flags 49 | 50 | readonly Metadata metadata 51 | readonly dict indexes 52 | 53 | cdef void add_index(self, SchemaIndex idx) 54 | cdef SchemaIndex get_index(self, index, create_dummy=*) 55 | 56 | 57 | cdef class SchemaDummySpace(SchemaSpace): 58 | pass 59 | 60 | 61 | cdef class Schema: 62 | cdef: 63 | readonly dict spaces 64 | readonly int id 65 | 66 | cdef SchemaSpace get_space(self, space) 67 | cdef SchemaSpace create_dummy_space(self, int space_id) 68 | cdef SchemaSpace get_or_create_space(self, space) 69 | 70 | cdef SchemaSpace parse_space(self, space_row) 71 | cdef SchemaIndex parse_index(self, index_row) 72 | 73 | cdef inline clear(self) 74 | 75 | @staticmethod 76 | cdef Schema parse(int64_t schema_id, spaces, indexes) 77 | 78 | 79 | cdef list dict_to_list_fields(dict d, Metadata metadata, bint default_none) 80 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/execute.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class ExecuteRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | cdef: 8 | char *begin 9 | char *p 10 | uint32_t body_map_sz 11 | uint32_t max_body_len 12 | 13 | bytes query_temp 14 | char *query_str 15 | ssize_t query_len 16 | uint32_t kind 17 | 18 | body_map_sz = 2 19 | max_body_len = 0 20 | 21 | query_str = NULL 22 | query_len = 0 23 | 24 | if self.query is not None: 25 | query_temp = encode_unicode_string(self.query, buffer._encoding) 26 | cpython.bytes.PyBytes_AsStringAndSize(query_temp, 27 | &query_str, 28 | &query_len) 29 | # Size description: 30 | # mp_sizeof_map() 31 | # + mp_sizeof_uint(TP_SQL_TEXT) 32 | # + mp_sizeof_str(query) 33 | # + mp_sizeof_uint(TP_SQL_BIND) 34 | max_body_len = 1 \ 35 | + 1 \ 36 | + mp_sizeof_str( query_len) \ 37 | + 1 38 | kind = tarantool.IPROTO_SQL_TEXT 39 | else: 40 | # Size description: 41 | # mp_sizeof_map() 42 | # + mp_sizeof_uint(IPROTO_STMT_ID) 43 | # + mp_sizeof_int(self.statement_id) 44 | # + mp_sizeof_uint(TP_SQL_BIND) 45 | max_body_len = 1 \ 46 | + 1 \ 47 | + 9 \ 48 | + 1 49 | kind = tarantool.IPROTO_STMT_ID 50 | 51 | buffer.ensure_allocated(max_body_len) 52 | 53 | p = begin = &buffer._buf[buffer._length] 54 | p = mp_encode_map(p, body_map_sz) 55 | p = mp_encode_uint(p, kind) 56 | if query_str != NULL: 57 | p = mp_encode_str(p, query_str, query_len) 58 | else: 59 | p = mp_encode_uint(p, self.statement_id) 60 | 61 | p = mp_encode_uint(p, tarantool.IPROTO_SQL_BIND) 62 | buffer._length += (p - begin) 63 | p = encode_key_sequence(buffer, p, self.args, None, False) 64 | -------------------------------------------------------------------------------- /asynctnt/iproto/response.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libc.stdint cimport int32_t, int64_t, uint32_t, uint64_t 3 | 4 | 5 | cdef struct Header: 6 | int32_t code 7 | int32_t return_code 8 | uint64_t sync 9 | int64_t schema_id 10 | 11 | cdef class Response: 12 | cdef: 13 | int32_t code_ 14 | int32_t return_code_ 15 | uint64_t sync_ 16 | int64_t schema_id_ 17 | readonly str errmsg 18 | readonly IProtoError error 19 | int _rowcount 20 | readonly list body 21 | readonly bytes encoding 22 | readonly Metadata metadata 23 | readonly Metadata params 24 | readonly int params_count 25 | readonly list autoincrement_ids 26 | uint64_t stmt_id_ 27 | bint _push_subscribe 28 | BaseRequest request_ 29 | object _exception 30 | object result_ 31 | 32 | readonly object _q 33 | readonly object _push_event 34 | object _q_append 35 | object _q_popleft 36 | object _push_event_set 37 | object _push_event_clear 38 | 39 | cdef inline bint is_error(self) 40 | cdef inline uint32_t _len(self) 41 | cdef inline void init_push(self) 42 | cdef inline void add_push(self, push) 43 | cdef inline object pop_push(self) 44 | cdef inline int push_len(self) 45 | cdef inline void set_data(self, list data) 46 | cdef inline void set_exception(self, exc) 47 | cdef inline object get_exception(self) 48 | cdef inline void notify(self) 49 | 50 | cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len, 51 | Header *hdr) except -1 52 | cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len, 53 | Response resp, BaseRequest req, 54 | bint is_chunk) except -1 55 | 56 | cdef class IProtoFeatures: 57 | cdef: 58 | readonly bint streams 59 | readonly bint transactions 60 | readonly bint error_extension 61 | readonly bint watchers 62 | readonly bint pagination 63 | readonly bint space_and_index_names 64 | readonly bint watch_once 65 | readonly bint dml_tuple_extension 66 | readonly bint call_ret_tuple_extension 67 | readonly bint call_arg_tuple_extension 68 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/select.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class SelectRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | cdef: 8 | char *begin 9 | char *p 10 | uint32_t body_map_sz 11 | uint32_t max_body_len 12 | uint32_t space_id, index_id 13 | 14 | space_id = self.space.sid 15 | index_id = self.index.iid 16 | 17 | body_map_sz = 3 \ 18 | + (index_id > 0) \ 19 | + (self.offset > 0) \ 20 | + (self.iterator > 0) 21 | # Size description: 22 | # mp_sizeof_map(body_map_sz) 23 | # + mp_sizeof_uint(TP_SPACE) 24 | # + mp_sizeof_uint(space) 25 | # + mp_sizeof_uint(TP_LIMIT) 26 | # + mp_sizeof_uint(limit) 27 | max_body_len = 1 \ 28 | + 1 \ 29 | + 9 \ 30 | + 1 \ 31 | + 9 32 | 33 | if index_id > 0: 34 | # mp_sizeof_uint(TP_INDEX) + mp_sizeof_uint(index_id) 35 | max_body_len += 1 + 9 36 | if self.offset > 0: 37 | # mp_sizeof_uint(TP_OFFSET) + mp_sizeof_uint(offset) 38 | max_body_len += 1 + 9 39 | if self.iterator > 0: 40 | # mp_sizeof_uint(TP_ITERATOR) + mp_sizeof_uint(iterator) 41 | max_body_len += 1 + 1 42 | 43 | max_body_len += 1 # mp_sizeof_uint(TP_KEY); 44 | 45 | buffer.ensure_allocated(max_body_len) 46 | 47 | p = begin = &buffer._buf[buffer._length] 48 | p = mp_encode_map(p, body_map_sz) 49 | p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID) 50 | p = mp_encode_uint(p, space_id) 51 | p = mp_encode_uint(p, tarantool.IPROTO_LIMIT) 52 | p = mp_encode_uint(p, self.limit) 53 | 54 | if index_id > 0: 55 | p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID) 56 | p = mp_encode_uint(p, index_id) 57 | if self.offset > 0: 58 | p = mp_encode_uint(p, tarantool.IPROTO_OFFSET) 59 | p = mp_encode_uint(p, self.offset) 60 | if self.iterator > 0: 61 | p = mp_encode_uint(p, tarantool.IPROTO_ITERATOR) 62 | p = mp_encode_uint(p, self.iterator) 63 | 64 | p = mp_encode_uint(p, tarantool.IPROTO_KEY) 65 | buffer._length += (p - begin) 66 | p = encode_key_sequence(buffer, p, self.key, self.index.metadata, False) 67 | -------------------------------------------------------------------------------- /asynctnt/iproto/buffer.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libc.stdint cimport int64_t, uint32_t, uint64_t 3 | 4 | 5 | @cython.final 6 | cdef class WriteBuffer: 7 | cdef: 8 | # Preallocated small buffer 9 | bint _smallbuf_inuse 10 | char _smallbuf[_BUFFER_INITIAL_SIZE] 11 | 12 | char *_buf 13 | ssize_t _size # Allocated size 14 | ssize_t _length # Length of data in the buffer 15 | int _view_count # Number of memoryviews attached to the buffer 16 | 17 | bytes _encoding 18 | 19 | @staticmethod 20 | cdef WriteBuffer create(bytes encoding) 21 | 22 | cdef inline _check_readonly(self) 23 | cdef inline len(self) 24 | cdef int ensure_allocated(self, ssize_t extra_length) except -1 25 | cdef char *_ensure_allocated(self, char *p, 26 | ssize_t extra_length) except NULL 27 | cdef int _reallocate(self, ssize_t new_size) except -1 28 | cdef int write_buffer(self, WriteBuffer buf) except -1 29 | cdef int write_header(self, uint64_t sync, 30 | tarantool.iproto_type op, 31 | int64_t schema_id, 32 | uint64_t stream_id) except -1 33 | cdef void write_length(self) 34 | 35 | cdef char *mp_encode_nil(self, char *p) except NULL 36 | cdef char *mp_encode_bool(self, char *p, bint value) except NULL 37 | cdef char *mp_encode_double(self, char *p, double value) except NULL 38 | cdef char *mp_encode_uint(self, char *p, uint64_t value) except NULL 39 | cdef char *mp_encode_int(self, char *p, int64_t value) except NULL 40 | cdef char *mp_encode_str(self, char *p, 41 | const char *str, uint32_t len) except NULL 42 | cdef char *mp_encode_bin(self, char *p, 43 | const char *data, uint32_t len) except NULL 44 | cdef char *mp_encode_decimal(self, char *p, object value) except NULL 45 | cdef char *mp_encode_uuid(self, char *p, object value) except NULL 46 | cdef char *mp_encode_datetime(self, char *p, object value) except NULL 47 | cdef char *mp_encode_interval(self, char *p, MPInterval value) except NULL 48 | cdef char *mp_encode_array(self, char *p, uint32_t len) except NULL 49 | cdef char *mp_encode_map(self, char *p, uint32_t len) except NULL 50 | cdef char *mp_encode_list(self, char *p, list arr) except NULL 51 | cdef char *mp_encode_tuple(self, char *p, tuple t) except NULL 52 | cdef char *mp_encode_dict(self, char *p, dict d) except NULL 53 | cdef char *mp_encode_obj(self, char *p, object o) except NULL 54 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/datetime.pyx: -------------------------------------------------------------------------------- 1 | cimport cpython.datetime 2 | from cpython.datetime cimport PyDateTimeAPI, datetime, datetime_tzinfo, timedelta_new 3 | from libc.stdint cimport uint32_t 4 | from libc.string cimport memcpy 5 | 6 | 7 | cdef inline void datetime_zero(IProtoDateTime *dt): 8 | dt.seconds = 0 9 | dt.nsec = 0 10 | dt.tzoffset = 0 11 | dt.tzindex = 0 12 | 13 | cdef inline uint32_t datetime_len(IProtoDateTime *dt): 14 | cdef uint32_t sz 15 | sz = sizeof(int64_t) 16 | if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0: 17 | return sz + DATETIME_TAIL_SZ 18 | return sz 19 | 20 | cdef char *datetime_encode(char *p, IProtoDateTime *dt) except NULL: 21 | store_u64(p, dt.seconds) 22 | p += sizeof(dt.seconds) 23 | if dt.nsec != 0 or dt.tzoffset != 0 or dt.tzindex != 0: 24 | memcpy(p, &dt.nsec, DATETIME_TAIL_SZ) 25 | p += DATETIME_TAIL_SZ 26 | return p 27 | 28 | cdef int datetime_decode( 29 | const char ** p, 30 | uint32_t length, 31 | IProtoDateTime *dt 32 | ) except -1: 33 | delta = None 34 | tz = None 35 | 36 | dt.seconds = load_u64(p[0]) 37 | p[0] += sizeof(dt.seconds) 38 | length -= sizeof(dt.seconds) 39 | 40 | if length == 0: 41 | return 0 42 | 43 | if length != DATETIME_TAIL_SZ: 44 | raise ValueError("invalid datetime size. got {} extra bytes".format( 45 | length 46 | )) 47 | 48 | dt.nsec = load_u32(p[0]) 49 | p[0] += 4 50 | dt.tzoffset = load_u16(p[0]) 51 | p[0] += 2 52 | dt.tzindex = load_u16(p[0]) 53 | p[0] += 2 54 | 55 | cdef void datetime_from_py(datetime ob, IProtoDateTime *dt): 56 | cdef: 57 | double ts 58 | int offset 59 | ts = ob.timestamp() 60 | dt.seconds = ts 61 | dt.nsec = ((ts - dt.seconds) * 1000000) * 1000 62 | if dt.nsec < 0: 63 | # correction for negative dates 64 | dt.seconds -= 1 65 | dt.nsec += 1000000000 66 | 67 | if datetime_tzinfo(ob) is not None: 68 | offset = ob.utcoffset().total_seconds() 69 | dt.tzoffset = (offset / 60) 70 | 71 | cdef object datetime_to_py(IProtoDateTime *dt): 72 | cdef: 73 | double timestamp 74 | object tz 75 | 76 | tz = None 77 | 78 | if dt.tzoffset != 0: 79 | delta = timedelta_new(0, dt.tzoffset * 60, 0) 80 | tz = timezone_new(delta) 81 | 82 | timestamp = dt.seconds + ( dt.nsec) / 1e9 83 | return PyDateTimeAPI.DateTime_FromTimestamp( 84 | PyDateTimeAPI.DateTimeType, 85 | (timestamp,) if tz is None else (timestamp, tz), 86 | NULL, 87 | ) 88 | -------------------------------------------------------------------------------- /asynctnt/prepared.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union 2 | 3 | from .iproto import protocol 4 | 5 | if TYPE_CHECKING: # pragma: nocover 6 | from .api import Api 7 | 8 | 9 | class PreparedStatement: 10 | __slots__ = ("_api", "_query", "_stmt_id", "_params", "_params_count") 11 | 12 | def __init__(self, api: "Api", query: str): 13 | self._api = api 14 | self._query = query 15 | self._stmt_id = None 16 | self._params = None 17 | self._params_count = 0 18 | 19 | @property 20 | def id(self) -> int: 21 | """ 22 | Prepared statement id 23 | """ 24 | return self._stmt_id 25 | 26 | @property 27 | def params_count(self) -> int: 28 | """ 29 | Bound params count 30 | """ 31 | return self._params_count 32 | 33 | @property 34 | def params(self) -> Optional[protocol.Metadata]: 35 | """ 36 | Bound params metadata 37 | """ 38 | return self._params 39 | 40 | async def prepare(self, timeout: float = -1.0) -> int: 41 | """ 42 | Prepare statement 43 | 44 | :param timeout: request timeout 45 | :return: prepared statement id 46 | """ 47 | resp = await self._api.prepare_iproto(self._query, timeout=timeout) 48 | self._stmt_id = resp.stmt_id 49 | self._params = resp.params 50 | self._params_count = resp.params_count 51 | return self._stmt_id 52 | 53 | async def execute( 54 | self, 55 | args: Optional[List[Union[Dict[str, Any], Any]]] = None, 56 | *, 57 | parse_metadata: bool = True, 58 | timeout: float = -1.0, 59 | ) -> protocol.Response: 60 | """ 61 | Execute this prepared statement with specified args 62 | :param args: arguments list 63 | :param parse_metadata: whether to parse response metadata or not 64 | :param timeout: request timeout 65 | """ 66 | return await self._api.execute( 67 | query=self._stmt_id, 68 | args=args, 69 | parse_metadata=parse_metadata, 70 | timeout=timeout, 71 | ) 72 | 73 | async def unprepare(self, timeout: float = -1.0): 74 | """ 75 | Unprepare current prepared statement 76 | :param timeout: request timeout 77 | """ 78 | await self._api.unprepare_iproto(self._stmt_id, timeout=timeout) 79 | self._stmt_id = None 80 | 81 | async def __aenter__(self): 82 | """ 83 | If used as a Context Manager `prepare()` and `unprepare()` methods 84 | are called automatically 85 | """ 86 | if self._stmt_id is None: 87 | await self.prepare() 88 | return self 89 | 90 | async def __aexit__(self, exc_type, exc_val, exc_tb): 91 | if self._stmt_id is not None: 92 | await self.unprepare() 93 | -------------------------------------------------------------------------------- /asynctnt/iproto/protocol.pxd: -------------------------------------------------------------------------------- 1 | cimport asynctnt.iproto.tarantool as tarantool 2 | cimport asynctnt.iproto.tupleobj as tupleobj 3 | 4 | include "const.pxi" 5 | 6 | include "cmsgpuck.pxd" 7 | include "xd.pxd" 8 | include "python.pxd" 9 | include "bit.pxd" 10 | 11 | include "unicodeutil.pxd" 12 | include "schema.pxd" 13 | include "ext/decimal.pxd" 14 | include "ext/uuid.pxd" 15 | include "ext/error.pxd" 16 | include "ext/datetime.pxd" 17 | include "ext/interval.pxd" 18 | include "buffer.pxd" 19 | include "rbuffer.pxd" 20 | 21 | include "requests/base.pxd" 22 | include "requests/ping.pxd" 23 | include "requests/call.pxd" 24 | include "requests/eval.pxd" 25 | include "requests/select.pxd" 26 | include "requests/insert.pxd" 27 | include "requests/delete.pxd" 28 | include "requests/update.pxd" 29 | include "requests/upsert.pxd" 30 | include "requests/prepare.pxd" 31 | include "requests/execute.pxd" 32 | include "requests/id.pxd" 33 | include "requests/auth.pxd" 34 | include "requests/streams.pxd" 35 | 36 | include "response.pxd" 37 | include "db.pxd" 38 | include "push.pxd" 39 | 40 | include "coreproto.pxd" 41 | 42 | cdef enum PostConnectionState: 43 | POST_CONNECTION_NONE = 0 44 | POST_CONNECTION_ID = 10 45 | POST_CONNECTION_AUTH = 20 46 | POST_CONNECTION_SCHEMA = 30 47 | POST_CONNECTION_DONE = 100 48 | 49 | 50 | ctypedef object (*req_execute_func)(BaseProtocol, BaseRequest, float) 51 | 52 | cdef class BaseProtocol(CoreProtocol): 53 | cdef: 54 | object loop 55 | str username 56 | str password 57 | bint fetch_schema 58 | bint auto_refetch_schema 59 | float request_timeout 60 | int post_con_state 61 | 62 | object connected_fut 63 | object on_connection_made_cb 64 | object on_connection_lost_cb 65 | 66 | object _on_request_completed_cb 67 | object _on_request_timeout_cb 68 | 69 | dict _reqs 70 | uint64_t _sync 71 | Schema _schema 72 | int64_t _schema_id 73 | bint _schema_fetch_in_progress 74 | object _refetch_schema_future 75 | Db _db 76 | IProtoFeatures _features 77 | req_execute_func execute 78 | 79 | object create_future 80 | 81 | cdef void _set_connection_ready(self) 82 | cdef void _set_connection_error(self, e) 83 | cdef void _post_con_state_machine(self) 84 | 85 | cdef void _do_id(self) 86 | cdef void _do_auth(self, str username, str password) 87 | cdef void _do_fetch_schema(self, object fut) 88 | cdef object _refetch_schema(self) 89 | 90 | cdef inline uint64_t next_sync(self) 91 | cdef inline uint64_t next_stream_id(self) 92 | cdef uint32_t transform_iterator(self, iterator) except * 93 | 94 | cdef object _new_waiter_for_request(self, Response response, BaseRequest req, float timeout) 95 | cdef Db _create_db(self, bint gen_stream_id) 96 | cdef object _execute_bad(self, BaseRequest req, float timeout) 97 | cdef object _execute_normal(self, BaseRequest req, float timeout) 98 | -------------------------------------------------------------------------------- /asynctnt/iproto/db.pxd: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from libc.stdint cimport uint32_t, uint64_t 3 | 4 | 5 | @cython.final 6 | cdef class Db: 7 | cdef: 8 | uint64_t _stream_id 9 | BaseProtocol _protocol 10 | bytes _encoding 11 | 12 | @staticmethod 13 | cdef inline Db create(BaseProtocol protocol, uint64_t stream_id) 14 | 15 | cdef inline uint64_t next_sync(self) 16 | 17 | cdef object _ping(self, float timeout) 18 | 19 | cdef object _id(self, float timeout) 20 | 21 | cdef object _auth(self, 22 | bytes salt, 23 | str username, 24 | str password, 25 | float timeout) 26 | 27 | cdef object _call(self, 28 | tarantool.iproto_type op, 29 | str func_name, 30 | object args, 31 | float timeout, 32 | bint push_subscribe) 33 | 34 | cdef object _eval(self, 35 | str expression, 36 | object args, 37 | float timeout, 38 | bint push_subscribe) 39 | 40 | cdef object _select(self, 41 | object space, 42 | object index, 43 | object key, 44 | uint64_t offset, 45 | uint64_t limit, 46 | object iterator, 47 | float timeout, 48 | bint check_schema_change) 49 | 50 | cdef object _insert(self, 51 | object space, 52 | object t, 53 | bint replace, 54 | float timeout) 55 | 56 | cdef object _delete(self, 57 | object space, 58 | object index, 59 | object key, 60 | float timeout) 61 | 62 | cdef object _update(self, 63 | object space, 64 | object index, 65 | object key, 66 | list operations, 67 | float timeout) 68 | 69 | cdef object _upsert(self, 70 | object space, 71 | object t, 72 | list operations, 73 | float timeout) 74 | 75 | cdef object _execute(self, 76 | query, 77 | object args, 78 | bint parse_metadata, 79 | float timeout) 80 | 81 | cdef object _prepare(self, 82 | query, 83 | bint parse_metadata, 84 | float timeout) 85 | 86 | cdef object _begin(self, 87 | uint32_t isolation, 88 | double tx_timeout, 89 | float timeout) 90 | 91 | cdef object _commit(self, float timeout) 92 | 93 | cdef object _rollback(self, float timeout) 94 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/auth.pyx: -------------------------------------------------------------------------------- 1 | cimport cpython 2 | cimport cpython.bytes 3 | cimport cython 4 | 5 | import hashlib 6 | 7 | 8 | cdef inline bytes _sha1(tuple values): 9 | cdef object sha = hashlib.sha1() 10 | for i in values: 11 | if i is not None: 12 | sha.update(i) 13 | return sha.digest() 14 | 15 | cdef inline bytes _strxor(bytes hash1, bytes scramble): 16 | cdef: 17 | char *hash1_str 18 | ssize_t hash1_len 19 | 20 | char *scramble_str 21 | ssize_t scramble_len 22 | cpython.bytes.PyBytes_AsStringAndSize(hash1, 23 | &hash1_str, &hash1_len) 24 | cpython.bytes.PyBytes_AsStringAndSize(scramble, 25 | &scramble_str, &scramble_len) 26 | for i in range(scramble_len): 27 | scramble_str[i] = hash1_str[i] ^ scramble_str[i] 28 | return scramble 29 | 30 | @cython.final 31 | cdef class AuthRequest(BaseRequest): 32 | cdef int encode_body(self, WriteBuffer buffer) except -1: 33 | cdef: 34 | char *begin 35 | char *p 36 | uint32_t body_map_sz 37 | uint32_t max_body_len 38 | 39 | char *username_str 40 | ssize_t username_len 41 | 42 | char *scramble_str 43 | ssize_t scramble_len 44 | 45 | username_bytes = encode_unicode_string(self.username, buffer._encoding) 46 | password_bytes = encode_unicode_string(self.password, buffer._encoding) 47 | 48 | hash1 = _sha1((password_bytes,)) 49 | hash2 = _sha1((hash1,)) 50 | scramble = _sha1((self.salt, hash2)) 51 | scramble = _strxor(hash1, scramble) 52 | 53 | cpython.bytes.PyBytes_AsStringAndSize(username_bytes, 54 | &username_str, &username_len) 55 | cpython.bytes.PyBytes_AsStringAndSize(scramble, 56 | &scramble_str, &scramble_len) 57 | body_map_sz = 2 58 | # Size description: 59 | # mp_sizeof_map() 60 | # + mp_sizeof_uint(TP_USERNAME) 61 | # + mp_sizeof_str(username_len) 62 | # + mp_sizeof_uint(TP_TUPLE) 63 | # + mp_sizeof_array(2) 64 | # + mp_sizeof_str(9) (chap-sha1) 65 | # + mp_sizeof_str(SCRAMBLE_SIZE) 66 | max_body_len = 1 \ 67 | + 1 \ 68 | + mp_sizeof_str( username_len) \ 69 | + 1 \ 70 | + 1 \ 71 | + 1 + 9 \ 72 | + mp_sizeof_str( scramble_len) 73 | 74 | buffer.ensure_allocated(max_body_len) 75 | 76 | p = begin = &buffer._buf[buffer._length] 77 | p = mp_encode_map(p, body_map_sz) 78 | p = mp_encode_uint(p, tarantool.IPROTO_USER_NAME) 79 | p = mp_encode_str(p, username_str, username_len) 80 | 81 | p = mp_encode_uint(p, tarantool.IPROTO_TUPLE) 82 | p = mp_encode_array(p, 2) 83 | p = mp_encode_str(p, "chap-sha1", 9) 84 | p = mp_encode_str(p, scramble_str, scramble_len) 85 | buffer._length += (p - begin) 86 | -------------------------------------------------------------------------------- /docs/sql.md: -------------------------------------------------------------------------------- 1 | # SQL Support 2 | 3 | Tarantool 2.x supports SQL interface to the database. `asynctnt` fully supports it including prepared statements and metadata parsing. 4 | 5 | ## Basic usage 6 | 7 | Tarantool config: 8 | 9 | ```lua 10 | box.cfg { 11 | listen = '127.0.0.1:3301' 12 | } 13 | 14 | box.once('v1', function() 15 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 16 | 17 | box.execute([[ 18 | create table users ( 19 | id int primary key autoincrement, 20 | name text 21 | ) 22 | ]]) 23 | end) 24 | ``` 25 | 26 | 27 | Python code: 28 | 29 | ```python 30 | import asyncio 31 | import asynctnt 32 | 33 | 34 | async def main(): 35 | conn = asynctnt.Connection(host='127.0.0.1', port=3301) 36 | await conn.connect() 37 | 38 | await conn.execute("insert into users (name) values (?)", ['James Bond']) 39 | resp = await conn.execute("insert into users (name) values (:name)", [{':name', 'Ethan Hunt'}]) 40 | 41 | # get value of auto incremented primary key 42 | print(resp.autoincrement_ids) 43 | 44 | data = await conn.execute('select * from users') 45 | 46 | for row in data: 47 | print(row) 48 | 49 | await conn.disconnect() 50 | 51 | asyncio.run(main()) 52 | ``` 53 | 54 | 55 | Stdout: 56 | ``` 57 | [2] 58 | 59 | 60 | ``` 61 | 62 | 63 | ## Metadata 64 | 65 | You can access all the metadata associated with the SQL response, like so: 66 | 67 | ```python 68 | res = await conn.execute("select * from users") 69 | assert res.metadata.fields[0].name == 'ID' 70 | assert res.metadata.fields[0].type == 'integer' 71 | 72 | assert res.metadata.fields[1].name == 'NAME' 73 | assert res.metadata.fields[0].type == 'string' 74 | ``` 75 | 76 | ## Prepared statement 77 | 78 | You can make use of [prepared statements](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/) 79 | in order to execute the same requests more optimally. Simple example: 80 | 81 | ```python 82 | stmt = conn.prepare('select id, name from users where id = ?') 83 | async with stmt: 84 | user1 = stmt.execute([1]) 85 | assert user1.name == 'James Bond' 86 | 87 | user2 = stmt.execute([2]) 88 | assert user2.name == 'Ethan Hunt' 89 | ``` 90 | 91 | `prepare()` and `unprepare()` calls are called automatically within a prepared statement context manager. 92 | You may want to control prepare/unprepare yourself: 93 | 94 | ```python 95 | stmt = conn.prepare('select id, name from users where id = ?') 96 | 97 | await stmt.prepare() 98 | 99 | user1 = stmt.execute([1]) 100 | assert user1.name == 'James Bond' 101 | 102 | user2 = stmt.execute([2]) 103 | assert user2.name == 'Ethan Hunt' 104 | 105 | await stmt.unprepare() 106 | ``` 107 | 108 | Named parameters are also supported: 109 | 110 | ```python 111 | stmt = conn.prepare('select id, name from users where id = :id') 112 | async with stmt: 113 | user1 = stmt.execute([ 114 | {':id': 1} 115 | ]) 116 | assert user1.name == 'James Bond' 117 | 118 | user2 = stmt.execute([ 119 | {':id': 2} 120 | ]) 121 | assert user2.name == 'Ethan Hunt' 122 | ``` 123 | -------------------------------------------------------------------------------- /bench/init.lua: -------------------------------------------------------------------------------- 1 | box.cfg{ 2 | listen = 3305, 3 | wal_mode = 'none', 4 | readahead = 1 * 1024 * 1024 5 | } 6 | 7 | local function bootstrap() 8 | local b = { 9 | tarantool_ver = box.info.version, 10 | has_new_types = false, 11 | types = {} 12 | } 13 | 14 | if b.tarantool_ver >= "1.7.1-245" then 15 | b.has_new_types = true 16 | b.types.string = 'string' 17 | b.types.unsigned = 'unsigned' 18 | b.types.integer = 'integer' 19 | else 20 | b.types.string = 'str' 21 | b.types.unsigned = 'num' 22 | b.types.integer = 'int' 23 | end 24 | b.types.number = 'number' 25 | b.types.array = 'array' 26 | b.types.scalar = 'scalar' 27 | b.types.any = '*' 28 | return b 29 | end 30 | 31 | _G.B = bootstrap() 32 | 33 | 34 | box.once('access', function() 35 | pcall(box.schema.user.grant, 'guest', 'read,write,execute', 'universe') 36 | 37 | box.schema.user.create('tt', {password = 'ttp'}) 38 | -- box.schema.user.grant('tt', 'read,write,execute', 'universe') 39 | 40 | box.schema.user.create('t1', {password = 't1'}) 41 | box.schema.user.grant('t1', 'read,write,execute', 'universe') 42 | 43 | local s = box.schema.create_space('tester') 44 | --s:format({ 45 | -- {type=B.types.unsigned, name='f1'}, 46 | -- {type=B.types.string, name='f2'}, 47 | -- {type=B.types.unsigned, name='f3'}, 48 | -- {type=B.types.unsigned, name='f4'}, 49 | -- {type=B.types.any, name='f5'}, 50 | --}) 51 | s:create_index('primary') 52 | end) 53 | 54 | --box.once("v2", function() 55 | -- box.execute([[ 56 | -- create table users ( 57 | -- id int primary key, 58 | -- name text 59 | -- ); 60 | -- ]]) 61 | -- box.execute([[ 62 | -- insert into users values (1, 'one'); 63 | -- ]]) 64 | -- box.execute([[ 65 | -- insert into users values (2, 'two'); 66 | -- ]]) 67 | --end) 68 | 69 | local fiber = require('fiber') 70 | 71 | function long(t) 72 | fiber.sleep(t) 73 | return 'ok' 74 | end 75 | 76 | function test() 77 | return 'hello' 78 | end 79 | 80 | function func_param(p) 81 | return {p} 82 | end 83 | 84 | function raise() 85 | box.error{reason='my reason'} 86 | end 87 | 88 | function asyncaction() 89 | for i=1,10 do 90 | box.session.push('hello_' .. tostring(i)) 91 | fiber.sleep(0.5) 92 | end 93 | 94 | return 'hi' 95 | end 96 | 97 | 98 | local function push_messages(sync) 99 | for i=1,10 do 100 | print(i) 101 | box.session.push('hello_' .. tostring(i), sync) 102 | --box.session.push(box.execute("select * from users limit 1"), sync) 103 | fiber.sleep(0.5) 104 | print('end', i, sync) 105 | end 106 | end 107 | 108 | function asyncaction() 109 | local sync = box.session.sync() 110 | --fiber.create(function(sync) 111 | -- fiber.sleep(5) 112 | push_messages(sync) 113 | --end, sync) 114 | 115 | return 'hi' 116 | end 117 | 118 | function asyncaction() 119 | for i=1,10 do 120 | print(i) 121 | box.session.push('hello_' .. tostring(i)) 122 | fiber.sleep(0.5) 123 | end 124 | 125 | return 'hi' 126 | end 127 | 128 | require('console').start() 129 | -------------------------------------------------------------------------------- /tests/test_op_upsert.py: -------------------------------------------------------------------------------- 1 | from asynctnt import Response 2 | from asynctnt.exceptions import TarantoolSchemaError 3 | from tests import BaseTarantoolTestCase 4 | 5 | 6 | class UpsertTestCase(BaseTarantoolTestCase): 7 | async def _fill_data(self): 8 | data = [ 9 | [0, "a", 1], 10 | [1, "b", 0], 11 | ] 12 | for t in data: 13 | await self.conn.insert(self.TESTER_SPACE_ID, t) 14 | 15 | return data 16 | 17 | async def test__upsert_empty_one_assign(self): 18 | data = [0, "hello2", 1, 4, "what is up"] 19 | 20 | res = await self.conn.upsert(self.TESTER_SPACE_ID, data, [["=", 2, 2]]) 21 | self.assertIsInstance(res, Response, "Got response") 22 | self.assertEqual(res.code, 0, "success") 23 | self.assertGreater(res.sync, 0, "sync > 0") 24 | self.assertResponseEqual(res, [], "Body ok") 25 | 26 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 27 | self.assertResponseEqual(res, [data], "Body ok") 28 | 29 | async def test__upsert_update_one_assign(self): 30 | data = [0, "hello2", 1, 4, "what is up"] 31 | 32 | await self.conn.insert(self.TESTER_SPACE_ID, data) 33 | res = await self.conn.upsert(self.TESTER_SPACE_ID, data, [["=", 2, 2]]) 34 | self.assertIsInstance(res, Response, "Got response") 35 | self.assertEqual(res.code, 0, "success") 36 | self.assertGreater(res.sync, 0, "sync > 0") 37 | self.assertResponseEqual(res, [], "Body ok") 38 | 39 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 40 | data[2] = 2 41 | self.assertResponseEqual(res, [data], "Body ok") 42 | 43 | async def test__upsert_by_name(self): 44 | data = [0, "hello2", 1, 4, "what is up"] 45 | 46 | await self.conn.upsert(self.TESTER_SPACE_NAME, data, [["=", 2, 2]]) 47 | 48 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 49 | self.assertIsInstance(res, Response, "Got response") 50 | self.assertEqual(res.code, 0, "success") 51 | self.assertGreater(res.sync, 0, "sync > 0") 52 | self.assertResponseEqual(res, [data], "Body ok") 53 | 54 | async def test__upsert_by_name_no_schema(self): 55 | await self.tnt_reconnect(fetch_schema=False) 56 | 57 | with self.assertRaises(TarantoolSchemaError): 58 | await self.conn.upsert( 59 | self.TESTER_SPACE_NAME, [0, "hello", 1], [["=", 2, 2]] 60 | ) 61 | 62 | async def test__upsert_dict_key(self): 63 | data = { 64 | "f1": 0, 65 | "f2": "hello", 66 | "f3": 1, 67 | "f4": 2, 68 | "f5": 100, 69 | } 70 | 71 | res = await self.conn.upsert(self.TESTER_SPACE_ID, data, [["=", 2, 2]]) 72 | self.assertResponseEqual(res, [], "Body ok") 73 | 74 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 75 | self.assertResponseEqual(res, [[0, "hello", 1, 2, 100]], "Body ok") 76 | 77 | async def test__usert_dict_resp_no_effect(self): 78 | data = { 79 | "f1": 0, 80 | "f2": "hello", 81 | "f3": 1, 82 | "f4": 10, 83 | "f5": 1000, 84 | } 85 | 86 | res = await self.conn.upsert(self.TESTER_SPACE_ID, data, [["=", 2, 2]]) 87 | self.assertResponseEqual(res, [], "Body ok") 88 | -------------------------------------------------------------------------------- /tests/test_op_eval.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asynctnt import Response 4 | from tests import BaseTarantoolTestCase 5 | from tests.util import get_complex_param 6 | 7 | 8 | class EvalTestCase(BaseTarantoolTestCase): 9 | async def test__eval_basic(self): 10 | res = await self.conn.eval('return "hola"') 11 | 12 | self.assertIsInstance(res, Response, "Got eval response") 13 | self.assertEqual(res.code, 0, "success") 14 | self.assertGreater(res.sync, 0, "sync > 0") 15 | self.assertResponseEqual(res, ["hola"], "Body ok") 16 | 17 | async def test__eval_basic_pack(self): 18 | res = await self.conn.eval('return {"hola"}') 19 | 20 | self.assertIsInstance(res, Response, "Got eval response") 21 | self.assertEqual(res.code, 0, "success") 22 | self.assertGreater(res.sync, 0, "sync > 0") 23 | self.assertResponseEqual(res, [["hola"]], "Body ok") 24 | 25 | async def test__eval_with_param(self): 26 | args = [1, 2, 3, "hello"] 27 | res = await self.conn.eval("return ...", args) 28 | 29 | self.assertResponseEqual(res, args, "Body ok") 30 | 31 | async def test__eval_with_param_pack(self): 32 | args = [1, 2, 3, "hello"] 33 | res = await self.conn.eval("return {...}", args) 34 | 35 | self.assertResponseEqual(res, [args], "Body ok") 36 | 37 | async def test__eval_func_name_invalid_type(self): 38 | with self.assertRaises(TypeError): 39 | await self.conn.eval(12) 40 | 41 | with self.assertRaises(TypeError): 42 | await self.conn.eval([1, 2]) 43 | 44 | with self.assertRaises(TypeError): 45 | await self.conn.eval({"a": 1}) 46 | 47 | with self.assertRaises(TypeError): 48 | await self.conn.eval(b"qwer") 49 | 50 | async def test__eval_params_invalid_type(self): 51 | with self.assertRaises(TypeError): 52 | await self.conn.eval("return {...}", 220349) 53 | 54 | with self.assertRaises(TypeError): 55 | await self.conn.eval("return {...}", "hey") 56 | 57 | with self.assertRaises(TypeError): 58 | await self.conn.eval("return {...}", {1: 1, 2: 2}) 59 | 60 | async def test__eval_args_tuple(self): 61 | try: 62 | await self.conn.eval("return {...}", (1, 2)) 63 | except Exception as e: 64 | self.fail(e) 65 | 66 | async def test__eval_complex_param(self): 67 | p, cmp = get_complex_param( 68 | encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0) 69 | ) 70 | res = await self.conn.eval("return {...}", [p]) 71 | self.assertDictEqual(res[0][0], cmp, "Body ok") 72 | 73 | async def test__eval_timeout_in_time(self): 74 | try: 75 | cmd = """ 76 | local args = {...} 77 | local fiber = require("fiber") 78 | fiber.sleep(args[1]) 79 | """ 80 | await self.conn.eval(cmd, [0.1], timeout=1) 81 | except Exception as e: 82 | self.fail(e) 83 | 84 | async def test__eval_timeout_late(self): 85 | cmd = """ 86 | local args = {...} 87 | local fiber = require("fiber") 88 | fiber.sleep(args[1]) 89 | """ 90 | with self.assertRaises(asyncio.TimeoutError): 91 | await self.conn.eval(cmd, [0.3], timeout=0.1) 92 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/error.pyx: -------------------------------------------------------------------------------- 1 | cimport cpython.list 2 | cimport cython 3 | from libc.stdint cimport uint32_t 4 | 5 | 6 | @cython.final 7 | cdef class IProtoErrorStackFrame: 8 | def __repr__(self): 9 | return "".format( 10 | self.error_type, 11 | self.code, 12 | self.message, 13 | ) 14 | 15 | @cython.final 16 | cdef class IProtoError: 17 | pass 18 | 19 | cdef inline IProtoErrorStackFrame parse_iproto_error_stack_frame(const char ** b, bytes encoding): 20 | cdef: 21 | uint32_t size 22 | uint32_t key 23 | const char * s 24 | uint32_t s_len 25 | IProtoErrorStackFrame frame 26 | uint32_t unum 27 | 28 | size = 0 29 | key = 0 30 | 31 | frame = IProtoErrorStackFrame.__new__(IProtoErrorStackFrame) 32 | 33 | size = mp_decode_map(b) 34 | for _ in range(size): 35 | key = mp_decode_uint(b) 36 | 37 | if key == tarantool.MP_ERROR_TYPE: 38 | s = NULL 39 | s_len = 0 40 | s = mp_decode_str(b, &s_len) 41 | frame.error_type = decode_string(s[:s_len], encoding) 42 | 43 | elif key == tarantool.MP_ERROR_FILE: 44 | s = NULL 45 | s_len = 0 46 | s = mp_decode_str(b, &s_len) 47 | frame.file = decode_string(s[:s_len], encoding) 48 | 49 | elif key == tarantool.MP_ERROR_LINE: 50 | frame.line = mp_decode_uint(b) 51 | 52 | elif key == tarantool.MP_ERROR_MESSAGE: 53 | s = NULL 54 | s_len = 0 55 | s = mp_decode_str(b, &s_len) 56 | frame.message = decode_string(s[:s_len], encoding) 57 | 58 | elif key == tarantool.MP_ERROR_ERRNO: 59 | frame.err_no = mp_decode_uint(b) 60 | 61 | elif key == tarantool.MP_ERROR_ERRCODE: 62 | frame.code = mp_decode_uint(b) 63 | 64 | elif key == tarantool.MP_ERROR_FIELDS: 65 | if mp_typeof(b[0][0]) != MP_MAP: # pragma: nocover 66 | raise TypeError(f'iproto_error stack frame fields must be a ' 67 | f'map, but got {mp_typeof(b[0][0])}') 68 | 69 | frame.fields = _decode_obj(b, encoding) 70 | 71 | else: # pragma: nocover 72 | logger.debug(f"unknown iproto_error stack element with key {key}") 73 | mp_next(b) 74 | 75 | return frame 76 | 77 | cdef inline IProtoError iproto_error_decode(const char ** b, bytes encoding): 78 | cdef: 79 | uint32_t size 80 | uint32_t arr_size 81 | uint32_t key 82 | uint32_t i 83 | IProtoError error 84 | 85 | size = 0 86 | arr_size = 0 87 | key = 0 88 | 89 | error = IProtoError.__new__(IProtoError) 90 | 91 | size = mp_decode_map(b) 92 | for _ in range(size): 93 | key = mp_decode_uint(b) 94 | 95 | if key == tarantool.MP_ERROR_STACK: 96 | arr_size = mp_decode_array(b) 97 | error.trace = cpython.list.PyList_New(arr_size) 98 | for i in range(arr_size): 99 | el = parse_iproto_error_stack_frame(b, encoding) 100 | cpython.Py_INCREF(el) 101 | cpython.list.PyList_SET_ITEM(error.trace, i, el) 102 | else: # pragma: nocover 103 | logger.debug(f"unknown iproto_error map field with key {key}") 104 | mp_next(b) 105 | 106 | return error 107 | -------------------------------------------------------------------------------- /asynctnt/iproto/tarantool.pxd: -------------------------------------------------------------------------------- 1 | cdef enum iproto_header_key: 2 | IPROTO_REQUEST_TYPE = 0x00 3 | IPROTO_SYNC = 0x01 4 | IPROTO_REPLICA_ID = 0x02 5 | IPROTO_LSN = 0x03 6 | IPROTO_TIMESTAMP = 0x04 7 | IPROTO_SCHEMA_VERSION = 0x05 8 | IPROTO_SERVER_VERSION = 0x06 9 | IPROTO_GROUP_ID = 0x07 10 | IPROTO_STREAM_ID=0x0a 11 | 12 | 13 | cdef enum iproto_key: 14 | IPROTO_SPACE_ID = 0x10 15 | IPROTO_INDEX_ID = 0x11 16 | IPROTO_LIMIT = 0x12 17 | IPROTO_OFFSET = 0x13 18 | IPROTO_ITERATOR = 0x14 19 | IPROTO_INDEX_BASE = 0x15 20 | 21 | IPROTO_KEY = 0x20 22 | IPROTO_TUPLE = 0x21 23 | IPROTO_FUNCTION_NAME = 0x22 24 | IPROTO_USER_NAME = 0x23 25 | IPROTO_INSTANCE_UUID = 0x24 26 | IPROTO_CLUSTER_UUID = 0x25 27 | IPROTO_VCLOCK = 0x26 28 | IPROTO_EXPR = 0x27 29 | IPROTO_OPS = 0x28 30 | IPROTO_BALLOT = 0x29 31 | IPROTO_TUPLE_META = 0x2a 32 | IPROTO_OPTIONS = 0x2b 33 | 34 | IPROTO_DATA = 0x30 35 | IPROTO_ERROR_24 = 0x31 36 | IPROTO_METADATA = 0x32 37 | IPROTO_BIND_METADATA = 0x33 38 | IPROTO_BIND_COUNT = 0x34 39 | 40 | IPROTO_SQL_TEXT = 0x40 41 | IPROTO_SQL_BIND = 0x41 42 | IPROTO_SQL_INFO = 0x42 43 | IPROTO_STMT_ID = 0x43 44 | 45 | IPROTO_ERROR = 0x52 46 | IPROTO_VERSION = 0x54 47 | IPROTO_FEATURES = 0x55 48 | IPROTO_AUTH_TYPE = 0x5b 49 | IPROTO_TIMEOUT = 0x56 50 | IPROTO_TXN_ISOLATION = 0x59 51 | 52 | IPROTO_CHUNK = 0x80 53 | 54 | 55 | cdef enum iproto_metadata_key: 56 | IPROTO_FIELD_NAME = 0x00 57 | IPROTO_FIELD_TYPE = 0x01 58 | IPROTO_FIELD_COLL = 0x02 59 | IPROTO_FIELD_IS_NULLABLE = 0x03 60 | IPROTO_FIELD_IS_AUTOINCREMENT = 0x04 61 | IPROTO_FIELD_SPAN = 0x05 62 | 63 | 64 | cdef enum iproto_sql_info_key: 65 | SQL_INFO_ROW_COUNT = 0x00 66 | SQL_INFO_AUTOINCREMENT_IDS = 0x01 67 | 68 | 69 | cdef enum iproto_type: 70 | IPROTO_SELECT = 0x01 71 | IPROTO_INSERT = 0x02 72 | IPROTO_REPLACE = 0x03 73 | IPROTO_UPDATE = 0x04 74 | IPROTO_DELETE = 0x05 75 | IPROTO_CALL_16 = 0x06 76 | IPROTO_AUTH = 0x07 77 | IPROTO_EVAL = 0x08 78 | IPROTO_UPSERT = 0x09 79 | IPROTO_CALL = 0x0a 80 | IPROTO_EXECUTE = 0x0b 81 | IPROTO_PREPARE = 0x0d 82 | IPROTO_BEGIN = 0x0e 83 | IPROTO_COMMIT = 0x0f 84 | IPROTO_ROLLBACK = 0x10 85 | IPROTO_PING = 0x40 86 | IPROTO_ID = 0x49 87 | 88 | 89 | cdef enum iproto_update_operation: 90 | IPROTO_OP_ADD = 43 # ord('+') 91 | IPROTO_OP_SUB = 45 # ord('-') 92 | IPROTO_OP_AND = 38 # ord('&') 93 | IPROTO_OP_XOR = 94 # ord('^') 94 | IPROTO_OP_OR = 124 # ord('|') 95 | IPROTO_OP_DELETE = 35 # ord('#') 96 | IPROTO_OP_INSERT = 33 # ord('!') 97 | IPROTO_OP_ASSIGN = 61 # ord('=') 98 | IPROTO_OP_SPLICE = 58 # ord(':') 99 | 100 | 101 | cdef enum mp_extension_type: 102 | MP_UNKNOWN_EXTENSION = 0 103 | MP_DECIMAL = 1 104 | MP_UUID = 2 105 | MP_ERROR = 3 106 | MP_DATETIME = 4 107 | MP_INTERVAL = 6 108 | 109 | cdef enum iproto_features: 110 | IPROTO_FEATURE_STREAMS = 0 111 | IPROTO_FEATURE_TRANSACTIONS = 1 112 | IPROTO_FEATURE_ERROR_EXTENSION = 2 113 | IPROTO_FEATURE_WATCHERS = 3 114 | 115 | cdef enum iproto_error_fields: 116 | MP_ERROR_STACK = 0x00 117 | 118 | cdef enum iproto_error_stack_fields: 119 | MP_ERROR_TYPE = 0x00 120 | MP_ERROR_FILE = 0x01 121 | MP_ERROR_LINE = 0x02 122 | MP_ERROR_MESSAGE = 0x03 123 | MP_ERROR_ERRNO = 0x04 124 | MP_ERROR_ERRCODE = 0x05 125 | MP_ERROR_FIELDS = 0x06 126 | -------------------------------------------------------------------------------- /asynctnt/iproto/cmsgpuck.pxd: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport int8_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t 2 | from libc.stdio cimport FILE 3 | 4 | 5 | cdef extern from "../../third_party/msgpuck/msgpuck.h": 6 | cdef enum mp_type: 7 | MP_NIL = 0 8 | MP_UINT 9 | MP_INT 10 | MP_STR 11 | MP_BIN 12 | MP_ARRAY 13 | MP_MAP 14 | MP_BOOL 15 | MP_FLOAT 16 | MP_DOUBLE 17 | MP_EXT 18 | 19 | cdef uint8_t mp_load_u8(const char **data) 20 | cdef uint16_t mp_load_u16(const char **data) 21 | cdef uint32_t mp_load_u32(const char **data) 22 | cdef uint64_t mp_load_u64(const char **data) 23 | 24 | cdef char *mp_store_u8(char *data, uint8_t val) 25 | cdef char *mp_store_u16(char *data, uint16_t val) 26 | cdef char *mp_store_u32(char *data, uint32_t val) 27 | cdef char *mp_store_u64(char *data, uint64_t val) 28 | 29 | cdef ptrdiff_t mp_check_uint(const char *cur, const char *end) 30 | cdef ptrdiff_t mp_check_int(const char *cur, const char *end) 31 | 32 | cdef mp_type mp_typeof(const char c) 33 | 34 | cdef uint32_t mp_sizeof_array(uint32_t size) 35 | cdef char *mp_encode_array(char *data, uint32_t size) 36 | cdef uint32_t mp_decode_array(const char **data) 37 | 38 | cdef uint32_t mp_sizeof_map(uint32_t size) 39 | cdef char *mp_encode_map(char *data, uint32_t size) 40 | cdef uint32_t mp_decode_map(const char **data) 41 | 42 | cdef uint32_t mp_sizeof_uint(uint64_t num) 43 | cdef char *mp_encode_uint(char *data, uint64_t num) 44 | cdef uint64_t mp_decode_uint(const char **data) 45 | 46 | cdef uint32_t mp_sizeof_int(int64_t num) 47 | cdef char *mp_encode_int(char *data, int64_t num) 48 | cdef int64_t mp_decode_int(const char **data) 49 | cdef int mp_read_int64(const char **data, int64_t *ret) 50 | 51 | cdef uint32_t mp_sizeof_float(float num) 52 | cdef char *mp_encode_float(char *data, float num) 53 | cdef float mp_decode_float(const char **data) 54 | 55 | cdef uint32_t mp_sizeof_double(double num) 56 | cdef char *mp_encode_double(char *data, double num) 57 | cdef double mp_decode_double(const char **data) 58 | 59 | cdef char *mp_encode_strl(char *data, uint32_t len) 60 | cdef uint32_t mp_decode_strl(const char **data) 61 | 62 | cdef uint32_t mp_sizeof_str(uint32_t len) 63 | cdef char *mp_encode_str(char *data, const char *str, uint32_t len) 64 | cdef const char *mp_decode_str(const char **data, uint32_t *len) 65 | 66 | cdef char *mp_encode_binl(char *data, uint32_t len) 67 | cdef uint32_t mp_decode_binl(const char **data) 68 | 69 | cdef uint32_t mp_sizeof_bin(uint32_t len) 70 | cdef char *mp_encode_bin(char *data, const char *str, uint32_t len) 71 | cdef const char *mp_decode_bin(const char **data, uint32_t *len) 72 | 73 | cdef uint32_t mp_sizeof_extl(uint32_t len) 74 | cdef uint32_t mp_sizeof_ext(uint32_t len) 75 | cdef char *mp_encode_extl(char *data, int8_t type, uint32_t len) 76 | cdef char *mp_encode_ext(char *data, int8_t type, char *str, uint32_t len) 77 | cdef uint32_t mp_decode_extl(const char **data, int8_t *type) 78 | cdef const char *mp_decode_ext(const char **data, int8_t *type, uint32_t *len) 79 | 80 | cdef uint32_t mp_decode_strbinl(const char **data) 81 | cdef const char *mp_decode_strbin(const char **data, uint32_t *len) 82 | 83 | cdef char *mp_encode_nil(char *data) 84 | cdef void mp_decode_nil(const char **data) 85 | 86 | cdef char *mp_encode_bool(char *data, bint val) 87 | cdef bint mp_decode_bool(const char **data) 88 | 89 | cdef void mp_next(const char **data) 90 | 91 | cdef void mp_fprint(FILE *file, const char *data) 92 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/decimal.pyx: -------------------------------------------------------------------------------- 1 | from libc.stdint cimport uint32_t 2 | 3 | from decimal import Decimal 4 | 5 | 6 | cdef uint32_t decimal_len(int exponent, uint32_t digits_count): 7 | cdef: 8 | uint32_t length 9 | 10 | length = bcd_len(digits_count) 11 | if exponent > 0: 12 | length += mp_sizeof_int(-exponent) 13 | else: 14 | length += mp_sizeof_uint(-exponent) 15 | 16 | return length 17 | 18 | cdef char *decimal_encode(char *p, 19 | uint32_t digits_count, 20 | uint8_t sign, 21 | tuple digits, 22 | int exponent) except NULL: 23 | cdef: 24 | int i 25 | uint8_t byte 26 | char *out 27 | uint32_t length 28 | 29 | # encode exponent 30 | if exponent > 0: 31 | p = mp_encode_int(p, -exponent) 32 | else: 33 | p = mp_encode_uint(p, -exponent) 34 | 35 | length = bcd_len(digits_count) 36 | 37 | out = &p[length - 1] 38 | if sign == 1: 39 | byte = 0x0d 40 | else: 41 | byte = 0x0c 42 | 43 | i = digits_count - 1 44 | while out >= p: 45 | if i >= 0: 46 | byte |= ( cpython.tuple.PyTuple_GET_ITEM(digits, i)) << 4 47 | 48 | out[0] = byte 49 | byte = 0 50 | 51 | if i > 0: 52 | byte = cpython.tuple.PyTuple_GET_ITEM(digits, i - 1) & 0xf 53 | 54 | out -= 1 55 | i -= 2 56 | 57 | p = &p[length] 58 | return p 59 | 60 | cdef object decimal_decode(const char ** p, uint32_t length): 61 | cdef: 62 | int exponent 63 | uint8_t sign 64 | mp_type obj_type 65 | const char *first 66 | const char *last 67 | uint32_t digits_count 68 | uint8_t nibble 69 | 70 | sign = 0 71 | first = &p[0][0] 72 | last = first + length - 1 73 | 74 | # decode exponent 75 | obj_type = mp_typeof(p[0][0]) 76 | if obj_type == MP_UINT: 77 | exponent = - mp_decode_uint(p) 78 | elif obj_type == MP_INT: 79 | exponent = - mp_decode_int(p) 80 | else: 81 | raise TypeError('unexpected exponent type: {}'.format(obj_type)) 82 | 83 | length -= (&p[0][0] - first) 84 | first = &p[0][0] 85 | 86 | while first[0] == 0: 87 | first += 1 # skipping leading zeros 88 | 89 | sign = last[0] & 0xf # extract sign 90 | if sign == 0x0a or sign == 0x0c or sign == 0x0e or sign == 0x0f: 91 | sign = 0 92 | else: 93 | sign = 1 94 | 95 | # decode digits 96 | digits_count = (last - first) * 2 + 1 97 | if first[0] & 0xf0 == 0: 98 | digits_count -= 1 # adjust for leading zero nibble 99 | 100 | digits = cpython.tuple.PyTuple_New(digits_count) 101 | 102 | if digits_count > 0: 103 | while True: 104 | nibble = (last[0] & 0xf0) >> 4 # left nibble first 105 | item = nibble 106 | cpython.Py_INCREF(item) 107 | cpython.tuple.PyTuple_SET_ITEM(digits, digits_count - 1, item) 108 | 109 | digits_count -= 1 110 | if digits_count == 0: 111 | break 112 | last -= 1 113 | 114 | nibble = last[0] & 0x0f # right nibble 115 | item = nibble 116 | cpython.Py_INCREF(item) 117 | cpython.tuple.PyTuple_SET_ITEM(digits, digits_count - 1, item) 118 | 119 | digits_count -= 1 120 | if digits_count == 0: 121 | break 122 | 123 | p[0] += length 124 | 125 | return Decimal(( sign, digits, exponent)) 126 | -------------------------------------------------------------------------------- /asynctnt/iproto/python.pxd: -------------------------------------------------------------------------------- 1 | from cpython.version cimport PY_VERSION_HEX 2 | 3 | 4 | cdef extern from "Python.h": 5 | char *PyByteArray_AS_STRING(object obj) 6 | int Py_REFCNT(object obj) 7 | 8 | 9 | cdef extern from "datetime.h": 10 | """ 11 | /* Backport for Python 2.x */ 12 | #if PY_MAJOR_VERSION < 3 13 | #ifndef PyDateTime_DELTA_GET_DAYS 14 | #define PyDateTime_DELTA_GET_DAYS(o) (((PyDateTime_Delta*)o)->days) 15 | #endif 16 | #ifndef PyDateTime_DELTA_GET_SECONDS 17 | #define PyDateTime_DELTA_GET_SECONDS(o) (((PyDateTime_Delta*)o)->seconds) 18 | #endif 19 | #ifndef PyDateTime_DELTA_GET_MICROSECONDS 20 | #define PyDateTime_DELTA_GET_MICROSECONDS(o) (((PyDateTime_Delta*)o)->microseconds) 21 | #endif 22 | #endif 23 | /* Backport for Python < 3.6 */ 24 | #if PY_VERSION_HEX < 0x030600a4 25 | #ifndef PyDateTime_TIME_GET_FOLD 26 | #define PyDateTime_TIME_GET_FOLD(o) ((void)(o), 0) 27 | #endif 28 | #ifndef PyDateTime_DATE_GET_FOLD 29 | #define PyDateTime_DATE_GET_FOLD(o) ((void)(o), 0) 30 | #endif 31 | #endif 32 | /* Backport for Python < 3.6 */ 33 | #if PY_VERSION_HEX < 0x030600a4 34 | #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \ 35 | ((void)(fold), PyDateTimeAPI->DateTime_FromDateAndTime(year, month, day, hour, minute, second, \ 36 | microsecond, tz, PyDateTimeAPI->DateTimeType)) 37 | #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \ 38 | ((void)(fold), PyDateTimeAPI->Time_FromTime(hour, minute, second, microsecond, tz, PyDateTimeAPI->TimeType)) 39 | #else /* For Python 3.6+ so that we can pass tz */ 40 | #define __Pyx_DateTime_DateTimeWithFold(year, month, day, hour, minute, second, microsecond, tz, fold) \ 41 | PyDateTimeAPI->DateTime_FromDateAndTimeAndFold(year, month, day, hour, minute, second, \ 42 | microsecond, tz, fold, PyDateTimeAPI->DateTimeType) 43 | #define __Pyx_DateTime_TimeWithFold(hour, minute, second, microsecond, tz, fold) \ 44 | PyDateTimeAPI->Time_FromTimeAndFold(hour, minute, second, microsecond, tz, fold, PyDateTimeAPI->TimeType) 45 | #endif 46 | /* Backport for Python < 3.7 */ 47 | #if PY_VERSION_HEX < 0x030700b1 48 | #define __Pyx_TimeZone_UTC NULL 49 | #define __Pyx_TimeZone_FromOffset(offset) ((void)(offset), (PyObject*)NULL) 50 | #define __Pyx_TimeZone_FromOffsetAndName(offset, name) ((void)(offset), (void)(name), (PyObject*)NULL) 51 | #else 52 | #define __Pyx_TimeZone_UTC PyDateTime_TimeZone_UTC 53 | #define __Pyx_TimeZone_FromOffset(offset) PyTimeZone_FromOffset(offset) 54 | #define __Pyx_TimeZone_FromOffsetAndName(offset, name) PyTimeZone_FromOffsetAndName(offset, name) 55 | #endif 56 | /* Backport for Python < 3.10 */ 57 | #if PY_VERSION_HEX < 0x030a00a1 58 | #ifndef PyDateTime_TIME_GET_TZINFO 59 | #define PyDateTime_TIME_GET_TZINFO(o) \ 60 | ((((PyDateTime_Time*)o)->hastzinfo) ? ((PyDateTime_Time*)o)->tzinfo : Py_None) 61 | #endif 62 | #ifndef PyDateTime_DATE_GET_TZINFO 63 | #define PyDateTime_DATE_GET_TZINFO(o) \ 64 | ((((PyDateTime_DateTime*)o)->hastzinfo) ? ((PyDateTime_DateTime*)o)->tzinfo : Py_None) 65 | #endif 66 | #endif 67 | """ 68 | 69 | # The above macros is Python 3.7+ so we use these instead 70 | object __Pyx_TimeZone_FromOffset(object offset) 71 | 72 | 73 | cdef inline object timezone_new(object offset): 74 | if PY_VERSION_HEX < 0x030700b1: 75 | from datetime import timezone 76 | return timezone(offset) 77 | return __Pyx_TimeZone_FromOffset(offset) 78 | -------------------------------------------------------------------------------- /asynctnt/iproto/push.pyx: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | cimport cython 4 | 5 | import asynctnt 6 | 7 | 8 | cdef class PushIterator: 9 | def __init__(self, fut: asyncio.Future): 10 | """ 11 | Creates PushIterator object. In order to receive push notifications 12 | this iterator must be created. 13 | 14 | Example: 15 | 16 | .. code-block:: pycon 17 | 18 | # tarantool function: 19 | # function f() 20 | # box.session.push('hello') 21 | # return 'finished' 22 | # end 23 | 24 | >>> fut = conn.call('async_function', push_subscribe=True) 25 | >>> async for item in PushIterator(fut): 26 | ... print(item) 27 | 28 | 29 | :param fut: Future object returned from call_async, eval_sync 30 | functions 31 | :type fut: asyncio.Future 32 | """ 33 | cdef: 34 | Response response 35 | BaseRequest request 36 | if not hasattr(fut, '_response'): 37 | raise ValueError('Future is invalid. Make sure to call with ' 38 | 'a future returned from a method with ' 39 | 'push_subscribe=True flag') 40 | 41 | response = fut._response 42 | request = response.request_ 43 | 44 | if not request.push_subscribe: 45 | raise ValueError('Future is invalid. Make sure to call with ' 46 | 'a future returned from a method with ' 47 | 'push_subscribe=True flag') 48 | 49 | self._fut = fut 50 | self._request = request 51 | self._response = response 52 | 53 | def __iter__(self): 54 | raise RuntimeError('Cannot use iter with PushIterator - use aiter') 55 | 56 | def __next__(self): 57 | raise RuntimeError('Cannot use next with PushIterator - use anext') 58 | 59 | def __aiter__(self): 60 | return self 61 | 62 | @cython.iterable_coroutine 63 | async def __anext__(self): 64 | cdef Response response 65 | response = self._response 66 | 67 | if response.push_len() == 0 and response.code_ >= 0: 68 | # no more data left 69 | raise StopAsyncIteration 70 | 71 | if response.push_len() > 0: 72 | return response.pop_push() 73 | 74 | ev = response._push_event 75 | await ev.wait() 76 | 77 | exc = response.get_exception() 78 | if exc is not None: 79 | # someone needs to await the underlying future 80 | # so we do it here, and most probably (like 99%) the exception 81 | # that happened is already in the self._fut. So, await-ing it 82 | # would cause an exception to be thrown. 83 | # But if it doesnt't throw we still await the future and throw 84 | # the exception ourselves. 85 | await self._fut 86 | raise exc 87 | 88 | if response.push_len() > 0: 89 | return response.pop_push() 90 | 91 | if response.code_ >= 0: 92 | ev.clear() 93 | raise StopAsyncIteration 94 | 95 | assert False, 'Impossible condition happened. ' \ 96 | 'Please file a bug to ' \ 97 | 'https://github.com/igorcoding/asynctnt' 98 | 99 | @property 100 | def response(self): 101 | """ 102 | Return current Response object. Might be handy to know if the 103 | request is finished already while iterating over the PushIterator 104 | 105 | :rtype: asynctnt.Response 106 | """ 107 | return self._response 108 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "asynctnt" 3 | description = "A fast Tarantool Database connector for Python/asyncio." 4 | authors = [ 5 | { name = "igorcoding", email = "igorcoding@gmail.com" } 6 | ] 7 | license = {text = "Apache License, Version 2.0"} 8 | dynamic = ["version"] 9 | classifiers=[ 10 | "Development Status :: 5 - Production/Stable", 11 | "Framework :: AsyncIO", 12 | "Operating System :: POSIX", 13 | "Operating System :: MacOS :: MacOS X", 14 | "Operating System :: Microsoft :: Windows", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3 :: Only", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | 'Programming Language :: Python :: Implementation :: CPython', 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: Apache Software License", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | "Topic :: Database :: Front-Ends" 28 | ] 29 | requires-python = '>=3.7.0' 30 | readme = "README.md" 31 | dependencies = [ 32 | 'PyYAML >= 5.0', 33 | ] 34 | 35 | [project.urls] 36 | github = "https://github.com/igorcoding/asynctnt" 37 | 38 | [project.optional-dependencies] 39 | test = [ 40 | 'isort', 41 | 'black', 42 | 'ruff', 43 | 'uvloop>=0.12.3; platform_system != "Windows" and platform.python_implementation != "PyPy"', 44 | 'pytest', 45 | 'pytest-cov', 46 | 'coverage[toml]', 47 | 'pytz', 48 | 'python-dateutil', 49 | "Cython==3.0.11", # for coverage 50 | ] 51 | 52 | docs = [ 53 | 'Sphinx>=5', 54 | 'sphinx_rtd_theme', 55 | 'sphinxcontrib-asyncio', 56 | 'myst-parser', 57 | 'sphinx-autodoc-typehints', 58 | 'sphinx-autoapi', 59 | ] 60 | 61 | [build-system] 62 | requires = [ 63 | "setuptools>=60", 64 | "wheel", 65 | 66 | "Cython==3.0.11", 67 | ] 68 | build-backend = "setuptools.build_meta" 69 | 70 | [tool.setuptools] 71 | zip-safe = false 72 | 73 | [tool.setuptools.packages.find] 74 | include = ["asynctnt", "asynctnt.*"] 75 | 76 | [tool.setuptools.exclude-package-data] 77 | "*" = ["*.c", "*.h"] 78 | 79 | [tool.pytest.ini_options] 80 | addopts = "--strict --tb native" 81 | testpaths = "tests" 82 | filterwarnings = "default" 83 | 84 | [tool.coverage.run] 85 | branch = true 86 | plugins = ["Cython.Coverage"] 87 | parallel = true 88 | source = ["asynctnt/", "tests/"] 89 | omit = [ 90 | "*.pxd", 91 | "asynctnt/_testbase.py", 92 | "asynctnt/instance.py", 93 | ] 94 | 95 | [tool.coverage.report] 96 | exclude_lines = [ 97 | "pragma: no cover", 98 | "def __repr__", 99 | "if self\\.debug", 100 | "if debug", 101 | "raise AssertionError", 102 | "raise NotImplementedError", 103 | "if __name__ == .__main__.", 104 | ] 105 | show_missing = true 106 | 107 | [tool.coverage.html] 108 | directory = "htmlcov" 109 | 110 | [tool.black] 111 | extend-exclude = '(env|.env|venv|.venv).*' 112 | 113 | [tool.isort] 114 | profile = "black" 115 | multi_line_output = 3 116 | skip_glob = [ 117 | "env*", 118 | "venv*", 119 | ] 120 | 121 | 122 | [tool.ruff] 123 | lint.select = [ 124 | "E", # pycodestyle errors 125 | "W", # pycodestyle warnings 126 | "F", # pyflakes 127 | # "I", # isort 128 | "C", # flake8-comprehensions 129 | "B", # flake8-bugbear 130 | ] 131 | lint.ignore = [ 132 | "E501", # line too long, handled by black 133 | "B008", # do not perform function calls in argument defaults 134 | "C901", # too complex 135 | ] 136 | 137 | extend-exclude = [ 138 | "app/store/migrations", 139 | ] 140 | -------------------------------------------------------------------------------- /bench/benchmark.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import datetime 4 | import logging 5 | import math 6 | import sys 7 | 8 | HOST = "127.0.0.1" 9 | PORT = 3305 10 | USERNAME = "t1" 11 | PASSWORD = "t1" 12 | 13 | 14 | def main(): 15 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument( 18 | "-n", type=int, default=200000, help="number of executed requests" 19 | ) 20 | parser.add_argument("-b", type=int, default=300, help="number of bulks") 21 | args = parser.parse_args() 22 | 23 | print("Running {} requests in {} batches. ".format(args.n, args.b)) 24 | 25 | scenarios = [ 26 | ["ping", []], 27 | ["call", ["test"]], 28 | ["call", ["test"], {"push_subscribe": True}], 29 | ["eval", ['return "hello"']], 30 | ["select", [512]], 31 | ["replace", [512, [2, "hhhh"]]], 32 | ["update", [512, [2], [(":", 1, 1, 3, "yo!")]]], 33 | ["execute", ["select 1 as a, 2 as b"], {"parse_metadata": False}], 34 | ] 35 | 36 | for use_uvloop in [True]: 37 | if use_uvloop: 38 | try: 39 | import uvloop 40 | except ImportError: 41 | print("No uvloop installed. Skipping.") 42 | continue 43 | 44 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 45 | else: 46 | asyncio.set_event_loop_policy(None) 47 | asyncio.set_event_loop(None) 48 | loop = asyncio.new_event_loop() 49 | asyncio.set_event_loop(loop) 50 | 51 | print("--------- uvloop: {} --------- ".format(use_uvloop)) 52 | 53 | for name, conn_creator in [ 54 | ("asynctnt", create_asynctnt), 55 | # ('aiotarantool', create_aiotarantool), 56 | ]: 57 | conn = loop.run_until_complete(conn_creator()) 58 | for scenario in scenarios: 59 | loop.run_until_complete( 60 | async_bench( 61 | name, 62 | conn, 63 | args.n, 64 | args.b, 65 | method=scenario[0], 66 | args=scenario[1], 67 | kwargs=scenario[2] if len(scenario) > 2 else {}, 68 | ) 69 | ) 70 | 71 | 72 | async def async_bench(name, conn, n, b, method, args=None, kwargs=None): 73 | if kwargs is None: 74 | kwargs = {} 75 | if args is None: 76 | args = [] 77 | n_requests_per_bulk = math.ceil(n / b) 78 | 79 | async def bulk_f(): 80 | for _ in range(n_requests_per_bulk): 81 | await getattr(conn, method)(*args, **kwargs) 82 | 83 | start = datetime.datetime.now() 84 | coros = [asyncio.create_task(bulk_f()) for _ in range(b)] 85 | 86 | await asyncio.wait(coros) 87 | end = datetime.datetime.now() 88 | 89 | elapsed = end - start 90 | print( 91 | "{} [{}] Elapsed: {}, RPS: {}".format( 92 | name, method, elapsed, n / elapsed.total_seconds() 93 | ) 94 | ) 95 | 96 | 97 | async def create_asynctnt(): 98 | import asynctnt 99 | 100 | conn = asynctnt.Connection( 101 | host=HOST, 102 | port=PORT, 103 | username=USERNAME, 104 | password=PASSWORD, 105 | reconnect_timeout=1, 106 | fetch_schema=False, 107 | auto_refetch_schema=False, 108 | ) 109 | await conn.connect() 110 | return conn 111 | 112 | 113 | async def create_aiotarantool(): 114 | import aiotarantool 115 | 116 | conn = aiotarantool.connect(HOST, PORT, user=USERNAME, password=PASSWORD) 117 | await conn.connect() 118 | return conn 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | -------------------------------------------------------------------------------- /third_party/xd.h: -------------------------------------------------------------------------------- 1 | #ifndef XD_H__ 2 | #define XD_H__ 3 | 4 | #include 5 | #include 6 | 7 | #define HEX_SZ (16*10 + 1) 8 | #define CHR_SZ (16*8 + 1) 9 | 10 | typedef struct { 11 | uint8_t row; 12 | uint8_t hpad; 13 | uint8_t cpad; 14 | uint8_t hsp; 15 | uint8_t csp; 16 | uint8_t cols; 17 | } xd_conf; 18 | 19 | static xd_conf default_xd_conf = { 16,1,0,1,1,4 }; 20 | 21 | static char * xd_extra(char *data, size_t size, xd_conf *cf) 22 | { 23 | /* dumps size bytes of *data to stdout. Looks like: 24 | * [0000] 75 6E 6B 6E 6F 77 6E 20 30 FF 00 00 00 00 39 00 unknown 0.....9. 25 | * src = 16 bytes. 26 | * dst = 6 + 16 * 3 + 4*2 + 16 + 1 27 | * prefix byte+pad sp between col visual newline 28 | */ 29 | if (!cf) cf = &default_xd_conf; 30 | uint8_t row = cf->row; 31 | uint8_t hpad = cf->hpad; 32 | uint8_t cpad = cf->cpad; 33 | uint8_t hsp = cf->hsp; 34 | uint8_t csp = cf->csp; 35 | uint8_t sp = cf->cols; 36 | 37 | uint8_t every = (uint8_t)row / sp; 38 | 39 | char *p = data; 40 | unsigned char c; 41 | size_t n; 42 | // unsigned addr; 43 | // char bytestr[4] = {0}; 44 | char addrstr[10] = {0}; 45 | char hexstr[ HEX_SZ ] = {0}; 46 | char chrstr[ CHR_SZ ] = {0}; 47 | unsigned hex_sz = row*(2+hpad) + hsp * sp + 1; /* size = bytes<16*2> + 16* + col */ 48 | unsigned chr_sz = row*(2+cpad) + csp * sp + 1; /* size = bytes<16> + 16*cpad + col */ 49 | 50 | if ( hex_sz > HEX_SZ ) { 51 | fprintf(stderr,"Parameters too big: estimated hex size will be %u, but have only %u\n", hex_sz, HEX_SZ); 52 | return NULL; 53 | } 54 | if ( chr_sz > CHR_SZ ) { 55 | fprintf(stderr,"Parameters too big: estimated chr size will be %u, but have only %u\n", chr_sz, CHR_SZ); 56 | return NULL; 57 | } 58 | 59 | size_t sv_sz = ( size + row-1 ) * ( (uint8_t)( 6 + 3 + hex_sz + 2 + chr_sz + 1 + row-1 ) / row ); 60 | /* ^ reserve for incomplete string \n ^ emulation of ceil */ 61 | char *rv = malloc(sv_sz); 62 | if (!rv) { 63 | fprintf(stderr,"Can't allocate memory\n"); 64 | return NULL; 65 | } 66 | char *rvptr = rv; 67 | 68 | char *curhex = hexstr; 69 | char *curchr = chrstr; 70 | for(n=1; n<=size; n++) { 71 | if (n % row == 1) 72 | snprintf(addrstr, sizeof(addrstr), "%04x", ( (int)(p-data) ) & 0xffff ); 73 | 74 | c = *p; 75 | if (c < 0x20 || c > 0x7f) { 76 | c = '.'; 77 | } 78 | 79 | /* store hex str (for left side) */ 80 | snprintf(curhex, 3+hpad, "%02X%-*s", (unsigned char)*p, hpad,""); curhex += 2+hpad; 81 | 82 | /* store char str (for right side) */ 83 | snprintf(curchr, 2+cpad, "%c%-*s", c, cpad, ""); curchr += 1+cpad; 84 | 85 | //warn("n=%d, row=%d, every=%d\n",n,row,every); 86 | if( n % row == 0 ) { 87 | /* line completed */ 88 | //printf("[%-4.4s] %s %s\n", addrstr, hexstr, chrstr); 89 | rvptr += snprintf(rvptr, (p-rvptr+sv_sz) ,"[%-4.4s] %s %s\n", addrstr, hexstr, chrstr); 90 | //sv_catpvf(rv,"[%-4.4s] %-*s %-*s\n", addrstr, hex_sz-1, hexstr, chr_sz-1, chrstr); 91 | hexstr[0] = 0; curhex = hexstr; 92 | chrstr[0] = 0; curchr = chrstr; 93 | } else if( every && ( n % every == 0 ) ) { 94 | /* half line: add whitespaces */ 95 | snprintf(curhex, 1+hsp, "%-*s", hsp, ""); curhex += hsp; 96 | snprintf(curchr, 1+csp, "%-*s", csp, ""); curchr += csp; 97 | } 98 | p++; /* next byte */ 99 | } 100 | 101 | if (curhex > hexstr) { 102 | /* print rest of buffer if not empty */ 103 | //printf("[%4.4s] %s %s\n", addrstr, hexstr, chrstr); 104 | rvptr += snprintf(rvptr, (p-rvptr+sv_sz),"[%-4.4s] %-*s %-*s\n", addrstr, hex_sz-1, hexstr, chr_sz-1, chrstr); 105 | } 106 | //warn("String len: %d, sv_sz=%d",SvCUR(rv),sv_sz); 107 | return rv; 108 | } 109 | 110 | static char * xd(char *data, size_t size) 111 | { 112 | return xd_extra(data, size, NULL); 113 | } 114 | 115 | #endif 116 | -------------------------------------------------------------------------------- /asynctnt/iproto/rbuffer.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc 3 | from libc.string cimport memcpy, memmove 4 | 5 | 6 | @cython.no_gc_clear 7 | @cython.final 8 | cdef class ReadBuffer: 9 | def __cinit__(self): 10 | self.buf = NULL 11 | self.initial_buffer_size = 0 12 | self.len = 0 13 | self.use = 0 14 | self.encoding = None 15 | 16 | @staticmethod 17 | cdef ReadBuffer create(str encoding, size_t initial_buffer_size=0x80000): 18 | cdef ReadBuffer b 19 | b = ReadBuffer.__new__(ReadBuffer) 20 | 21 | b.buf = PyMem_Malloc(sizeof(char) * initial_buffer_size) 22 | if b.buf is NULL: 23 | raise MemoryError 24 | 25 | b.initial_buffer_size = initial_buffer_size 26 | b.len = initial_buffer_size 27 | b.use = 0 28 | b.encoding = encoding 29 | return b 30 | 31 | def __dealloc__(self): 32 | if self.buf is not NULL: 33 | PyMem_Free(self.buf) 34 | self.buf = NULL 35 | self.initial_buffer_size = 0 36 | self.len = 0 37 | self.use = 0 38 | 39 | cdef void _reallocate(self, size_t new_size) except *: 40 | cdef char *new_buf 41 | 42 | # print('ReadBuffer reallocate: {}'.format(new_size)) 43 | new_buf = PyMem_Realloc(self.buf, new_size) 44 | if new_buf is NULL: 45 | PyMem_Free(self.buf) 46 | self.buf = NULL 47 | self.initial_buffer_size = 0 48 | self.len = 0 49 | self.use = 0 50 | raise MemoryError 51 | self.buf = new_buf 52 | self.len = new_size 53 | 54 | cdef int extend(self, const char *data, size_t len) except -1: 55 | cdef: 56 | size_t new_size 57 | size_t dealloc_threshold 58 | new_size = self.use + len 59 | dealloc_threshold = self.len // _DEALLOCATE_RATIO 60 | if new_size > self.len: 61 | self._reallocate( 62 | size_t_max(nearest_power_of_2(new_size), self.len << 1) 63 | ) 64 | elif dealloc_threshold >= self.initial_buffer_size \ 65 | and new_size < dealloc_threshold: 66 | self._reallocate(dealloc_threshold) 67 | 68 | memcpy(&self.buf[self.use], data, len) 69 | self.use += len 70 | return 0 71 | 72 | cdef void move(self, size_t pos): 73 | cdef size_t delta = self.use - pos 74 | memmove(self.buf, &self.buf[pos], delta) 75 | self.use = delta 76 | 77 | cdef void move_offset(self, ssize_t offset, size_t size) except *: 78 | cdef size_t dealloc_threshold = self.len // _DEALLOCATE_RATIO 79 | if offset == 0: 80 | return 81 | assert offset > 0, \ 82 | 'Offset incorrect. Got: {}. use:{}, len:{}'.format( 83 | offset, self.use, self.len 84 | ) 85 | memmove(self.buf, &self.buf[offset], size) 86 | 87 | if dealloc_threshold >= self.initial_buffer_size \ 88 | and size < dealloc_threshold: 89 | self._reallocate(dealloc_threshold) 90 | 91 | cdef bytes get_slice(self, size_t begin, size_t end): 92 | cdef: 93 | ssize_t diff 94 | char *p 95 | p = &self.buf[begin] 96 | diff = end - begin 97 | return p[:diff] 98 | 99 | cdef bytes get_slice_begin(self, size_t begin): 100 | cdef: 101 | ssize_t diff 102 | char *p 103 | p = &self.buf[begin] 104 | diff = self.use - begin 105 | return p[:diff] 106 | 107 | cdef bytes get_slice_end(self, size_t end): 108 | cdef: 109 | ssize_t diff 110 | char *p 111 | p = &self.buf[0] 112 | diff = end - 0 113 | return p[:diff] 114 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # asynctnt documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Mar 12 18:57:44 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | 20 | import os 21 | import sys 22 | 23 | import sphinx_rtd_theme 24 | 25 | sys.path.insert(0, os.path.abspath("..")) 26 | 27 | 28 | def find_version(): 29 | import re 30 | 31 | for line in open("../asynctnt/__init__.py"): 32 | if line.startswith("__version__"): 33 | return re.match(r"""__version__\s*=\s*(['"])([^'"]+)\1""", line).group(2) 34 | 35 | 36 | _ver = find_version() 37 | 38 | # -- General configuration ------------------------------------------------ 39 | 40 | extensions = [ 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.viewcode", 43 | "sphinx.ext.githubpages", 44 | "sphinxcontrib.asyncio", 45 | "myst_parser", 46 | "sphinx_autodoc_typehints", 47 | "sphinx_rtd_theme", 48 | "autoapi.extension", 49 | ] 50 | 51 | templates_path = ["_templates"] 52 | source_suffix = { 53 | ".rst": "restructuredtext", 54 | ".md": "markdown", 55 | } 56 | master_doc = "index" 57 | 58 | project = "asynctnt" 59 | copyright = "2022, igorcoding" 60 | author = "igorcoding" 61 | 62 | version = _ver 63 | release = _ver 64 | 65 | language = "en" 66 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 67 | pygments_style = "sphinx" 68 | todo_include_todos = False 69 | 70 | # -- Options for HTML output ---------------------------------------------- 71 | 72 | html_theme = "sphinx_rtd_theme" 73 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 74 | html_static_path = ["_static"] 75 | 76 | # -- Options for HTMLHelp output ------------------------------------------ 77 | htmlhelp_basename = "asynctntdoc" 78 | 79 | # -- Options for LaTeX output --------------------------------------------- 80 | 81 | latex_elements = { 82 | # The paper size ('letterpaper' or 'a4paper'). 83 | # 84 | # 'papersize': 'letterpaper', 85 | # The font size ('10pt', '11pt' or '12pt'). 86 | # 87 | # 'pointsize': '10pt', 88 | # Additional stuff for the LaTeX preamble. 89 | # 90 | # 'preamble': '', 91 | # Latex figure (float) alignment 92 | # 93 | # 'figure_align': 'htbp', 94 | } 95 | 96 | # Grouping the document tree into LaTeX files. List of tuples 97 | # (source start file, target name, title, 98 | # author, documentclass [howto, manual, or own class]). 99 | latex_documents = [ 100 | (master_doc, "asynctnt.tex", "asynctnt Documentation", "igorcoding", "manual"), 101 | ] 102 | 103 | # -- Options for manual page output --------------------------------------- 104 | 105 | # One entry per manual page. List of tuples 106 | # (source start file, name, description, authors, manual section). 107 | man_pages = [(master_doc, "asynctnt", "asynctnt Documentation", [author], 1)] 108 | 109 | # -- Options for Texinfo output ------------------------------------------- 110 | 111 | # Grouping the document tree into Texinfo files. List of tuples 112 | # (source start file, target name, title, author, 113 | # dir menu entry, description, category) 114 | texinfo_documents = [ 115 | ( 116 | master_doc, 117 | "asynctnt", 118 | "asynctnt Documentation", 119 | author, 120 | "asynctnt", 121 | "One line description of project.", 122 | "Miscellaneous", 123 | ), 124 | ] 125 | 126 | always_document_param_types = True 127 | typehints_defaults = "comma" 128 | 129 | autoapi_type = "python" 130 | autoapi_dirs = ["../asynctnt"] 131 | 132 | html_context = { 133 | "display_github": True, 134 | "github_user": "igorcoding", 135 | "github_repo": "asynctnt", 136 | "github_version": "master/docs/", 137 | } 138 | -------------------------------------------------------------------------------- /tests/test_op_sql_prepared.py: -------------------------------------------------------------------------------- 1 | from asynctnt import Response 2 | from asynctnt.prepared import PreparedStatement 3 | from tests import BaseTarantoolTestCase 4 | from tests._testbase import ensure_version 5 | 6 | 7 | class SQLPreparedStatementTestCase(BaseTarantoolTestCase): 8 | @ensure_version(min=(2, 0)) 9 | async def test__basic(self): 10 | stmt = self.conn.prepare("select 1, 2") 11 | self.assertIsInstance(stmt, PreparedStatement, "Got correct instance") 12 | async with stmt: 13 | self.assertIsNotNone(stmt.id, "statement has been prepared") 14 | 15 | res = await stmt.execute() 16 | self.assertIsInstance(res, Response, "Got response") 17 | self.assertEqual(res.code, 0, "success") 18 | self.assertGreater(res.sync, 0, "sync > 0") 19 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 20 | 21 | self.assertIsNone(stmt.id, "statement has been unprepared") 22 | 23 | @ensure_version(min=(2, 0)) 24 | async def test__manual(self): 25 | stmt = self.conn.prepare("select 1, 2") 26 | self.assertIsInstance(stmt, PreparedStatement, "Got correct instance") 27 | stmt_id = await stmt.prepare() 28 | self.assertIsNotNone(stmt_id, "statement has been prepared") 29 | res = await stmt.execute() 30 | self.assertIsInstance(res, Response, "Got response") 31 | self.assertEqual(res.code, 0, "success") 32 | self.assertGreater(res.sync, 0, "sync > 0") 33 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 34 | await stmt.unprepare() 35 | 36 | @ensure_version(min=(2, 0)) 37 | async def test__manual_iproto(self): 38 | res = await self.conn.prepare_iproto("select 1, 2") 39 | self.assertEqual(res.code, 0, "success") 40 | stmt_id = res.stmt_id 41 | self.assertNotEqual(stmt_id, 0, "received statement_id") 42 | 43 | res = await self.conn.execute(stmt_id, []) 44 | self.assertIsInstance(res, Response, "Got response") 45 | self.assertEqual(res.code, 0, "success") 46 | self.assertGreater(res.sync, 0, "sync > 0") 47 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 48 | 49 | res = await self.conn.unprepare_iproto(stmt_id) 50 | self.assertEqual(res.code, 0, "success") 51 | 52 | @ensure_version(min=(2, 0)) 53 | async def test__bind(self): 54 | stmt = self.conn.prepare("select 1, 2 where 1 = ? and 2 = ?") 55 | async with stmt: 56 | res = await stmt.execute([1, 2]) 57 | self.assertIsInstance(res, Response, "Got response") 58 | self.assertEqual(res.code, 0, "success") 59 | self.assertGreater(res.sync, 0, "sync > 0") 60 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 61 | 62 | @ensure_version(min=(2, 0)) 63 | async def test__bind_metadata(self): 64 | stmt = self.conn.prepare("select 1, 2 where 1 = :a and 2 = :b") 65 | async with stmt: 66 | self.assertIsNotNone(stmt.params_count) 67 | self.assertIsNotNone(stmt.params) 68 | 69 | self.assertEqual(2, stmt.params_count) 70 | self.assertEqual(":a", stmt.params.fields[0].name) 71 | self.assertEqual("ANY", stmt.params.fields[0].type) 72 | self.assertEqual(":b", stmt.params.fields[1].name) 73 | self.assertEqual("ANY", stmt.params.fields[1].type) 74 | 75 | @ensure_version(min=(2, 0)) 76 | async def test__bind_2_execute(self): 77 | stmt = self.conn.prepare("select 1, 2 where 1 = ? and 2 = ?") 78 | async with stmt: 79 | res = await stmt.execute([1, 2]) 80 | self.assertIsInstance(res, Response, "Got response") 81 | self.assertEqual(res.code, 0, "success") 82 | self.assertGreater(res.sync, 0, "sync > 0") 83 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 84 | 85 | res = await stmt.execute([3, 4]) 86 | self.assertIsInstance(res, Response, "Got response") 87 | self.assertEqual(res.code, 0, "success") 88 | self.assertGreater(res.sync, 0, "sync > 0") 89 | self.assertResponseEqual(res, [], "Body is empty") 90 | 91 | @ensure_version(min=(2, 0)) 92 | async def test__context_manager_double_enter(self): 93 | stmt = self.conn.prepare("select 1, 2 where 1 = ? and 2 = ?") 94 | async with stmt: 95 | async with stmt: # does nothing 96 | res = await stmt.execute([1, 2]) 97 | self.assertResponseEqual(res, [[1, 2]], "Body ok") 98 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | 5 | import setuptools 6 | from setuptools.command import build_ext as setuptools_build_ext 7 | 8 | 9 | def find_version(): 10 | for line in open("asynctnt/__init__.py"): 11 | if line.startswith("__version__"): 12 | return re.match(r"""__version__\s*=\s*(['"])([^'"]+)\1""", line).group(2) 13 | 14 | 15 | CYTHON_VERSION = "3.0.11" 16 | 17 | 18 | class build_ext(setuptools_build_ext.build_ext): 19 | user_options = setuptools_build_ext.build_ext.user_options + [ 20 | ("cython-always", None, "run cythonize() even if .c files are present"), 21 | ( 22 | "cython-annotate", 23 | None, 24 | "Produce a colorized HTML version of the Cython source.", 25 | ), 26 | ("cython-directives=", None, "Cython compiler directives"), 27 | ] 28 | 29 | def initialize_options(self): 30 | if getattr(self, "_initialized", False): 31 | return 32 | 33 | super(build_ext, self).initialize_options() 34 | 35 | if os.environ.get("ASYNCTNT_DEBUG"): 36 | self.cython_always = True 37 | self.cython_annotate = True 38 | self.cython_directives = { 39 | "linetrace": True, 40 | } 41 | self.define = "CYTHON_TRACE,CYTHON_TRACE_NOGIL" 42 | self.debug = True 43 | self.gdb_debug = True 44 | else: 45 | self.cython_always = True 46 | self.cython_annotate = None 47 | self.cython_directives = None 48 | self.gdb_debug = False 49 | 50 | def finalize_options(self): 51 | if getattr(self, "_initialized", False): 52 | return 53 | 54 | need_cythonize = self.cython_always 55 | 56 | if not need_cythonize: 57 | for extension in self.distribution.ext_modules: 58 | for i, sfile in enumerate(extension.sources): 59 | if sfile.endswith(".pyx"): 60 | prefix, ext = os.path.splitext(sfile) 61 | cfile = prefix + ".c" 62 | 63 | if os.path.exists(cfile) and not self.cython_always: 64 | extension.sources[i] = cfile 65 | else: 66 | need_cythonize = True 67 | 68 | if need_cythonize: 69 | self.cythonize() 70 | 71 | super(build_ext, self).finalize_options() 72 | 73 | def cythonize(self): 74 | try: 75 | import Cython 76 | except ImportError as e: 77 | raise RuntimeError( 78 | "please install Cython to compile asynctnt from source" 79 | ) from e 80 | 81 | if Cython.__version__ != CYTHON_VERSION: 82 | raise RuntimeError( 83 | "asynctnt requires Cython version {}, got {}".format( 84 | CYTHON_VERSION, Cython.__version__ 85 | ) 86 | ) 87 | 88 | from Cython.Build import cythonize 89 | 90 | directives = {"language_level": "3"} 91 | if self.cython_directives: 92 | if isinstance(self.cython_directives, str): 93 | for directive in self.cython_directives.split(","): 94 | k, _, v = directive.partition("=") 95 | if v.lower() == "false": 96 | v = False 97 | if v.lower() == "true": 98 | v = True 99 | 100 | directives[k] = v 101 | elif isinstance(self.cython_directives, dict): 102 | directives.update(self.cython_directives) 103 | 104 | self.distribution.ext_modules[:] = cythonize( 105 | self.distribution.ext_modules, 106 | compiler_directives=directives, 107 | annotate=self.cython_annotate, 108 | gdb_debug=self.gdb_debug, 109 | ) 110 | 111 | 112 | setuptools.setup( 113 | version=find_version(), 114 | cmdclass={"build_ext": build_ext}, 115 | ext_modules=[ 116 | setuptools.extension.Extension( 117 | "asynctnt.iproto.protocol", 118 | sources=[ 119 | "asynctnt/iproto/protocol.pyx", 120 | "third_party/msgpuck/msgpuck.c", 121 | "third_party/msgpuck/hints.c", 122 | "asynctnt/iproto/tupleobj/tupleobj.c", 123 | ], 124 | include_dirs=[ 125 | "third_party", 126 | "asynctnt/iproto", 127 | ], 128 | ) 129 | ], 130 | ) 131 | -------------------------------------------------------------------------------- /tests/test_op_delete.py: -------------------------------------------------------------------------------- 1 | from asynctnt import Response 2 | from asynctnt.exceptions import TarantoolSchemaError 3 | from tests import BaseTarantoolTestCase 4 | 5 | 6 | class DeleteTestCase(BaseTarantoolTestCase): 7 | async def _fill_data(self): 8 | data = [ 9 | [0, "a", 1, 2, "hello my darling"], 10 | [1, "b", 3, 4, "hello my darling, again"], 11 | ] 12 | for t in data: 13 | await self.conn.insert(self.TESTER_SPACE_ID, t) 14 | 15 | return data 16 | 17 | async def test__delete_one(self): 18 | data = await self._fill_data() 19 | 20 | res = await self.conn.delete(self.TESTER_SPACE_ID, [data[0][0]]) 21 | self.assertIsInstance(res, Response, "Got response") 22 | self.assertEqual(res.code, 0, "success") 23 | self.assertGreater(res.sync, 0, "sync > 0") 24 | self.assertResponseEqual(res, [data[0]], "Body ok") 25 | 26 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 27 | self.assertResponseEqual(res, [], "Body ok") 28 | 29 | async def test__delete_by_name(self): 30 | data = await self._fill_data() 31 | 32 | res = await self.conn.delete(self.TESTER_SPACE_NAME, [data[0][0]]) 33 | self.assertIsInstance(res, Response, "Got response") 34 | self.assertEqual(res.code, 0, "success") 35 | self.assertGreater(res.sync, 0, "sync > 0") 36 | self.assertResponseEqual(res, [data[0]], "Body ok") 37 | 38 | res = await self.conn.select(self.TESTER_SPACE_ID, [0]) 39 | self.assertResponseEqual(res, [], "Body ok") 40 | 41 | async def test__delete_by_index_id(self): 42 | index_name = "temp_idx" 43 | res = self.tnt.command('make_third_index("{}")'.format(index_name)) 44 | index_id = res[0][0] 45 | 46 | try: 47 | await self.tnt_reconnect() 48 | 49 | data = await self._fill_data() 50 | 51 | res = await self.conn.delete( 52 | self.TESTER_SPACE_NAME, [data[1][2]], index=index_id 53 | ) 54 | self.assertIsInstance(res, Response, "Got response") 55 | self.assertEqual(res.code, 0, "success") 56 | self.assertGreater(res.sync, 0, "sync > 0") 57 | self.assertResponseEqual(res, [data[1]], "Body ok") 58 | 59 | res = await self.conn.select( 60 | self.TESTER_SPACE_ID, [data[1][2]], index=index_id 61 | ) 62 | self.assertResponseEqual(res, [], "Body ok") 63 | finally: 64 | self.tnt.command( 65 | "box.space.{}.index.{}:drop()".format( 66 | self.TESTER_SPACE_NAME, index_name 67 | ) 68 | ) 69 | 70 | async def test__delete_by_index_name(self): 71 | index_name = "temp_idx" 72 | res = self.tnt.command('make_third_index("{}")'.format(index_name)) 73 | index_id = res[0][0] 74 | 75 | try: 76 | await self.tnt_reconnect() 77 | 78 | data = await self._fill_data() 79 | 80 | res = await self.conn.delete( 81 | self.TESTER_SPACE_NAME, [data[1][2]], index=index_name 82 | ) 83 | self.assertIsInstance(res, Response, "Got response") 84 | self.assertEqual(res.code, 0, "success") 85 | self.assertGreater(res.sync, 0, "sync > 0") 86 | self.assertResponseEqual(res, [data[1]], "Body ok") 87 | 88 | res = await self.conn.select( 89 | self.TESTER_SPACE_ID, [data[1][2]], index=index_id 90 | ) 91 | self.assertResponseEqual(res, [], "Body ok") 92 | finally: 93 | self.tnt.command( 94 | "box.space.{}.index.{}:drop()".format( 95 | self.TESTER_SPACE_NAME, index_name 96 | ) 97 | ) 98 | 99 | async def test__delete_by_name_no_schema(self): 100 | await self.tnt_reconnect(fetch_schema=False) 101 | 102 | with self.assertRaises(TarantoolSchemaError): 103 | await self.conn.delete(self.TESTER_SPACE_NAME, [0]) 104 | 105 | async def test__delete_by_index_name_no_schema(self): 106 | await self.tnt_reconnect(fetch_schema=False) 107 | 108 | with self.assertRaises(TarantoolSchemaError): 109 | await self.conn.delete(self.TESTER_SPACE_ID, [0], index="primary") 110 | 111 | async def test__delete_invalid_types(self): 112 | with self.assertRaisesRegex( 113 | TypeError, "missing 2 required positional arguments: 'space' and 'key'" 114 | ): 115 | await self.conn.delete() 116 | 117 | async def test__delete_key_tuple(self): 118 | try: 119 | await self.conn.delete(self.TESTER_SPACE_ID, (1,)) 120 | except Exception as e: 121 | self.fail(e) 122 | 123 | async def test__delete_dict_key(self): 124 | data = await self._fill_data() 125 | 126 | res = await self.conn.delete(self.TESTER_SPACE_ID, {"f1": 0}) 127 | self.assertResponseEqual(res, [data[0]], "Body ok") 128 | 129 | async def test__delete_dict_resp(self): 130 | data = [0, "hello", 0, 1, "wow"] 131 | await self.conn.insert(self.TESTER_SPACE_ID, data) 132 | 133 | res = await self.conn.delete(self.TESTER_SPACE_ID, [0]) 134 | self.assertResponseEqualKV( 135 | res, [{"f1": 0, "f2": "hello", "f3": 0, "f4": 1, "f5": "wow"}] 136 | ) 137 | -------------------------------------------------------------------------------- /tests/test_op_insert.py: -------------------------------------------------------------------------------- 1 | from asynctnt import Response 2 | from asynctnt.exceptions import TarantoolSchemaError 3 | from tests import BaseTarantoolTestCase 4 | from tests.util import get_complex_param 5 | 6 | 7 | class InsertTestCase(BaseTarantoolTestCase): 8 | async def test__insert_one(self): 9 | data = [1, "hello", 1, 4, "what is up"] 10 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 11 | 12 | self.assertIsInstance(res, Response, "Got response") 13 | self.assertEqual(res.code, 0, "success") 14 | self.assertGreater(res.sync, 0, "sync > 0") 15 | self.assertResponseEqual(res, [data], "Body ok") 16 | 17 | async def test__insert_by_name(self): 18 | data = [1, "hello", 1, 4, "what is up"] 19 | res = await self.conn.insert(self.TESTER_SPACE_NAME, data) 20 | 21 | self.assertIsInstance(res, Response, "Got response") 22 | self.assertEqual(res.code, 0, "success") 23 | self.assertGreater(res.sync, 0, "sync > 0") 24 | self.assertResponseEqual(res, [data], "Body ok") 25 | 26 | async def test__insert_by_name_no_schema(self): 27 | await self.tnt_reconnect(fetch_schema=False) 28 | 29 | data = [1, "hello", 1, 4, "what is up"] 30 | with self.assertRaises(TarantoolSchemaError): 31 | await self.conn.insert(self.TESTER_SPACE_NAME, data) 32 | 33 | async def test__insert_complex_tuple(self): 34 | p, p_cmp = get_complex_param(replace_bin=False) 35 | data = [1, "hello", 1, 2, p] 36 | data_cmp = [1, "hello", 1, 2, p_cmp] 37 | 38 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 39 | self.assertResponseEqual(res, [data_cmp], "Body ok") 40 | 41 | async def test__insert_replace(self): 42 | data = [1, "hello", 1, 4, "what is up"] 43 | 44 | await self.conn.insert(self.TESTER_SPACE_ID, data) 45 | 46 | try: 47 | data = [1, "hello2", 1, 4, "what is up"] 48 | res = await self.conn.insert(self.TESTER_SPACE_ID, t=data, replace=True) 49 | 50 | self.assertResponseEqual(res, [data], "Body ok") 51 | except Exception as e: 52 | self.fail(e) 53 | 54 | async def test__insert_invalid_types(self): 55 | with self.assertRaisesRegex( 56 | TypeError, 57 | r"missing 2 required positional arguments: " r"\'space\' and \'t\'", 58 | ): 59 | await self.conn.insert() 60 | 61 | with self.assertRaisesRegex( 62 | TypeError, r"missing 1 required positional argument: \'t\'" 63 | ): 64 | await self.conn.insert(self.TESTER_SPACE_ID) 65 | 66 | async def test__replace(self): 67 | data = [1, "hello", 1, 4, "what is up"] 68 | res = await self.conn.replace(self.TESTER_SPACE_ID, data) 69 | self.assertResponseEqual(res, [data], "Body ok") 70 | 71 | data = [1, "hello2", 1, 5, "what is up"] 72 | res = await self.conn.replace(self.TESTER_SPACE_ID, data) 73 | self.assertResponseEqual(res, [data], "Body ok") 74 | 75 | async def test__replace_invalid_types(self): 76 | with self.assertRaisesRegex( 77 | TypeError, 78 | r"missing 2 required positional arguments: " r"\'space\' and \'t\'", 79 | ): 80 | await self.conn.replace() 81 | 82 | with self.assertRaisesRegex( 83 | TypeError, r"missing 1 required positional argument: \'t\'" 84 | ): 85 | await self.conn.replace(self.TESTER_SPACE_ID) 86 | 87 | async def test__insert_dict_key(self): 88 | data = { 89 | "f1": 1, 90 | "f2": "hello", 91 | "f3": 5, 92 | "f4": 6, 93 | "f5": "hello dog", 94 | } 95 | data_cmp = [1, "hello", 5, 6, "hello dog"] 96 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 97 | self.assertResponseEqual(res, [data_cmp], "Body ok") 98 | 99 | async def test__insert_dict_key_holes(self): 100 | data = { 101 | "f1": 1, 102 | "f2": "hello", 103 | "f3": 3, 104 | "f4": 6, 105 | "f5": None, 106 | } 107 | data_cmp = [1, "hello", 3, 6, None] 108 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 109 | self.assertResponseEqual(res, [data_cmp], "Body ok") 110 | 111 | async def test__insert_no_special_empty_key(self): 112 | data = { 113 | "f1": 1, 114 | "f2": "hello", 115 | "f3": 3, 116 | "f4": 6, 117 | "f5": None, 118 | } 119 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 120 | 121 | with self.assertRaises(KeyError): 122 | res[0][""] 123 | 124 | async def test__insert_dict_resp(self): 125 | data = [0, "hello", 0, 5, "wow"] 126 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 127 | self.assertResponseEqualKV( 128 | res, [{"f1": 0, "f2": "hello", "f3": 0, "f4": 5, "f5": "wow"}] 129 | ) 130 | 131 | async def test__insert_resp_extra(self): 132 | data = [0, "hello", 5, 6, "help", "common", "yo"] 133 | res = await self.conn.insert(self.TESTER_SPACE_ID, data) 134 | self.assertResponseEqual(res, [data]) 135 | 136 | async def test__insert_bin_as_str(self): 137 | try: 138 | (await self.conn.call("func_load_bin_str"))[0] 139 | except UnicodeDecodeError as e: 140 | self.fail(e) 141 | -------------------------------------------------------------------------------- /.github/workflows/actions.yaml: -------------------------------------------------------------------------------- 1 | name: asynctnt 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ ubuntu-latest, macos-latest ] 10 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.10'] 11 | tarantool: ['1.10', '2', '3'] 12 | exclude: 13 | - os: macos-latest 14 | tarantool: '1.10' 15 | - os: macos-latest 16 | tarantool: '2' 17 | - os: macos-latest 18 | python-version: '3.7' 19 | - python-version: 'pypy3.10' 20 | tarantool: '1.10' 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: recursive 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Install Tarantool ${{ matrix.tarantool }} on Ubuntu 34 | if: matrix.os == 'ubuntu-latest' 35 | run: | 36 | curl -L https://tarantool.io/release/${{ matrix.tarantool }}/installer.sh | bash 37 | sudo apt-get -y install tarantool 38 | 39 | - name: Install Tarantool ${{ matrix.tarantool }} on MacOS 40 | if: matrix.os == 'macos-latest' 41 | run: brew install tarantool 42 | 43 | - name: Install dependencies 44 | run: | 45 | python -m pip install --upgrade pip setuptools wheel coveralls 46 | - name: Run tests 47 | run: | 48 | if [[ "$RUNNER_OS" == "Linux" && ${{ matrix.python-version }} == "3.12" && ${{ matrix.tarantool }} == "3" ]]; then 49 | make build && make test 50 | make clean && make debug && make coverage 51 | # coveralls 52 | else 53 | make build && make lint && make quicktest 54 | fi 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}" 57 | 58 | build-wheels: 59 | name: Build wheels on ${{ matrix.os }} 60 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 61 | runs-on: ${{ matrix.os }} 62 | strategy: 63 | matrix: 64 | os: [ ubuntu-latest, windows-latest, macos-latest ] 65 | needs: 66 | - test 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | with: 71 | submodules: recursive 72 | 73 | - uses: actions/setup-python@v5 74 | 75 | - name: Install cibuildwheel 76 | run: python -m pip install --upgrade cibuildwheel 77 | 78 | - name: Build wheels 79 | run: python -m cibuildwheel --output-dir wheelhouse 80 | env: 81 | CIBW_BUILD: "cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* pp310-*" 82 | 83 | - uses: actions/upload-artifact@v4 84 | with: 85 | name: wheels-${{ matrix.os }} 86 | path: ./wheelhouse/*.whl 87 | 88 | publish: 89 | name: Publish wheels 90 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 91 | needs: 92 | - build-wheels 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Get tag 96 | id: get_tag 97 | run: echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} 98 | - run: echo "Current tag is ${{ steps.get_tag.outputs.TAG }}" 99 | - uses: actions/checkout@v4 100 | with: 101 | submodules: recursive 102 | - name: Set up Python 103 | uses: actions/setup-python@v5 104 | with: 105 | python-version: '3.12' 106 | 107 | - name: Install dependencies 108 | run: | 109 | python -m pip install --upgrade pip setuptools wheel twine build 110 | 111 | - uses: actions/download-artifact@v4 112 | with: 113 | name: wheels-ubuntu-latest 114 | path: wheels-ubuntu 115 | 116 | - uses: actions/download-artifact@v4 117 | with: 118 | name: wheels-macos-latest 119 | path: wheels-macos 120 | 121 | - uses: actions/download-artifact@v4 122 | with: 123 | name: wheels-windows-latest 124 | path: wheels-windows 125 | 126 | - name: Publish dist 127 | run: | 128 | python -m build . -s 129 | tree dist wheels-ubuntu wheels-macos wheels-windows 130 | twine upload dist/* wheels-ubuntu/*.whl wheels-macos/*.whl wheels-windows/*.whl 131 | env: 132 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 133 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 134 | - uses: marvinpinto/action-automatic-releases@latest 135 | with: 136 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 137 | prerelease: false 138 | title: ${{ steps.get_tag.outputs.TAG }} 139 | files: | 140 | wheels-ubuntu/*.whl 141 | wheels-macos/*.whl 142 | wheels-windows/*.whl 143 | dist/* 144 | 145 | docs: 146 | name: Publish docs 147 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 148 | runs-on: ubuntu-latest 149 | needs: 150 | - test 151 | steps: 152 | - uses: actions/checkout@v4 153 | with: 154 | submodules: recursive 155 | 156 | - name: Set up Python 157 | uses: actions/setup-python@v5 158 | with: 159 | python-version: '3.12' 160 | 161 | - name: Install dependencies 162 | run: | 163 | python -m pip install --upgrade pip setuptools wheel build 164 | make build 165 | 166 | - name: Build docs 167 | run: make docs 168 | 169 | - name: Deploy 170 | uses: JamesIves/github-pages-deploy-action@v4 171 | with: 172 | branch: gh-pages 173 | folder: docs/_build/html 174 | -------------------------------------------------------------------------------- /asynctnt/iproto/coreproto.pyx: -------------------------------------------------------------------------------- 1 | cimport cpython 2 | cimport cpython.bytearray 3 | cimport cpython.bytes 4 | cimport cpython.dict 5 | from cpython.ref cimport PyObject 6 | from libc.stdint cimport uint32_t, uint64_t 7 | 8 | import base64 9 | import re 10 | import socket 11 | 12 | from asynctnt.exceptions import TarantoolDatabaseError 13 | from asynctnt.log import logger 14 | 15 | VERSION_STRING_REGEX = re.compile(r'\s*Tarantool\s+([\d.]+)\s+.*') 16 | 17 | 18 | cdef class CoreProtocol: 19 | def __init__(self, 20 | host, port, 21 | encoding=None, 22 | initial_read_buffer_size=None): 23 | self.host = host 24 | self.port = port 25 | 26 | encoding = encoding or b'utf-8' 27 | if isinstance(encoding, str): 28 | self.encoding = encoding.encode() 29 | elif isinstance(self.encoding, bytes): 30 | self.encoding = encoding 31 | else: 32 | raise TypeError('encoding must be either str or bytes') 33 | 34 | initial_read_buffer_size = initial_read_buffer_size or 0x20000 35 | self.transport = None 36 | 37 | self.rbuf = ReadBuffer.create(encoding, initial_read_buffer_size) 38 | self.state = PROTOCOL_IDLE 39 | self.con_state = CONNECTION_BAD 40 | 41 | self.version = None 42 | self.salt = None 43 | 44 | cdef bint _is_connected(self): 45 | return self.con_state != CONNECTION_BAD 46 | 47 | cdef bint _is_fully_connected(self): 48 | return self.con_state == CONNECTION_FULL 49 | 50 | def is_connected(self): 51 | return self._is_connected() 52 | 53 | def is_fully_connected(self): 54 | return self._is_fully_connected() 55 | 56 | def get_version(self): 57 | return self.version 58 | 59 | cdef void _write(self, buf) except *: 60 | self.transport.write(memoryview(buf)) 61 | 62 | cdef void _on_data_received(self, data): 63 | cdef: 64 | size_t ruse, curr 65 | const char *p 66 | const char *q 67 | uint32_t packet_len 68 | 69 | char *data_str 70 | ssize_t data_len 71 | ssize_t buf_len 72 | 73 | data_str = NULL 74 | data_len = 0 75 | 76 | if cpython.PyBytes_CheckExact(data): 77 | cpython.bytes.PyBytes_AsStringAndSize( data, 78 | &data_str, 79 | &data_len) 80 | self.rbuf.extend(data_str, data_len) 81 | elif cpython.bytearray.PyByteArray_CheckExact(data): 82 | data_len = cpython.bytearray.PyByteArray_Size( data) 83 | self.rbuf.extend(cpython.bytearray.PyByteArray_AsString( data), data_len) 84 | else: 85 | raise BufferError( 86 | '_on_data_received: expected bytes or bytearray object, got {}'.format( 87 | type(data), 88 | )) 89 | 90 | if data_len == 0: 91 | return 92 | 93 | if self.state == PROTOCOL_GREETING: 94 | if self.rbuf.use < IPROTO_GREETING_SIZE: 95 | # not enough for greeting 96 | return 97 | self._process__greeting() 98 | self.rbuf.move(IPROTO_GREETING_SIZE) 99 | elif self.state == PROTOCOL_NORMAL: 100 | p = self.rbuf.buf 101 | end = &self.rbuf.buf[self.rbuf.use] 102 | 103 | while p < end: 104 | q = p # q is temporary to parse packet length 105 | buf_len = end - p 106 | if buf_len < 5: 107 | # not enough 108 | break 109 | 110 | q = &q[1] # skip to 2nd byte of packet length 111 | packet_len = mp_load_u32(&q) 112 | 113 | if buf_len < 5 + packet_len: 114 | # not enough to read an entire packet 115 | break 116 | 117 | p = &p[5] # skip length header 118 | self._on_response_received(p, packet_len) 119 | p = &p[packet_len] 120 | 121 | if p == end: 122 | self.rbuf.use = 0 123 | break 124 | 125 | self.rbuf.use = end - p 126 | if self.rbuf.use > 0: 127 | self.rbuf.move_offset(p - self.rbuf.buf, self.rbuf.use) 128 | else: 129 | # TODO: raise exception 130 | pass 131 | 132 | cdef void _process__greeting(self): 133 | cdef size_t ver_length = TARANTOOL_VERSION_LENGTH 134 | rbuf = self.rbuf 135 | self.version = self._parse_version(self.rbuf.get_slice_end(ver_length)) 136 | self.salt = base64.b64decode( 137 | self.rbuf.get_slice(ver_length, 138 | ver_length + SALT_LENGTH) 139 | )[:SCRAMBLE_SIZE] 140 | self.state = PROTOCOL_NORMAL 141 | self._on_greeting_received() 142 | 143 | def _parse_version(self, version): 144 | m = VERSION_STRING_REGEX.match(version.decode('ascii')) 145 | if m is not None: 146 | ver = m.group(1) 147 | return tuple(map(int, ver.split('.'))) 148 | 149 | cdef void _on_greeting_received(self): 150 | pass 151 | 152 | cdef void _on_response_received(self, const char *buf, uint32_t buf_len): 153 | pass 154 | 155 | cdef void _on_connection_made(self): 156 | pass 157 | 158 | cdef void _on_connection_lost(self, exc): 159 | pass 160 | 161 | # asyncio callbacks 162 | 163 | def data_received(self, data): 164 | self._on_data_received(data) 165 | 166 | def connection_made(self, transport): 167 | self.transport = transport 168 | self.con_state = CONNECTION_CONNECTED 169 | 170 | sock = transport.get_extra_info('socket') 171 | if sock is not None \ 172 | and (not hasattr(socket, 'AF_UNIX') 173 | or sock.family != socket.AF_UNIX): 174 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 175 | 176 | self.state = PROTOCOL_GREETING 177 | self._on_connection_made() 178 | 179 | def connection_lost(self, exc): 180 | self.con_state = CONNECTION_BAD 181 | self.version = None 182 | self.salt = None 183 | self.rbuf = None 184 | 185 | self._on_connection_lost(exc) 186 | -------------------------------------------------------------------------------- /asynctnt/iproto/protocol.pyi: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any, Dict, Iterator, List, Optional, Tuple, Union 3 | 4 | from asynctnt.iproto.protocol import Adjust 5 | 6 | class Field: 7 | name: Optional[str] 8 | """ Field name """ 9 | 10 | type: Optional[str] 11 | """ Field type """ 12 | 13 | collation: Optional[str] 14 | """ Field collation value """ 15 | 16 | is_nullable: Optional[bool] 17 | """ If field may be null """ 18 | 19 | is_autoincrement: Optional[bool] 20 | """ Is Autoincrement """ 21 | 22 | span: Optional[str] 23 | 24 | class Metadata: 25 | fields: List[Field] 26 | """ List of fields """ 27 | 28 | name_id_map: Dict[str, int] 29 | """ Mapping name -> id """ 30 | 31 | class SchemaIndex: 32 | iid: int 33 | """ Index id """ 34 | 35 | sid: int 36 | """ Space id """ 37 | 38 | name: Optional[str] 39 | index_type: Optional[str] 40 | unique: Optional[bool] 41 | metadata: Optional[Metadata] 42 | 43 | class SchemaSpace: 44 | sid: int 45 | owner: int 46 | name: Optional[str] 47 | engine: Optional[str] 48 | field_count: int 49 | flags: Optional[Any] 50 | metadata: Optional[Metadata] 51 | indexes: Dict[Union[int, str], SchemaIndex] 52 | 53 | class Schema: 54 | id: int 55 | spaces: Dict[Union[str, int], SchemaSpace] 56 | 57 | class TarantoolTuple: 58 | def __repr__(self) -> str: ... 59 | def __index__(self, i: int) -> Any: ... 60 | def __len__(self) -> int: ... 61 | def __contains__(self, item: str) -> bool: ... 62 | def __getitem__(self, item: Union[int, str, slice]) -> Any: ... 63 | def keys(self) -> Iterator[str]: ... 64 | def values(self) -> Iterator[Any]: ... 65 | def items(self) -> Iterator[Tuple[str, Any]]: ... 66 | def get(self, item: str) -> Optional[Any]: ... 67 | def __iter__(self): ... 68 | def __next__(self): ... 69 | 70 | class IProtoErrorStackFrame: 71 | error_type: str 72 | file: str 73 | line: int 74 | message: str 75 | err_no: int 76 | code: int 77 | fields: Dict[str, Any] 78 | 79 | class IProtoError: 80 | trace: List[IProtoErrorStackFrame] 81 | 82 | BodyItem = Union[TarantoolTuple, List[Any], Dict[Any, Any], Any] 83 | 84 | class Response: 85 | errmsg: Optional[str] 86 | error: Optional[IProtoError] 87 | encoding: bytes 88 | autoincrement_ids: Optional[List[int]] 89 | body: Optional[List[BodyItem]] 90 | metadata: Optional[Metadata] 91 | params: Optional[Metadata] 92 | params_count: int 93 | 94 | @property 95 | def sync(self) -> int: ... 96 | @property 97 | def code(self) -> int: ... 98 | @property 99 | def return_code(self) -> int: ... 100 | @property 101 | def schema_id(self) -> int: ... 102 | @property 103 | def stmt_id(self) -> int: ... 104 | @property 105 | def rowcount(self) -> int: ... 106 | def done(self) -> bool: ... 107 | def __len__(self) -> int: ... 108 | def __getitem__(self, i) -> BodyItem: ... 109 | def __iter__(self): ... 110 | 111 | class PushIterator: 112 | def __init__(self, fut: asyncio.Future): ... 113 | def __iter__(self): ... 114 | def __next__(self): ... 115 | def __aiter__(self): ... 116 | async def __anext__(self): ... 117 | @property 118 | def response(self) -> Response: ... 119 | 120 | class Db: 121 | @property 122 | def stream_id(self) -> int: ... 123 | def set_stream_id(self, stream_id: int): ... 124 | def ping(self, timeout: float = -1): ... 125 | def call16( 126 | self, 127 | func_name: str, 128 | args=None, 129 | timeout: float = -1, 130 | push_subscribe: bool = False, 131 | ): ... 132 | def call( 133 | self, 134 | func_name: str, 135 | args=None, 136 | timeout: float = -1, 137 | push_subscribe: bool = False, 138 | ): ... 139 | def eval( 140 | self, 141 | expression: str, 142 | args=None, 143 | timeout: float = -1, 144 | push_subscribe: bool = False, 145 | ): ... 146 | def select( 147 | self, 148 | space, 149 | key=None, 150 | offset: int = 0, 151 | limit: int = 0xFFFFFFFF, 152 | index=0, 153 | iterator=0, 154 | timeout: float = -1, 155 | check_schema_change: bool = True, 156 | ): ... 157 | def insert(self, space, t, replace: bool = False, timeout: float = -1): ... 158 | def replace(self, space, t, timeout: float = -1): ... 159 | def delete(self, space, key, index=0, timeout: float = -1): ... 160 | def update(self, space, key, operations, index=0, timeout: float = -1): ... 161 | def upsert(self, space, t, operations, timeout: float = -1): ... 162 | def execute( 163 | self, query, args, parse_metadata: bool = True, timeout: float = -1 164 | ): ... 165 | def prepare(self, query, parse_metadata: bool = True, timeout: float = -1): ... 166 | def begin(self, isolation: int, tx_timeout: float, timeout: float = -1): ... 167 | def commit(self, timeout: float = -1): ... 168 | def rollback(self, timeout: float = -1): ... 169 | 170 | class Protocol: 171 | @property 172 | def schema_id(self) -> int: ... 173 | @property 174 | def schema(self) -> Schema: ... 175 | @property 176 | def features(self) -> IProtoFeatures: ... 177 | def create_db(self, gen_stream_id: bool = False) -> Db: ... 178 | def get_common_db(self) -> Db: ... 179 | def refetch_schema(self) -> asyncio.Future: ... 180 | def is_connected(self) -> bool: ... 181 | def is_fully_connected(self) -> bool: ... 182 | def get_version(self) -> tuple: ... 183 | 184 | class MPInterval: 185 | year: int 186 | month: int 187 | week: int 188 | day: int 189 | hour: int 190 | min: int 191 | sec: int 192 | nsec: int 193 | adjust: Adjust 194 | 195 | def __init__( 196 | self, 197 | year: int = 0, 198 | month: int = 0, 199 | week: int = 0, 200 | day: int = 0, 201 | hour: int = 0, 202 | min: int = 0, 203 | sec: int = 0, 204 | nsec: int = 0, 205 | adjust: Adjust = Adjust.NONE, 206 | ): ... 207 | def __eq__(self, other) -> bool: ... 208 | 209 | class IProtoFeatures: 210 | streams: bool 211 | transactions: bool 212 | error_extension: bool 213 | watchers: bool 214 | pagination: bool 215 | space_and_index_names: bool 216 | watch_once: bool 217 | dml_tuple_extension: bool 218 | call_ret_tuple_extension: bool 219 | call_arg_tuple_extension: bool 220 | -------------------------------------------------------------------------------- /tests/test_stream.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asynctnt.exceptions import ErrorCode, TarantoolDatabaseError 4 | from tests import BaseTarantoolTestCase 5 | from tests._testbase import ensure_bin_version, ensure_version 6 | 7 | 8 | @ensure_bin_version(min=(2, 10)) 9 | class StreamTestCase(BaseTarantoolTestCase): 10 | EXTRA_BOX_CFG = "memtx_use_mvcc_engine = true" 11 | 12 | @ensure_version(min=(2, 10)) 13 | async def test__transaction_commit(self): 14 | s = self.conn.stream() 15 | self.assertGreater(s.stream_id, 0) 16 | 17 | await s.begin() 18 | data = [1, "hello", 1, 4, "what is up"] 19 | await s.insert(self.TESTER_SPACE_NAME, data) 20 | res = await s.select(self.TESTER_SPACE_NAME) 21 | self.assertResponseEqual(res, [data]) 22 | 23 | await s.commit() 24 | 25 | res = await self.conn.select(self.TESTER_SPACE_NAME) 26 | self.assertResponseEqual(res, [data]) 27 | 28 | @ensure_version(min=(2, 10)) 29 | async def test__transaction_rolled_back(self): 30 | s = self.conn.stream() 31 | await s.begin() 32 | data = [1, "hello", 1, 4, "what is up"] 33 | await s.insert(self.TESTER_SPACE_NAME, data) 34 | res = await s.select(self.TESTER_SPACE_NAME) 35 | self.assertResponseEqual(res, [data]) 36 | 37 | await s.rollback() 38 | 39 | res = await self.conn.select(self.TESTER_SPACE_NAME) 40 | self.assertResponseEqual(res, []) 41 | 42 | @ensure_version(min=(2, 10)) 43 | async def test__transaction_begin_through_call(self): 44 | s = self.conn.stream() 45 | await s.call("box.begin") 46 | data = [1, "hello", 1, 4, "what is up"] 47 | await s.insert(self.TESTER_SPACE_NAME, data) 48 | res = await s.select(self.TESTER_SPACE_NAME) 49 | self.assertResponseEqual(res, [data]) 50 | 51 | await s.commit() 52 | 53 | res = await self.conn.select(self.TESTER_SPACE_NAME) 54 | self.assertResponseEqual(res, [data]) 55 | 56 | @ensure_version(min=(2, 10)) 57 | async def test__transaction_commit_through_call(self): 58 | s = self.conn.stream() 59 | await s.begin() 60 | data = [1, "hello", 1, 4, "what is up"] 61 | await s.insert(self.TESTER_SPACE_NAME, data) 62 | res = await s.select(self.TESTER_SPACE_NAME) 63 | self.assertResponseEqual(res, [data]) 64 | 65 | await s.call("box.commit") 66 | 67 | res = await self.conn.select(self.TESTER_SPACE_NAME) 68 | self.assertResponseEqual(res, [data]) 69 | 70 | @ensure_version(min=(2, 10)) 71 | async def test__transaction_rolled_back_through_call(self): 72 | s = self.conn.stream() 73 | await s.begin() 74 | data = [1, "hello", 1, 4, "what is up"] 75 | await s.insert(self.TESTER_SPACE_NAME, data) 76 | res = await s.select(self.TESTER_SPACE_NAME) 77 | self.assertResponseEqual(res, [data]) 78 | 79 | await s.call("box.rollback") 80 | 81 | res = await self.conn.select(self.TESTER_SPACE_NAME) 82 | self.assertResponseEqual(res, []) 83 | 84 | @ensure_version(min=(2, 10)) 85 | async def test__transaction_commit_through_sql(self): 86 | s = self.conn.stream() 87 | await s.execute("START TRANSACTION") 88 | data = [1, "hello", 1, 4, "what is up"] 89 | await s.insert(self.TESTER_SPACE_NAME, data) 90 | res = await s.select(self.TESTER_SPACE_NAME) 91 | self.assertResponseEqual(res, [data]) 92 | 93 | await s.execute("COMMIT") 94 | 95 | res = await self.conn.select(self.TESTER_SPACE_NAME) 96 | self.assertResponseEqual(res, [data]) 97 | 98 | @ensure_version(min=(2, 10)) 99 | async def test__transaction_rollback_through_sql(self): 100 | s = self.conn.stream() 101 | await s.execute("START TRANSACTION") 102 | data = [1, "hello", 1, 4, "what is up"] 103 | await s.insert(self.TESTER_SPACE_NAME, data) 104 | res = await s.select(self.TESTER_SPACE_NAME) 105 | self.assertResponseEqual(res, [data]) 106 | 107 | await s.execute("ROLLBACK") 108 | 109 | res = await self.conn.select(self.TESTER_SPACE_NAME) 110 | self.assertResponseEqual(res, []) 111 | 112 | @ensure_version(min=(2, 10)) 113 | async def test__transaction_context_manager_commit(self): 114 | data = [1, "hello", 1, 4, "what is up"] 115 | 116 | async with self.conn.stream() as s: 117 | await s.insert(self.TESTER_SPACE_NAME, data) 118 | res = await s.select(self.TESTER_SPACE_NAME) 119 | self.assertResponseEqual(res, [data]) 120 | 121 | res = await self.conn.select(self.TESTER_SPACE_NAME) 122 | self.assertResponseEqual(res, [data]) 123 | 124 | @ensure_version(min=(2, 10)) 125 | async def test__transaction_context_manager_rollback(self): 126 | class ExpectedError(Exception): 127 | pass 128 | 129 | data = [1, "hello", 1, 4, "what is up"] 130 | 131 | try: 132 | async with self.conn.stream() as s: 133 | await s.insert(self.TESTER_SPACE_NAME, data) 134 | res = await s.select(self.TESTER_SPACE_NAME) 135 | self.assertResponseEqual(res, [data]) 136 | 137 | raise ExpectedError("some error") 138 | except ExpectedError: 139 | pass 140 | 141 | res = await self.conn.select(self.TESTER_SPACE_NAME) 142 | self.assertResponseEqual(res, []) 143 | 144 | @ensure_version(min=(2, 10)) 145 | async def test__transaction_2_streams(self): 146 | data1 = [1, "hello", 1, 4, "what is up"] 147 | data2 = [2, "hi", 100, 400, "nothing match"] 148 | 149 | s1 = self.conn.stream() 150 | s2 = self.conn.stream() 151 | 152 | await s1.begin() 153 | await s2.begin() 154 | 155 | await s1.insert(self.TESTER_SPACE_NAME, data1) 156 | await s2.insert(self.TESTER_SPACE_NAME, data2) 157 | 158 | res = await s1.select(self.TESTER_SPACE_NAME, [1]) 159 | self.assertResponseEqual(res, [data1]) 160 | 161 | res = await s2.select(self.TESTER_SPACE_NAME, [2]) 162 | self.assertResponseEqual(res, [data2]) 163 | 164 | await s1.commit() 165 | await s2.commit() 166 | 167 | res = await self.conn.select(self.TESTER_SPACE_NAME) 168 | self.assertResponseEqual(res, [data1, data2]) 169 | 170 | @ensure_version(min=(2, 10)) 171 | async def test__transaction_timeout(self): 172 | s = self.conn.stream() 173 | await s.begin(tx_timeout=0.5) 174 | 175 | await asyncio.sleep(1.0) 176 | 177 | with self.assertRaises(TarantoolDatabaseError) as exc: 178 | await s.commit() 179 | 180 | self.assertEqual(ErrorCode.ER_TRANSACTION_TIMEOUT, exc.exception.code) 181 | self.assertEqual( 182 | "Transaction has been aborted by timeout", exc.exception.message 183 | ) 184 | -------------------------------------------------------------------------------- /asynctnt/iproto/ext/interval.pyx: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from libc.stdint cimport int64_t, uint8_t, uint32_t, uint64_t 4 | 5 | 6 | class Adjust(enum.IntEnum): 7 | """ 8 | Interval adjustment mode for year and month arithmetic. 9 | """ 10 | EXCESS = 0 11 | NONE = 1 12 | LAST = 2 13 | 14 | 15 | cdef class MPInterval: 16 | def __cinit__(self, 17 | int year=0, 18 | int month=0, 19 | int week=0, 20 | int day=0, 21 | int hour=0, 22 | int min=0, 23 | int sec=0, 24 | int nsec=0, 25 | object adjust=Adjust.NONE): 26 | self.year = year 27 | self.month = month 28 | self.week = week 29 | self.day = day 30 | self.hour = hour 31 | self.min = min 32 | self.sec = sec 33 | self.nsec = nsec 34 | self.adjust = adjust 35 | 36 | def __repr__(self): 37 | return (f"asynctnt.Interval(" 38 | f"year={self.year}, " 39 | f"month={self.month}, " 40 | f"week={self.week}, " 41 | f"day={self.day}, " 42 | f"hour={self.hour}, " 43 | f"min={self.min}, " 44 | f"sec={self.sec}, " 45 | f"nsec={self.nsec}, " 46 | f"adjust={self.adjust!r}" 47 | f")") 48 | 49 | def __eq__(self, other): 50 | cdef: 51 | MPInterval other_interval 52 | 53 | if not isinstance(other, MPInterval): 54 | return False 55 | 56 | other_interval = other 57 | 58 | return (self.year == other_interval.year 59 | and self.month == other_interval.month 60 | and self.week == other_interval.week 61 | and self.day == other_interval.day 62 | and self.hour == other_interval.hour 63 | and self.min == other_interval.min 64 | and self.sec == other_interval.sec 65 | and self.nsec == other_interval.nsec 66 | and self.adjust == other_interval.adjust 67 | ) 68 | 69 | cdef uint32_t interval_value_len(int64_t value): 70 | if value == 0: 71 | return 0 72 | 73 | if value > 0: 74 | return 1 + mp_sizeof_uint( value) 75 | 76 | return 1 + mp_sizeof_int(value) 77 | 78 | cdef char *interval_value_pack(char *data, mp_interval_fields field, int64_t value): 79 | if value == 0: 80 | return data 81 | 82 | data = mp_encode_uint(data, field) 83 | 84 | if value > 0: 85 | return mp_encode_uint(data, value) 86 | 87 | return mp_encode_int(data, value) 88 | 89 | cdef uint32_t interval_len(MPInterval interval): 90 | return (1 91 | + interval_value_len(interval.year) 92 | + interval_value_len(interval.month) 93 | + interval_value_len(interval.week) 94 | + interval_value_len(interval.day) 95 | + interval_value_len(interval.hour) 96 | + interval_value_len(interval.min) 97 | + interval_value_len(interval.sec) 98 | + interval_value_len(interval.nsec) 99 | + interval_value_len( interval.adjust.value) 100 | ) 101 | 102 | cdef char *interval_encode(char *data, MPInterval interval) except NULL: 103 | cdef: 104 | uint8_t fields_count 105 | 106 | fields_count = ((interval.year != 0) 107 | + (interval.month != 0) 108 | + (interval.week != 0) 109 | + (interval.day != 0) 110 | + (interval.hour != 0) 111 | + (interval.min != 0) 112 | + (interval.sec != 0) 113 | + (interval.nsec != 0) 114 | + (interval.adjust != 0) 115 | ) 116 | data = mp_store_u8(data, fields_count) 117 | data = interval_value_pack(data, MP_INTERVAL_FIELD_YEAR, interval.year) 118 | data = interval_value_pack(data, MP_INTERVAL_FIELD_MONTH, interval.month) 119 | data = interval_value_pack(data, MP_INTERVAL_FIELD_WEEK, interval.week) 120 | data = interval_value_pack(data, MP_INTERVAL_FIELD_DAY, interval.day) 121 | data = interval_value_pack(data, MP_INTERVAL_FIELD_HOUR, interval.hour) 122 | data = interval_value_pack(data, MP_INTERVAL_FIELD_MINUTE, interval.min) 123 | data = interval_value_pack(data, MP_INTERVAL_FIELD_SECOND, interval.sec) 124 | data = interval_value_pack(data, MP_INTERVAL_FIELD_NANOSECOND, interval.nsec) 125 | data = interval_value_pack(data, MP_INTERVAL_FIELD_ADJUST, interval.adjust.value) 126 | return data 127 | 128 | cdef MPInterval interval_decode(const char ** p, 129 | uint32_t length) except*: 130 | cdef: 131 | char *end 132 | MPInterval interval 133 | uint8_t fields_count 134 | int64_t value 135 | uint8_t field_type 136 | mp_type field_value_type 137 | 138 | end = p[0] + length 139 | fields_count = mp_load_u8(p) 140 | length -= sizeof(uint8_t) 141 | if fields_count > 0 and length < 2: 142 | raise ValueError("Invalid MPInterval length") 143 | 144 | interval = MPInterval.__new__(MPInterval) 145 | 146 | # NONE is default but it will be encoded, 147 | # and because zeros are not encoded then we must set a zero value 148 | interval.adjust = Adjust.EXCESS 149 | 150 | for i in range(fields_count): 151 | field_type = mp_load_u8(p) 152 | value = 0 153 | field_value_type = mp_typeof(p[0][0]) 154 | if field_value_type == MP_UINT: 155 | if mp_check_uint(p[0], end) > 0: 156 | raise ValueError(f"invalid uint. field_type: {field_type}") 157 | 158 | elif field_value_type == MP_INT: 159 | if mp_check_int(p[0], end) > 0: 160 | raise ValueError(f"invalid int. field_type: {field_type}") 161 | 162 | else: 163 | raise ValueError("Invalid MPInterval field value type") 164 | 165 | if mp_read_int64(p, &value) != 0: 166 | raise ValueError("Invalid MPInterval value") 167 | 168 | if field_type == MP_INTERVAL_FIELD_YEAR: 169 | interval.year = value 170 | elif field_type == MP_INTERVAL_FIELD_MONTH: 171 | interval.month = value 172 | elif field_type == MP_INTERVAL_FIELD_WEEK: 173 | interval.week = value 174 | elif field_type == MP_INTERVAL_FIELD_DAY: 175 | interval.day = value 176 | elif field_type == MP_INTERVAL_FIELD_HOUR: 177 | interval.hour = value 178 | elif field_type == MP_INTERVAL_FIELD_MINUTE: 179 | interval.min = value 180 | elif field_type == MP_INTERVAL_FIELD_SECOND: 181 | interval.sec = value 182 | elif field_type == MP_INTERVAL_FIELD_NANOSECOND: 183 | interval.nsec = value 184 | elif field_type == MP_INTERVAL_FIELD_ADJUST: 185 | interval.adjust = Adjust( value) 186 | else: 187 | raise ValueError(f"Invalid MPInterval field type {field_type}") 188 | 189 | return interval 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asynctnt 2 | 3 | [![Build](https://github.com/igorcoding/asynctnt/actions/workflows/actions.yaml/badge.svg?branch=master)](https://github.com/igorcoding/asynctnt/actions) 4 | [![PyPI](https://img.shields.io/pypi/v/asynctnt.svg)](https://pypi.python.org/pypi/asynctnt) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/6cec8adae280cda3e161/maintainability)](https://codeclimate.com/github/igorcoding/asynctnt/maintainability) 6 | 7 | 8 | 9 | 10 | asynctnt is a high-performance [Tarantool](https://tarantool.org/) database 11 | connector library for Python/asyncio. It was highly inspired by 12 | [asyncpg](https://github.com/MagicStack/asyncpg) module. 13 | 14 | asynctnt requires Python 3.7 or later and is supported for Tarantool 15 | versions 1.10+. 16 | 17 | 18 | ## Installation 19 | Use pip to install: 20 | ```bash 21 | $ pip install asynctnt 22 | ``` 23 | 24 | 25 | ## Documentation 26 | 27 | Documentation is available [here](https://igorcoding.github.io/asynctnt). 28 | 29 | 30 | ## Key features 31 | 32 | * Support for all the **basic requests** that Tarantool supports. This includes: 33 | `insert`, `select`, `update`, `upsert`, `call`, `eval`, `execute`. 34 | * Full support for [SQL](https://www.tarantool.io/en/doc/latest/tutorials/sql_tutorial/), 35 | including [prepared statements](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_sql/prepare/). 36 | * Support for [interactive transaction](https://www.tarantool.io/en/doc/latest/book/box/atomic/txn_mode_mvcc/) via Tarantool streams. 37 | * Support of `Decimal`, `UUID`,`datetime` types natively. 38 | * Support for [interval types](https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/interval_object/). 39 | * Support for parsing [custom errors](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_error/new/). 40 | * **Schema fetching** on connection establishment, so you can use spaces and 41 | indexes names rather than their ids, and **auto refetching** if schema in 42 | Tarantool is changed 43 | * **Auto reconnect**. If connection is lost for some reason - asynctnt will 44 | start automatic reconnection procedure (with authorization and schema 45 | fetching, of course). 46 | * Ability to use **dicts for tuples** with field names as keys in DML requests 47 | (select, insert, replace, delete, update, upsert). This is possible only 48 | if space.format is specified in Tarantool. Field names can also be used 49 | in update operations instead of field numbers. Moreover, tuples are decoded 50 | into the special structures that can act either as `tuple`s or by `dict`s with 51 | the appropriate API. 52 | * All requests support specification of `timeout` value, so if request is 53 | executed for too long, asyncio.TimeoutError is raised. 54 | 55 | 56 | ## Basic Usage 57 | 58 | Tarantool config: 59 | 60 | ```lua 61 | box.cfg { 62 | listen = '127.0.0.1:3301' 63 | } 64 | 65 | box.once('v1', function() 66 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 67 | 68 | local s = box.schema.create_space('tester') 69 | s:create_index('primary') 70 | s:format({ 71 | { name = 'id', type = 'unsigned' }, 72 | { name = 'name', type = 'string' }, 73 | { name = 'uuid', type = 'uuid' }, 74 | }) 75 | end) 76 | ``` 77 | 78 | Python code: 79 | 80 | ```python 81 | import uuid 82 | import asyncio 83 | import asynctnt 84 | 85 | 86 | async def main(): 87 | conn = asynctnt.Connection(host='127.0.0.1', port=3301) 88 | await conn.connect() 89 | 90 | for i in range(1, 11): 91 | await conn.insert('tester', [i, 'hello{}'.format(i), uuid.uuid4()]) 92 | 93 | data = await conn.select('tester', []) 94 | tup = data[0] 95 | print('tuple:', tup) 96 | print(f'{tup[0]=}; {tup["id"]=}') 97 | print(f'{tup[1]=}; {tup["name"]=}') 98 | print(f'{tup[2]=}; {tup["uuid"]=}') 99 | 100 | await conn.disconnect() 101 | 102 | 103 | asyncio.run(main()) 104 | ``` 105 | 106 | Stdout: 107 | 108 | *(note that you can simultaneously access fields either by indices 109 | or by their names)* 110 | ``` 111 | tuple: 112 | tup[0]=1; tup["id"]=1 113 | tup[1]='hello1'; tup["name"]='hello1' 114 | tup[2]=UUID('ebbad14c-f78c-42e8-bd12-bfcc564443a6'); tup["uuid"]=UUID('ebbad14c-f78c-42e8-bd12-bfcc564443a6') 115 | ``` 116 | 117 | ## SQL 118 | 119 | Tarantool 2.x brought out an SQL interface to the database. You can easily use it 120 | in `asynctnt` 121 | 122 | ```lua 123 | box.cfg { 124 | listen = '127.0.0.1:3301' 125 | } 126 | 127 | box.once('v1', function() 128 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 129 | 130 | box.execute([[ 131 | create table users ( 132 | id int primary key, 133 | name text 134 | ) 135 | ]]) 136 | end) 137 | ``` 138 | 139 | ```python 140 | import asyncio 141 | import asynctnt 142 | 143 | 144 | async def main(): 145 | conn = asynctnt.Connection(host='127.0.0.1', port=3301) 146 | await conn.connect() 147 | 148 | await conn.execute("insert into users (id, name) values (?, ?)", [1, 'James Bond']) 149 | await conn.execute("insert into users (id, name) values (?, ?)", [2, 'Ethan Hunt']) 150 | data = await conn.execute('select * from users') 151 | 152 | for row in data: 153 | print(row) 154 | 155 | await conn.disconnect() 156 | 157 | asyncio.run(main()) 158 | ``` 159 | 160 | Stdout: 161 | ``` 162 | 163 | 164 | ``` 165 | 166 | More about SQL features in asynctnt please refer to the [documentation](https://igorcoding.github.io/asynctnt/sql.html) 167 | 168 | ## Performance 169 | 170 | Two performance tests were conducted: 171 | 1. `Seq` -- Sequentially calling 40k requests and measuring performance 172 | 2. `Parallel` -- Sending 200k in 300 parallel coroutines 173 | 174 | On all the benchmarks below `wal_mode = none`. 175 | Turning `uvloop` on has a massive effect on the performance, so it is recommended to use `asynctnt` with it 176 | 177 | **Benchmark environment** 178 | * MacBook Pro 2020 179 | * CPU: 2 GHz Quad-Core Intel Core i5 180 | * Memory: 16GB 3733 MHz LPDDR4X 181 | 182 | Tarantool: 183 | ```lua 184 | box.cfg{wal_mode = 'none'} 185 | ``` 186 | 187 | | | Seq (uvloop=off) | Seq (uvloop=on) | Parallel (uvloop=off) | Parallel (uvloop=on) | 188 | |-----------|------------------:|----------------:|----------------------:|---------------------:| 189 | | `ping` | 12940.93 | 19980.82 | 88341.95 | 215756.24 | 190 | | `call` | 11586.38 | 18783.56 | 74651.40 | 137557.25 | 191 | | `eval` | 10631.19 | 17040.57 | 61077.84 | 121542.42 | 192 | | `select` | 9613.88 | 16718.97 | 61584.07 | 152526.21 | 193 | | `insert` | 10077.10 | 16989.06 | 65594.82 | 135491.25 | 194 | | `update` | 10832.16 | 16562.80 | 63003.31 | 121892.28 | 195 | | `execute` | 10431.75 | 16967.85 | 58377.81 | 96891.61 | 196 | 197 | 198 | ## License 199 | asynctnt is developed and distributed under the Apache 2.0 license. 200 | 201 | 202 | ## References 203 | 1. [Tarantool](https://tarantool.org) - in-memory database and application server. 204 | 2. [aiotarantool](https://github.com/shveenkov/aiotarantool) - alternative Python/asyncio connector 205 | 3. [asynctnt-queue](https://github.com/igorcoding/asynctnt-queue) - bindings on top of `asynctnt` for [tarantool-queue](https://github.com/tarantool/queue) 206 | -------------------------------------------------------------------------------- /tests/files/app.lua: -------------------------------------------------------------------------------- 1 | local uuid = require 'uuid' 2 | 3 | local function check_version(expected, version) 4 | -- from tarantool/queue compat.lua 5 | local fun = require 'fun' 6 | local iter, op = fun.iter, fun.operator 7 | 8 | local function split(self, sep) 9 | local sep, fields = sep or ":", {} 10 | local pattern = string.format("([^%s]+)", sep) 11 | self:gsub(pattern, function(c) table.insert(fields, c) end) 12 | return fields 13 | end 14 | 15 | local function reducer(res, l, r) 16 | if res ~= nil then 17 | return res 18 | end 19 | if tonumber(l) == tonumber(r) then 20 | return nil 21 | end 22 | return tonumber(l) > tonumber(r) 23 | end 24 | 25 | local function split_version(version_string) 26 | local vtable = split(version_string, '.') 27 | local vtable2 = split(vtable[3], '-') 28 | vtable[3], vtable[4] = vtable2[1], vtable2[2] 29 | return vtable 30 | end 31 | 32 | local function check_version_internal(expected, version) 33 | version = version or _TARANTOOL 34 | if type(version) == 'string' then 35 | version = split_version(version) 36 | end 37 | local res = iter(version):zip(expected):reduce(reducer, nil) 38 | 39 | if res or res == nil then res = true end 40 | return res 41 | end 42 | 43 | return check_version_internal(expected, version) 44 | end 45 | 46 | 47 | local function bootstrap() 48 | local b = { 49 | tarantool_ver = box.info.version, 50 | has_new_types = false, 51 | types = {} 52 | } 53 | 54 | function b:sql_space_name(space_name) 55 | if self:check_version({3, 0}) then 56 | return space_name 57 | else 58 | return space_name:upper() 59 | end 60 | end 61 | 62 | function b:check_version(expected) 63 | return check_version(expected, self.tarantool_ver) 64 | end 65 | 66 | if b:check_version({1, 7, 1, 245}) then 67 | b.has_new_types = true 68 | b.types.string = 'string' 69 | b.types.unsigned = 'unsigned' 70 | b.types.integer = 'integer' 71 | else 72 | b.types.string = 'str' 73 | b.types.unsigned = 'num' 74 | b.types.integer = 'int' 75 | end 76 | b.types.decimal = 'decimal' 77 | b.types.uuid = 'uuid' 78 | b.types.datetime = 'datetime' 79 | b.types.number = 'number' 80 | b.types.array = 'array' 81 | b.types.scalar = 'scalar' 82 | b.types.any = '*' 83 | return b 84 | end 85 | 86 | _G.B = bootstrap() 87 | 88 | function change_format() 89 | box.space.tester:format({ 90 | {type=B.types.unsigned, name='f1'}, 91 | {type=B.types.string, name='f2'}, 92 | {type=B.types.unsigned, name='f3'}, 93 | {type=B.types.unsigned, name='f4'}, 94 | {type=B.types.any, name='f5'}, 95 | {type=B.types.any, name='f6'}, 96 | }) 97 | end 98 | 99 | box.schema.func.create('change_format', {setuid=true}) 100 | 101 | 102 | box.once('v1', function() 103 | box.schema.user.create('t1', {password = 't1'}) 104 | 105 | if B:check_version({2, 0}) then 106 | box.schema.user.grant('t1', 'read,write,execute,create,drop,alter', 'universe') 107 | box.schema.user.grant('guest', 'read,write,execute,create,drop,alter', 'universe') 108 | else 109 | box.schema.user.grant('t1', 'read,write,execute', 'universe') 110 | end 111 | 112 | local s = box.schema.create_space('tester') 113 | s:format({ 114 | {type=B.types.unsigned, name='f1'}, 115 | {type=B.types.string, name='f2'}, 116 | {type=B.types.unsigned, name='f3'}, 117 | {type=B.types.unsigned, name='f4'}, 118 | {type=B.types.any, name='f5'}, 119 | }) 120 | s:create_index('primary') 121 | s:create_index('txt', {unique = false, parts = {2, B.types.string}}) 122 | 123 | s = box.schema.create_space('no_schema_space') 124 | s:create_index('primary') 125 | s:create_index('primary_hash', 126 | {type = 'hash', parts = {1, B.types.unsigned}}) 127 | end) 128 | 129 | if B:check_version({2, 0}) then 130 | box.once('v2', function() 131 | box.execute([[ 132 | CREATE TABLE sql_space ( 133 | id INT PRIMARY KEY, 134 | name TEXT COLLATE "unicode" 135 | ) 136 | ]]) 137 | box.execute([[ 138 | CREATE TABLE sql_space_autoincrement ( 139 | id INT PRIMARY KEY AUTOINCREMENT, 140 | name TEXT 141 | ) 142 | ]]) 143 | box.execute([[ 144 | CREATE TABLE sql_space_autoincrement_multiple ( 145 | id INT PRIMARY KEY AUTOINCREMENT, 146 | name TEXT 147 | ) 148 | ]]) 149 | end) 150 | 151 | box.once('v2.1', function() 152 | if B:check_version({2, 2}) then 153 | local s = box.schema.create_space('tester_ext_dec') 154 | s:format({ 155 | {type=B.types.unsigned, name='f1'}, 156 | {type=B.types.decimal, name='f2'}, 157 | }) 158 | s:create_index('primary') 159 | end 160 | 161 | if B:check_version({2, 4, 1}) then 162 | s = box.schema.create_space('tester_ext_uuid') 163 | s:format({ 164 | {type=B.types.unsigned, name='f1'}, 165 | {type=B.types.uuid, name='f2'}, 166 | }) 167 | s:create_index('primary') 168 | end 169 | 170 | if B:check_version({2, 10, 0}) then 171 | s = box.schema.create_space('tester_ext_datetime') 172 | s:format({ 173 | {type=B.types.unsigned, name='id'}, 174 | {type=B.types.datetime, name='dt'}, 175 | }) 176 | s:create_index('primary') 177 | end 178 | end) 179 | end 180 | 181 | 182 | function make_third_index(name) 183 | local i = box.space.tester:create_index(name, {unique = true, parts = {3, B.types.unsigned}}) 184 | return {i.id} 185 | end 186 | 187 | 188 | local function _truncate(sp) 189 | if sp == nil then 190 | return 191 | end 192 | 193 | local keys = {} 194 | for _, el in sp:pairs() do 195 | table.insert(keys, el[1]) 196 | end 197 | 198 | for _, k in ipairs(keys) do 199 | sp:delete({k}) 200 | end 201 | end 202 | 203 | 204 | function truncate() 205 | _truncate(box.space.tester) 206 | _truncate(box.space.no_schema_space) 207 | 208 | local sql_spaces = { 209 | 'sql_space', 210 | 'sql_space_autoincrement', 211 | 'sql_space_autoincrement_multiple', 212 | } 213 | for _, sql_space in ipairs(sql_spaces) do 214 | local variants = { 215 | sql_space, 216 | sql_space:upper(), 217 | } 218 | 219 | for _, variant in ipairs(variants) do 220 | if box.space[variant] ~= nil then 221 | box.execute('DELETE FROM ' .. variant) 222 | end 223 | end 224 | end 225 | 226 | _truncate(box.space.tester_ext_dec) 227 | _truncate(box.space.tester_ext_uuid) 228 | _truncate(box.space.tester_ext_datetime) 229 | end 230 | 231 | 232 | _G.fiber = require('fiber') 233 | 234 | 235 | function func_long(t) 236 | fiber.sleep(t) 237 | return 'ok' 238 | end 239 | 240 | 241 | function func_param(p) 242 | return {p} 243 | end 244 | 245 | 246 | function func_param_bare(p) 247 | return p 248 | end 249 | 250 | 251 | function func_hello_bare() 252 | return 'hello' 253 | end 254 | 255 | 256 | function func_hello() 257 | return {'hello'} 258 | end 259 | 260 | function func_load_bin_str() 261 | local bin_data = uuid.bin() 262 | return box.space.tester:insert({ 263 | 100, bin_data, 12, 15, 'hello' 264 | }) 265 | end 266 | 267 | function raise() 268 | box.error{reason='my reason'} 269 | end 270 | 271 | function async_action() 272 | if box.session.push then 273 | for i=1,5 do 274 | box.session.push('hello_' .. tostring(i)) 275 | require'fiber'.sleep(0.01) 276 | end 277 | end 278 | 279 | return 'ret' 280 | end 281 | -------------------------------------------------------------------------------- /tests/test_op_call.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asynctnt import Response 4 | from asynctnt.exceptions import ErrorCode, TarantoolDatabaseError 5 | from tests import BaseTarantoolTestCase 6 | from tests.util import get_complex_param 7 | 8 | 9 | class CallTestCase(BaseTarantoolTestCase): 10 | def has_new_call(self): 11 | return self.conn.version >= (1, 7) 12 | 13 | async def test__call_basic(self): 14 | res = await self.conn.call("func_hello") 15 | 16 | self.assertIsInstance(res, Response, "Got call response") 17 | self.assertEqual(res.code, 0, "success") 18 | self.assertGreater(res.sync, 0, "sync > 0") 19 | self.assertResponseEqual(res, [["hello"]], "Body ok") 20 | 21 | async def test__call_basic_bare(self): 22 | res = await self.conn.call("func_hello_bare") 23 | cmp = ["hello"] 24 | 25 | self.assertIsInstance(res, Response, "Got call response") 26 | self.assertEqual(res.code, 0, "success") 27 | self.assertGreater(res.sync, 0, "sync > 0") 28 | if not self.has_new_call(): 29 | cmp = [cmp] 30 | self.assertResponseEqual(res, cmp, "Body ok") 31 | 32 | async def test__call_unknown_function(self): 33 | with self.assertRaises(TarantoolDatabaseError) as ctx: 34 | await self.conn.call("blablabla") 35 | self.assertEqual(ctx.exception.code, ErrorCode.ER_NO_SUCH_PROC) 36 | 37 | async def test__call_with_param(self): 38 | res = await self.conn.call("func_param", ["myparam"]) 39 | 40 | self.assertIsInstance(res, Response, "Got call response") 41 | self.assertResponseEqual(res, [["myparam"]], "Body ok") 42 | 43 | async def test__call_with_param_bare(self): 44 | res = await self.conn.call("func_param_bare", ["myparam"]) 45 | cmp = ["myparam"] 46 | if not self.has_new_call(): 47 | cmp = [cmp] 48 | 49 | self.assertIsInstance(res, Response, "Got call response") 50 | self.assertResponseEqual(res, cmp, "Body ok") 51 | 52 | async def test__call_func_name_invalid_type(self): 53 | with self.assertRaises(TypeError): 54 | await self.conn.call(12) 55 | 56 | with self.assertRaises(TypeError): 57 | await self.conn.call([1, 2]) 58 | 59 | with self.assertRaises(TypeError): 60 | await self.conn.call({"a": 1}) 61 | 62 | with self.assertRaises(TypeError): 63 | await self.conn.call(b"qwer") 64 | 65 | async def test__call_params_invalid_type(self): 66 | with self.assertRaises(TypeError): 67 | await self.conn.call("func_param", 220349) 68 | 69 | with self.assertRaises(TypeError): 70 | await self.conn.call("func_param", "hey") 71 | 72 | with self.assertRaises(TypeError): 73 | await self.conn.call("func_param", {1: 1, 2: 2}) 74 | 75 | async def test__call_args_tuple(self): 76 | try: 77 | await self.conn.call("func_param", (1, 2)) 78 | except Exception as e: 79 | self.fail(e) 80 | 81 | async def test__call_complex_param(self): 82 | p, cmp = get_complex_param( 83 | encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0) 84 | ) 85 | res = await self.conn.call("func_param", [p]) 86 | self.assertDictEqual(res[0][0], cmp, "Body ok") 87 | 88 | async def test__call_complex_param_bare(self): 89 | p, cmp = get_complex_param( 90 | encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0) 91 | ) 92 | cmp = [cmp] 93 | res = await self.conn.call("func_param_bare", [p]) 94 | if not self.has_new_call(): 95 | cmp = [cmp] 96 | self.assertResponseEqual(res, cmp, "Body ok") 97 | 98 | async def test__call_timeout_in_time(self): 99 | try: 100 | await self.conn.call("func_long", [0.1], timeout=1) 101 | except Exception as e: 102 | self.fail(e) 103 | 104 | async def test__call_timeout_late(self): 105 | with self.assertRaises(asyncio.TimeoutError): 106 | await self.conn.call("func_long", [0.3], timeout=0.1) 107 | 108 | async def test__call_raise(self): 109 | with self.assertRaises(TarantoolDatabaseError) as e: 110 | await self.conn.call("raise") 111 | 112 | self.assertEqual(e.exception.code, 0, "code by box.error{} is 0") 113 | self.assertEqual(e.exception.message, "my reason", "Reason ok") 114 | 115 | 116 | class Call16TestCase(BaseTarantoolTestCase): 117 | async def test__call16_basic(self): 118 | res = await self.conn.call16("func_hello") 119 | 120 | self.assertIsInstance(res, Response, "Got call response") 121 | self.assertEqual(res.code, 0, "success") 122 | self.assertGreater(res.sync, 0, "sync > 0") 123 | self.assertResponseEqual(res, [["hello"]], "Body ok") 124 | 125 | async def test__call16_basic_bare(self): 126 | # Tarantool automatically wraps return result into tuple 127 | 128 | res = await self.conn.call16("func_hello") 129 | 130 | self.assertIsInstance(res, Response, "Got call response") 131 | self.assertEqual(res.code, 0, "success") 132 | self.assertGreater(res.sync, 0, "sync > 0") 133 | self.assertResponseEqual(res, [["hello"]], "Body ok") 134 | 135 | async def test__call16_unknown_function(self): 136 | with self.assertRaises(TarantoolDatabaseError) as ctx: 137 | await self.conn.call16("blablabla") 138 | self.assertEqual(ctx.exception.code, ErrorCode.ER_NO_SUCH_PROC) 139 | 140 | async def test__call16_with_param(self): 141 | res = await self.conn.call16("func_param", ["myparam"]) 142 | 143 | self.assertIsInstance(res, Response, "Got call response") 144 | self.assertResponseEqual(res, [["myparam"]], "Body ok") 145 | 146 | async def test__call16_with_param_bare(self): 147 | # Tarantool automatically wraps return result into tuple 148 | 149 | res = await self.conn.call16("func_param_bare", ["myparam"]) 150 | 151 | self.assertIsInstance(res, Response, "Got call response") 152 | self.assertResponseEqual(res, [["myparam"]], "Body ok") 153 | 154 | async def test__call16_func_name_invalid_type(self): 155 | with self.assertRaises(TypeError): 156 | await self.conn.call16(12) 157 | 158 | with self.assertRaises(TypeError): 159 | await self.conn.call16([1, 2]) 160 | 161 | with self.assertRaises(TypeError): 162 | await self.conn.call16({"a": 1}) 163 | 164 | with self.assertRaises(TypeError): 165 | await self.conn.call16(b"qwer") 166 | 167 | async def test__call16_params_invalid_type(self): 168 | with self.assertRaises(TypeError): 169 | await self.conn.call16("func_param", 220349) 170 | 171 | with self.assertRaises(TypeError): 172 | await self.conn.call16("func_param", "hey") 173 | 174 | with self.assertRaises(TypeError): 175 | await self.conn.call16("func_param", {1: 1, 2: 2}) 176 | 177 | async def test__call16_args_tuple(self): 178 | try: 179 | await self.conn.call16("func_param", (1, 2)) 180 | except Exception as e: 181 | self.fail(e) 182 | 183 | async def test__call16_complex_param(self): 184 | p, cmp = get_complex_param( 185 | encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0) 186 | ) 187 | res = await self.conn.call("func_param", [p]) 188 | self.assertDictEqual(res[0][0], cmp, "Body ok") 189 | 190 | async def test__call16_complex_param_bare(self): 191 | p, cmp = get_complex_param( 192 | encoding=self.conn.encoding, replace_bin=self.conn.version < (3, 0) 193 | ) 194 | res = await self.conn.call16("func_param_bare", [p]) 195 | self.assertDictEqual(res[0][0], cmp, "Body ok") 196 | 197 | async def test__call16_timeout_in_time(self): 198 | try: 199 | await self.conn.call16("func_long", [0.1], timeout=1) 200 | except Exception as e: 201 | self.fail(e) 202 | 203 | async def test__call_timeout_late(self): 204 | with self.assertRaises(asyncio.TimeoutError): 205 | await self.conn.call16("func_long", [0.3], timeout=0.1) 206 | -------------------------------------------------------------------------------- /asynctnt/iproto/requests/update.pyx: -------------------------------------------------------------------------------- 1 | cimport cython 2 | 3 | 4 | @cython.final 5 | cdef class UpdateRequest(BaseRequest): 6 | cdef int encode_body(self, WriteBuffer buffer) except -1: 7 | return encode_request_update(buffer, self.space, self.index, 8 | self.key, self.operations, False) 9 | 10 | cdef char *encode_update_ops(WriteBuffer buffer, 11 | char *p, list operations, 12 | SchemaSpace space) except NULL: 13 | cdef: 14 | char *begin 15 | uint32_t ops_len, op_len 16 | bytes str_temp 17 | char *str_c 18 | ssize_t str_len 19 | 20 | char *op_str_c 21 | ssize_t op_str_len 22 | char op 23 | 24 | uint32_t extra_length 25 | 26 | bint field_encode_as_str 27 | uint64_t field_no 28 | char *field_str_c 29 | ssize_t field_str_len 30 | object field_no_obj 31 | 32 | uint32_t splice_position, splice_offset 33 | 34 | field_encode_as_str = 0 35 | field_str_c = NULL 36 | 37 | begin = NULL 38 | 39 | if operations is not None: 40 | ops_len = cpython.list.PyList_GET_SIZE(operations) 41 | else: 42 | ops_len = 0 43 | 44 | p = buffer.mp_encode_array(p, ops_len) 45 | if ops_len == 0: 46 | return p 47 | 48 | for operation in operations: 49 | if isinstance(operation, tuple): 50 | op_len = cpython.tuple.PyTuple_GET_SIZE(operation) 51 | elif isinstance(operation, list): 52 | op_len = cpython.list.PyList_GET_SIZE(operation) 53 | else: 54 | raise TypeError( 55 | 'Single operation must be a tuple or list') 56 | if op_len < 3: 57 | raise IndexError( 58 | 'Operation length must be at least 3') 59 | 60 | op_type_str = operation[0] 61 | if isinstance(op_type_str, str): 62 | str_temp = encode_unicode_string(op_type_str, buffer._encoding) 63 | elif isinstance(op_type_str, bytes): 64 | str_temp = op_type_str 65 | else: 66 | raise TypeError( 67 | 'Operation type must of a str or bytes type') 68 | 69 | cpython.bytes.PyBytes_AsStringAndSize(str_temp, &op_str_c, 70 | &op_str_len) 71 | 72 | field_no_obj = operation[1] 73 | if isinstance(field_no_obj, int): 74 | field_no = field_no_obj 75 | elif isinstance(field_no_obj, str): 76 | if space.metadata is not None: 77 | field_no = space.metadata.id_by_name_safe(field_no_obj) 78 | if field_no == -1: 79 | field_encode_as_str = 1 80 | else: 81 | field_encode_as_str = 1 82 | 83 | if field_encode_as_str: 84 | str_temp = encode_unicode_string(field_no_obj, buffer._encoding) 85 | cpython.bytes.PyBytes_AsStringAndSize(str_temp, &field_str_c, &field_str_len) 86 | else: 87 | raise TypeError( 88 | 'Operation field_no must be of either int or str type') 89 | 90 | op = 0 91 | if op_str_len == 1: 92 | op = op_str_c[0] 93 | 94 | if op == tarantool.IPROTO_OP_ADD \ 95 | or op == tarantool.IPROTO_OP_SUB \ 96 | or op == tarantool.IPROTO_OP_AND \ 97 | or op == tarantool.IPROTO_OP_XOR \ 98 | or op == tarantool.IPROTO_OP_OR \ 99 | or op == tarantool.IPROTO_OP_DELETE \ 100 | or op == tarantool.IPROTO_OP_INSERT \ 101 | or op == tarantool.IPROTO_OP_ASSIGN: 102 | # mp_sizeof_array(3) 103 | # + mp_sizeof_str(1) 104 | # + mp_sizeof_uint(field_no) 105 | extra_length = 1 + 2 + mp_sizeof_uint(field_no) 106 | p = begin = buffer._ensure_allocated(p, extra_length) 107 | 108 | p = mp_encode_array(p, 3) 109 | p = mp_encode_str(p, op_str_c, 1) 110 | if field_str_c == NULL: 111 | p = mp_encode_uint(p, field_no) 112 | else: 113 | p = mp_encode_str(p, field_str_c, field_str_len) 114 | 115 | buffer._length += (p - begin) 116 | 117 | op_argument = operation[2] 118 | p = buffer.mp_encode_obj(p, op_argument) 119 | 120 | elif op == tarantool.IPROTO_OP_SPLICE: 121 | if op_len < 5: 122 | raise ValueError( 123 | 'Splice operation must have length of 5, ' 124 | 'but got: {}'.format(op_len) 125 | ) 126 | 127 | splice_position_obj = operation[2] 128 | splice_offset_obj = operation[3] 129 | op_argument = operation[4] 130 | if not isinstance(splice_position_obj, int): 131 | raise TypeError('Splice position must be int') 132 | if not isinstance(splice_offset_obj, int): 133 | raise TypeError('Splice offset must be int') 134 | 135 | splice_position = splice_position_obj 136 | splice_offset = splice_offset_obj 137 | 138 | # mp_sizeof_array(5) + mp_sizeof_str(1) + ... 139 | extra_length = 1 + 2 \ 140 | + mp_sizeof_uint(field_no) \ 141 | + mp_sizeof_uint(splice_position) \ 142 | + mp_sizeof_uint(splice_offset) 143 | p = begin = buffer._ensure_allocated(p, extra_length) 144 | 145 | p = mp_encode_array(p, 5) 146 | p = mp_encode_str(p, op_str_c, 1) 147 | if field_str_c == NULL: 148 | p = mp_encode_uint(p, field_no) 149 | else: 150 | p = mp_encode_str(p, field_str_c, field_str_len) 151 | p = mp_encode_uint(p, splice_position) 152 | p = mp_encode_uint(p, splice_offset) 153 | buffer._length += (p - begin) 154 | p = buffer.mp_encode_obj(p, op_argument) 155 | else: 156 | raise TypeError( 157 | 'Unknown update operation type `{}`'.format(op_type_str)) 158 | return p 159 | 160 | cdef int encode_request_update(WriteBuffer buffer, 161 | SchemaSpace space, SchemaIndex index, 162 | key_tuple, list operations, 163 | bint is_upsert) except -1: 164 | cdef: 165 | char *begin 166 | char *p 167 | uint32_t body_map_sz 168 | uint32_t max_body_len 169 | uint32_t space_id, index_id 170 | uint32_t key_of_tuple, key_of_operations 171 | Metadata metadata 172 | bint default_fields_none 173 | 174 | space_id = space.sid 175 | index_id = index.iid 176 | 177 | if not is_upsert: 178 | key_of_tuple = tarantool.IPROTO_KEY 179 | key_of_operations = tarantool.IPROTO_TUPLE 180 | metadata = index.metadata 181 | default_fields_none = False 182 | else: 183 | key_of_tuple = tarantool.IPROTO_TUPLE 184 | key_of_operations = tarantool.IPROTO_OPS 185 | metadata = space.metadata 186 | default_fields_none = True 187 | 188 | body_map_sz = 3 + (index_id > 0) 189 | # Size description: 190 | # mp_sizeof_map(body_map_sz) 191 | # + mp_sizeof_uint(TP_SPACE) 192 | # + mp_sizeof_uint(space) 193 | max_body_len = 1 \ 194 | + 1 \ 195 | + 9 196 | 197 | if index_id > 0: 198 | # + mp_sizeof_uint(TP_INDEX) 199 | # + mp_sizeof_uint(index) 200 | max_body_len += 1 + 9 201 | 202 | max_body_len += 1 # + mp_sizeof_uint(TP_KEY) 203 | max_body_len += 1 # + mp_sizeof_uint(TP_TUPLE) 204 | 205 | buffer.ensure_allocated(max_body_len) 206 | 207 | p = begin = &buffer._buf[buffer._length] 208 | p = mp_encode_map(p, body_map_sz) 209 | p = mp_encode_uint(p, tarantool.IPROTO_SPACE_ID) 210 | p = mp_encode_uint(p, space_id) 211 | 212 | if index_id > 0: 213 | p = mp_encode_uint(p, tarantool.IPROTO_INDEX_ID) 214 | p = mp_encode_uint(p, index_id) 215 | buffer._length += (p - begin) 216 | 217 | p = buffer.mp_encode_uint(p, key_of_tuple) 218 | p = encode_key_sequence(buffer, p, key_tuple, metadata, default_fields_none) 219 | 220 | p = buffer.mp_encode_uint(p, key_of_operations) 221 | p = encode_update_ops(buffer, p, operations, space) 222 | --------------------------------------------------------------------------------