diff --git a/.gitignore b/.gitignore index 0f272e6..0033cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,5 @@ deploy_key* !.ci/deploy_key.enc /core cython_debug + +temp diff --git a/CHANGELOG.md b/CHANGELOG.md index c463f0a..a29ce89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v2.3.0 **New features:** * Added support for [interval types](https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/interval_object/) [#30](https://github.com/igorcoding/asynctnt/issues/30) +* Added ability to retrieve IProto features [available](https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_iproto/feature/) in Tarantool using `conn.features` property ## v2.2.0 diff --git a/asynctnt/connection.py b/asynctnt/connection.py index 12e4571..4fd8e3c 100644 --- a/asynctnt/connection.py +++ b/asynctnt/connection.py @@ -444,7 +444,7 @@ async def reconnect(self): await self.disconnect() await self.connect() - async def __aenter__(self): + async def __aenter__(self) -> "Connection": """ Executed on entering the async with section. Connects to Tarantool instance. @@ -606,7 +606,7 @@ def _normalize_api(self): Api.call = Api.call16 Connection.call = Connection.call16 - if self.version < (2, 10): # pragma: nocover + if not self.features.streams: # pragma: nocover def stream_stub(_): raise TarantoolError("streams are available only in Tarantool 2.10+") @@ -627,6 +627,14 @@ def stream(self) -> Stream: stream._set_db(db) return stream + @property + def features(self) -> protocol.IProtoFeatures: + """ + Lookup available Tarantool features - https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_iproto/feature/ + :return: + """ + return self._protocol.features + async def connect(**kwargs) -> Connection: """ diff --git a/asynctnt/iproto/protocol.pxd b/asynctnt/iproto/protocol.pxd index 4fe93b1..f7e5045 100644 --- a/asynctnt/iproto/protocol.pxd +++ b/asynctnt/iproto/protocol.pxd @@ -73,6 +73,7 @@ cdef class BaseProtocol(CoreProtocol): bint _schema_fetch_in_progress object _refetch_schema_future Db _db + IProtoFeatures _features req_execute_func execute object create_future diff --git a/asynctnt/iproto/protocol.pyi b/asynctnt/iproto/protocol.pyi index 462a589..23c897c 100644 --- a/asynctnt/iproto/protocol.pyi +++ b/asynctnt/iproto/protocol.pyi @@ -172,6 +172,8 @@ class Protocol: def schema_id(self) -> int: ... @property def schema(self) -> Schema: ... + @property + def features(self) -> IProtoFeatures: ... def create_db(self, gen_stream_id: bool = False) -> Db: ... def get_common_db(self) -> Db: ... def refetch_schema(self) -> asyncio.Future: ... @@ -179,6 +181,7 @@ class Protocol: def is_fully_connected(self) -> bool: ... def get_version(self) -> tuple: ... + class MPInterval: year: int month: int @@ -203,3 +206,16 @@ class MPInterval: adjust: Adjust = Adjust.NONE, ): ... def __eq__(self, other) -> bool: ... + + +class IProtoFeatures: + streams: bool + transactions: bool + error_extension: bool + watchers: bool + pagination: bool + space_and_index_names: bool + watch_once: bool + dml_tuple_extension: bool + call_ret_tuple_extension: bool + call_arg_tuple_extension: bool diff --git a/asynctnt/iproto/protocol.pyx b/asynctnt/iproto/protocol.pyx index ea6116a..0e8601c 100644 --- a/asynctnt/iproto/protocol.pyx +++ b/asynctnt/iproto/protocol.pyx @@ -102,6 +102,7 @@ cdef class BaseProtocol(CoreProtocol): self._schema_fetch_in_progress = False self._refetch_schema_future = None self._db = self._create_db( False) + self._features = IProtoFeatures.__new__(IProtoFeatures) self.execute = self._execute_bad try: @@ -257,9 +258,7 @@ cdef class BaseProtocol(CoreProtocol): return e = f.exception() if not e: - logger.debug('Tarantool[%s:%s] identified successfully', - self.host, self.port) - + self._features = ( f.result()).result_ self.post_con_state = POST_CONNECTION_AUTH self._post_con_state_machine() else: @@ -519,6 +518,10 @@ cdef class BaseProtocol(CoreProtocol): def refetch_schema(self): return self._refetch_schema() + @property + def features(self) -> IProtoFeatures: + return self._features + class Protocol(BaseProtocol, asyncio.Protocol): pass diff --git a/asynctnt/iproto/response.pxd b/asynctnt/iproto/response.pxd index afac6ae..ff01067 100644 --- a/asynctnt/iproto/response.pxd +++ b/asynctnt/iproto/response.pxd @@ -27,6 +27,7 @@ cdef class Response: bint _push_subscribe BaseRequest request_ object _exception + object result_ readonly object _q readonly object _push_event @@ -51,3 +52,16 @@ cdef ssize_t response_parse_header(const char *buf, uint32_t buf_len, cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len, Response resp, BaseRequest req, bint is_chunk) except -1 + +cdef class IProtoFeatures: + cdef: + readonly bint streams + readonly bint transactions + readonly bint error_extension + readonly bint watchers + readonly bint pagination + readonly bint space_and_index_names + readonly bint watch_once + readonly bint dml_tuple_extension + readonly bint call_ret_tuple_extension + readonly bint call_arg_tuple_extension diff --git a/asynctnt/iproto/response.pyx b/asynctnt/iproto/response.pyx index 3a13bf3..a915716 100644 --- a/asynctnt/iproto/response.pyx +++ b/asynctnt/iproto/response.pyx @@ -10,6 +10,24 @@ from libc.stdint cimport uint32_t from asynctnt.log import logger +@cython.final +cdef class IProtoFeatures: + def __repr__(self): + return (f"" + ) + + @cython.final @cython.freelist(REQUEST_FREELIST) cdef class Response: @@ -26,6 +44,7 @@ cdef class Response: self.errmsg = None self.error = None self._rowcount = 0 + self.result_ = None self.body = None self.encoding = None self.metadata = None @@ -451,6 +470,7 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len, const char *s list data Field field + IProtoFeatures features b = buf # mp_fprint(stdio.stdout, b) @@ -540,7 +560,33 @@ cdef ssize_t response_parse_body(const char *buf, uint32_t buf_len, logger.debug("IProto version: %s", _decode_obj(&b, resp.encoding)) elif key == tarantool.IPROTO_FEATURES: - logger.debug("IProto features available: %s", _decode_obj(&b, resp.encoding)) + features = IProtoFeatures.__new__(IProtoFeatures) + + for item in _decode_obj(&b, resp.encoding): + if item == 0: + features.streams = 1 + elif item == 1: + features.transactions = 1 + elif item == 2: + features.error_extension = 1 + elif item == 3: + features.watchers = 1 + elif item == 4: + features.pagination = 1 + elif item == 5: + features.space_and_index_names = 1 + elif item == 6: + features.watch_once = 1 + elif item == 7: + features.dml_tuple_extension = 1 + elif item == 8: + features.call_ret_tuple_extension = 1 + elif item == 9: + features.call_arg_tuple_extension = 1 + else: + logger.debug("unknown iproto feature available: %d", item) + + resp.result_ = features elif key == tarantool.IPROTO_AUTH_TYPE: logger.debug("IProto auth type: %s", _decode_obj(&b, resp.encoding)) diff --git a/tests/test_connect.py b/tests/test_connect.py index 6bf62b4..97c21e3 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -802,3 +802,52 @@ async def state_checker(): await conn.call("box.info") finally: await conn.disconnect() + + async def test__features(self): + async with asynctnt.Connection(host=self.tnt.host, port=self.tnt.port) as conn: + if not check_version( + self, + conn.version, + min=(2, 10), + max=(3, 0), + min_included=True, + max_included=False, + ): + return + + self.assertIsNotNone(conn.features) + self.assertTrue(conn.features.streams) + self.assertTrue(conn.features.watchers) + self.assertTrue(conn.features.error_extension) + self.assertTrue(conn.features.transactions) + self.assertTrue(conn.features.pagination) + + self.assertFalse(conn.features.space_and_index_names) + self.assertFalse(conn.features.watch_once) + self.assertFalse(conn.features.dml_tuple_extension) + self.assertFalse(conn.features.call_ret_tuple_extension) + self.assertFalse(conn.features.call_arg_tuple_extension) + + async def test__features_3_0(self): + async with asynctnt.Connection(host=self.tnt.host, port=self.tnt.port) as conn: + if not check_version( + self, + conn.version, + min=(3, 0), + min_included=True, + max_included=False, + ): + return + + self.assertIsNotNone(conn.features) + self.assertTrue(conn.features.streams) + self.assertTrue(conn.features.watchers) + self.assertTrue(conn.features.error_extension) + self.assertTrue(conn.features.transactions) + self.assertTrue(conn.features.pagination) + + self.assertTrue(conn.features.space_and_index_names) + self.assertTrue(conn.features.watch_once) + self.assertTrue(conn.features.dml_tuple_extension) + self.assertTrue(conn.features.call_ret_tuple_extension) + self.assertTrue(conn.features.call_arg_tuple_extension)