From 66d6b9a3008072377b610cbe481b4cd1d36025d7 Mon Sep 17 00:00:00 2001 From: Cole Greer <112986082+Cole-Greer@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:16:53 -0700 Subject: [PATCH] Python: Add `FUNCTION LIST` command. (#1738) * FUNCTION LIST tracer code * update defaults * fix type hints * add tests * add client tests * update client tests with FUNCTION LIST * fix formatting * fix typing * update typing * typing updates * fix formatting * Apply suggestions from code review Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> * update response type to use bytes instead of str * formatting * add routing to function list tests * fix routing tests * update routing tests * fix tests * formatting * rename args, add docstrings, update tests * formatting * updated docstrings and tests * changelog * fix tests * update type definitions * Apply suggestions from code review Co-authored-by: Yury-Fridlyand * update docstrings * Apply suggestions from code review Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> * minor cleanup * black fixes * fix typing * update to use TEncodable * introduce TFunctionListResponse to abstract away complex return type --------- Co-authored-by: Aaron <69273634+aaron-congo@users.noreply.github.com> Co-authored-by: Yury-Fridlyand --- CHANGELOG.md | 1 + .../glide/async_commands/cluster_commands.py | 61 +++- .../async_commands/standalone_commands.py | 48 ++- .../glide/async_commands/transaction.py | 30 ++ python/python/glide/constants.py | 6 + python/python/tests/test_async_client.py | 302 +++++++++++++++++- python/python/tests/test_transaction.py | 33 ++ python/python/tests/utils/utils.py | 46 ++- 8 files changed, 510 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 118242b557..b3fb2ae6e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ * Python: Added RANDOMKEY command ([#1701](https://github.com/aws/glide-for-redis/pull/1701)) * Python: Added FUNCTION FLUSH command ([#1700](https://github.com/aws/glide-for-redis/pull/1700)) * Python: Added FUNCTION DELETE command ([#1714](https://github.com/aws/glide-for-redis/pull/1714)) +* Python: Added FUNCTION LIST command ([#1738](https://github.com/aws/glide-for-redis/pull/1738)) * Python: Added SSCAN command ([#1709](https://github.com/aws/glide-for-redis/pull/1709)) * Python: Added LCS command ([#1716](https://github.com/aws/glide-for-redis/pull/1716)) * Python: Added WAIT command ([#1710](https://github.com/aws/glide-for-redis/pull/1710)) diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/python/glide/async_commands/cluster_commands.py index 0510f94d40..867e4910ce 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/python/glide/async_commands/cluster_commands.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List, Mapping, Optional, Union, cast +from typing import Any, Dict, List, Mapping, Optional, Set, Union, cast from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( @@ -12,7 +12,14 @@ _build_sort_args, ) from glide.async_commands.transaction import BaseTransaction, ClusterTransaction -from glide.constants import TOK, TClusterResponse, TEncodable, TResult, TSingleNodeRoute +from glide.constants import ( + TOK, + TClusterResponse, + TEncodable, + TFunctionListResponse, + TResult, + TSingleNodeRoute, +) from glide.protobuf.redis_request_pb2 import RequestType from glide.routes import Route @@ -361,6 +368,56 @@ async def function_load( ), ) + async def function_list( + self, + library_name_pattern: Optional[TEncodable] = None, + with_code: bool = False, + route: Optional[Route] = None, + ) -> TClusterResponse[TFunctionListResponse]: + """ + Returns information about the functions and libraries. + + See https://valkey.io/commands/function-list/ for more details. + + Args: + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. + route (Optional[Route]): The command will be routed to a random node, unless `route` is provided, + in which case the client will route the command to the nodes defined by `route`. + + Returns: + TClusterResponse[TFunctionListResponse]: Info + about all or selected libraries and their functions. + + Examples: + >>> response = await client.function_list("myLib?_backup", True) + [{ + b"library_name": b"myLib5_backup", + b"engine": b"LUA", + b"functions": [{ + b"name": b"myfunc", + b"description": None, + b"flags": {b"no-writes"}, + }], + b"library_code": b"#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + }] + + Since: Redis 7.0.0. + """ + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return cast( + TClusterResponse[TFunctionListResponse], + await self._execute_command( + RequestType.FunctionList, + args, + route, + ), + ) + async def function_flush( self, mode: Optional[FlushMode] = None, route: Optional[Route] = None ) -> TOK: diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/python/glide/async_commands/standalone_commands.py index a803ffafd5..f7cb65088a 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/python/glide/async_commands/standalone_commands.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List, Mapping, Optional, cast +from typing import Any, Dict, List, Mapping, Optional, Set, Union, cast from glide.async_commands.command_args import Limit, OrderBy from glide.async_commands.core import ( @@ -12,7 +12,7 @@ _build_sort_args, ) from glide.async_commands.transaction import BaseTransaction, Transaction -from glide.constants import OK, TOK, TEncodable, TResult +from glide.constants import OK, TOK, TEncodable, TFunctionListResponse, TResult from glide.protobuf.redis_request_pb2 import RequestType @@ -265,6 +265,50 @@ async def function_load( ), ) + async def function_list( + self, library_name_pattern: Optional[TEncodable] = None, with_code: bool = False + ) -> TFunctionListResponse: + """ + Returns information about the functions and libraries. + + See https://valkey.io/commands/function-list/ for more details. + + Args: + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. + + Returns: + TFunctionListResponse: Info about all or + selected libraries and their functions. + + Examples: + >>> response = await client.function_list("myLib?_backup", True) + [{ + b"library_name": b"myLib5_backup", + b"engine": b"LUA", + b"functions": [{ + b"name": b"myfunc", + b"description": None, + b"flags": {b"no-writes"}, + }], + b"library_code": b"#!lua name=mylib \n redis.register_function('myfunc', function(keys, args) return args[1] end)" + }] + + Since: Redis 7.0.0. + """ + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return cast( + TFunctionListResponse, + await self._execute_command( + RequestType.FunctionList, + args, + ), + ) + async def function_flush(self, mode: Optional[FlushMode] = None) -> TOK: """ Deletes all function libraries. diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 5017611af7..88daf11945 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -1885,6 +1885,36 @@ def function_load( ["REPLACE", library_code] if replace else [library_code], ) + def function_list( + self: TTransaction, + library_name_pattern: Optional[TEncodable] = None, + with_code: bool = False, + ) -> TTransaction: + """ + Returns information about the functions and libraries. + + See https://valkey.io/commands/function-list/ for more details. + + Args: + library_name_pattern (Optional[TEncodable]): A wildcard pattern for matching library names. + with_code (bool): Specifies whether to request the library code from the server or not. + + Commands response: + TFunctionListResponse: Info about all or + selected libraries and their functions. + + Since: Redis 7.0.0. + """ + args = [] + if library_name_pattern is not None: + args.extend(["LIBRARYNAME", library_name_pattern]) + if with_code: + args.append("WITHCODE") + return self.append_command( + RequestType.FunctionList, + args, + ) + def function_flush( self: TTransaction, mode: Optional[FlushMode] = None ) -> TTransaction: diff --git a/python/python/glide/constants.py b/python/python/glide/constants.py index c86756d5d6..b9f5615a5f 100644 --- a/python/python/glide/constants.py +++ b/python/python/glide/constants.py @@ -36,3 +36,9 @@ # For more information, see: https://redis.io/docs/data-types/json/path/ . TJsonResponse = Union[T, List[Optional[T]]] TEncodable = Union[str, bytes] +TFunctionListResponse = List[ + Mapping[ + bytes, + Union[bytes, List[Mapping[bytes, Union[bytes, Set[bytes]]]]], + ] +] diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 7c9632ed93..7bde22635f 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -89,6 +89,7 @@ ) from tests.conftest import create_client from tests.utils.utils import ( + check_function_list_response, check_if_server_version_lt, compare_maps, convert_bytes_to_string_object, @@ -7114,7 +7115,6 @@ async def test_object_refcount(self, redis_client: TGlideClient): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_function_load(self, redis_client: TGlideClient): - # TODO: Test with FUNCTION LIST min_version = "7.0.0" if await check_if_server_version_lt(redis_client, min_version): return pytest.mark.skip(reason=f"Redis version required >= {min_version}") @@ -7123,6 +7123,9 @@ async def test_function_load(self, redis_client: TGlideClient): func_name = f"myfunc1c{get_random_string(5)}" code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + # verify function does not yet exist + assert await redis_client.function_list(lib_name) == [] + assert await redis_client.function_load(code) == lib_name.encode() assert await redis_client.fcall(func_name, arguments=["one", "two"]) == b"one" @@ -7130,7 +7133,14 @@ async def test_function_load(self, redis_client: TGlideClient): await redis_client.fcall_ro(func_name, arguments=["one", "two"]) == b"one" ) - # TODO: add FUNCTION LIST once implemented + # verify with FUNCTION LIST + check_function_list_response( + await redis_client.function_list(lib_name, with_code=True), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) # re-load library without replace with pytest.raises(RequestError) as e: @@ -7159,7 +7169,6 @@ async def test_function_load(self, redis_client: TGlideClient): async def test_function_load_cluster_with_route( self, redis_client: GlideClusterClient, single_route: bool ): - # TODO: Test with FUNCTION LIST min_version = "7.0.0" if await check_if_server_version_lt(redis_client, min_version): return pytest.mark.skip(reason=f"Redis version required >= {min_version}") @@ -7169,6 +7178,15 @@ async def test_function_load_cluster_with_route( code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + # verify function does not yet exist + function_list = await redis_client.function_list(lib_name, False, route) + if single_route: + assert function_list == [] + else: + assert isinstance(function_list, dict) + for functions in function_list.values(): + assert functions == [] + assert await redis_client.function_load(code, False, route) == lib_name.encode() result = await redis_client.fcall_route( @@ -7193,7 +7211,28 @@ async def test_function_load_cluster_with_route( for nodeResponse in result.values(): assert nodeResponse == b"one" - # TODO: add FUNCTION LIST once implemented + # verify with FUNCTION LIST + function_list = await redis_client.function_list( + lib_name, with_code=True, route=route + ) + if single_route: + check_function_list_response( + function_list, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + else: + assert isinstance(function_list, dict) + for nodeResponse in function_list.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) # re-load library without replace with pytest.raises(RequestError) as e: @@ -7237,6 +7276,201 @@ async def test_function_load_cluster_with_route( assert await redis_client.function_flush(FlushMode.SYNC, route) is OK + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_list(self, redis_client: TGlideClient): + min_version = "7.0.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + original_functions_count = len(await redis_client.function_list()) + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Assert function `lib_name` does not yet exist + assert await redis_client.function_list(lib_name) == [] + + # load library + await redis_client.function_load(code) + + check_function_list_response( + await redis_client.function_list(lib_name), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + await redis_client.function_list(f"{lib_name}*"), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + await redis_client.function_list(lib_name, with_code=True), + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + no_args_response = await redis_client.function_list() + wildcard_pattern_response = await redis_client.function_list("*", False) + assert len(no_args_response) == original_functions_count + 1 + assert len(wildcard_pattern_response) == original_functions_count + 1 + check_function_list_response( + no_args_response, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + check_function_list_response( + wildcard_pattern_response, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + @pytest.mark.parametrize("single_route", [True, False]) + async def test_function_list_with_routing( + self, redis_client: GlideClusterClient, single_route: bool + ): + min_version = "7.0.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + route = SlotKeyRoute(SlotType.PRIMARY, "1") if single_route else AllPrimaries() + + lib_name = f"mylib1C{get_random_string(5)}" + func_name = f"myfunc1c{get_random_string(5)}" + code = generate_lua_lib_code(lib_name, {func_name: "return args[1]"}, True) + + # Assert function `lib_name` does not yet exist + result = await redis_client.function_list(lib_name, route=route) + if single_route: + assert result == [] + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert nodeResponse == [] + + # load library + await redis_client.function_load(code, route=route) + + result = await redis_client.function_list(lib_name, route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + result = await redis_client.function_list(f"{lib_name}*", route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + None, + ) + + result = await redis_client.function_list(lib_name, with_code=True, route=route) + if single_route: + check_function_list_response( + result, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + check_function_list_response( + nodeResponse, + lib_name, + {func_name: None}, + {func_name: {b"no-writes"}}, + code, + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_function_list_with_multiple_functions( + self, redis_client: TGlideClient + ): + min_version = "7.0.0" + if await check_if_server_version_lt(redis_client, min_version): + return pytest.mark.skip(reason=f"Redis version required >= {min_version}") + + await redis_client.function_flush() + assert len(await redis_client.function_list()) == 0 + + lib_name_1 = f"mylib1C{get_random_string(5)}" + func_name_1 = f"myfunc1c{get_random_string(5)}" + func_name_2 = f"myfunc2c{get_random_string(5)}" + code_1 = generate_lua_lib_code( + lib_name_1, + {func_name_1: "return args[1]", func_name_2: "return args[2]"}, + False, + ) + await redis_client.function_load(code_1) + + lib_name_2 = f"mylib2C{get_random_string(5)}" + func_name_3 = f"myfunc3c{get_random_string(5)}" + code_2 = generate_lua_lib_code( + lib_name_2, {func_name_3: "return args[3]"}, True + ) + await redis_client.function_load(code_2) + + no_args_response = await redis_client.function_list() + + assert len(no_args_response) == 2 + check_function_list_response( + no_args_response, + lib_name_1, + {func_name_1: None, func_name_2: None}, + {func_name_1: set(), func_name_2: set()}, + None, + ) + check_function_list_response( + no_args_response, + lib_name_2, + {func_name_3: None}, + {func_name_3: {b"no-writes"}}, + None, + ) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_function_flush(self, redis_client: TGlideClient): @@ -7251,17 +7485,22 @@ async def test_function_flush(self, redis_client: TGlideClient): # Load the function assert await redis_client.function_load(code) == lib_name.encode() - # TODO: Ensure the function exists with FUNCTION LIST + # verify function exists + assert len(await redis_client.function_list(lib_name)) == 1 # Flush functions assert await redis_client.function_flush(FlushMode.SYNC) == OK assert await redis_client.function_flush(FlushMode.ASYNC) == OK - # TODO: Ensure the function is no longer present with FUNCTION LIST + # verify function is removed + assert len(await redis_client.function_list(lib_name)) == 0 # Attempt to re-load library without overwriting to ensure FLUSH was effective assert await redis_client.function_load(code) == lib_name.encode() + # verify function exists + assert len(await redis_client.function_list(lib_name)) == 1 + # Clean up by flushing functions again await redis_client.function_flush() @@ -7283,17 +7522,40 @@ async def test_function_flush_with_routing( # Load the function assert await redis_client.function_load(code, False, route) == lib_name.encode() - # TODO: Ensure the function exists with FUNCTION LIST + # verify function exists + result = await redis_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 # Flush functions assert await redis_client.function_flush(FlushMode.SYNC, route) == OK assert await redis_client.function_flush(FlushMode.ASYNC, route) == OK - # TODO: Ensure the function is no longer present with FUNCTION LIST + # verify function is removed + result = await redis_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 0 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 0 # Attempt to re-load library without overwriting to ensure FLUSH was effective assert await redis_client.function_load(code, False, route) == lib_name.encode() + # verify function exists + result = await redis_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 + # Clean up by flushing functions again assert await redis_client.function_flush(route=route) == OK @@ -7311,12 +7573,14 @@ async def test_function_delete(self, redis_client: TGlideClient): # Load the function assert await redis_client.function_load(code) == lib_name.encode() - # TODO: Ensure the library exists with FUNCTION LIST + # verify function exists + assert len(await redis_client.function_list(lib_name)) == 1 # Delete the function assert await redis_client.function_delete(lib_name) == OK - # TODO: Ensure the function is no longer present with FUNCTION LIST + # verify function is removed + assert len(await redis_client.function_list(lib_name)) == 0 # deleting a non-existing library with pytest.raises(RequestError) as e: @@ -7341,12 +7605,26 @@ async def test_function_delete_with_routing( # Load the function assert await redis_client.function_load(code, False, route) == lib_name.encode() - # TODO: Ensure the library exists with FUNCTION LIST + # verify function exists + result = await redis_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 1 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 1 # Delete the function assert await redis_client.function_delete(lib_name, route) == OK - # TODO: Ensure the function is no longer present with FUNCTION LIST + # verify function is removed + result = await redis_client.function_list(lib_name, False, route) + if single_route: + assert len(result) == 0 + else: + assert isinstance(result, dict) + for nodeResponse in result.values(): + assert len(nodeResponse) == 0 # deleting a non-existing library with pytest.raises(RequestError) as e: diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index a557dd1788..221d7e29c0 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -109,6 +109,39 @@ async def transaction_test( args.append(lib_name.encode()) transaction.function_load(code, True) args.append(lib_name.encode()) + transaction.function_list(lib_name) + args.append( + [ + { + b"library_name": lib_name.encode(), + b"engine": b"LUA", + b"functions": [ + { + b"name": func_name.encode(), + b"description": None, + b"flags": {b"no-writes"}, + } + ], + } + ] + ) + transaction.function_list(lib_name, True) + args.append( + [ + { + b"library_name": lib_name.encode(), + b"engine": b"LUA", + b"functions": [ + { + b"name": func_name.encode(), + b"description": None, + b"flags": {b"no-writes"}, + } + ], + b"library_code": code.encode(), + } + ] + ) transaction.fcall(func_name, [], arguments=["one", "two"]) args.append(b"one") transaction.fcall(func_name, [key], arguments=["one", "two"]) diff --git a/python/python/tests/utils/utils.py b/python/python/tests/utils/utils.py index 9a684a09e3..cd1ac3a8c7 100644 --- a/python/python/tests/utils/utils.py +++ b/python/python/tests/utils/utils.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List, Mapping, Optional, Set, TypeVar, Union, cast from glide.async_commands.core import InfoSection -from glide.constants import TResult +from glide.constants import TClusterResponse, TFunctionListResponse, TResult from glide.glide_client import TGlideClient from packaging import version @@ -225,3 +225,47 @@ def generate_lua_lib_code( code += ", flags = { 'no-writes' }" code += " }\n" return code + + +def check_function_list_response( + response: TClusterResponse[TFunctionListResponse], + lib_name: str, + function_descriptions: Mapping[str, Optional[bytes]], + function_flags: Mapping[str, Set[bytes]], + lib_code: Optional[str] = None, +): + """ + Validate whether `FUNCTION LIST` response contains required info. + + Args: + response (List[Mapping[bytes, Any]]): The response from redis. + libName (bytes): Expected library name. + functionDescriptions (Mapping[bytes, Optional[bytes]]): Expected function descriptions. Key - function name, value - + description. + functionFlags (Mapping[bytes, Set[bytes]]): Expected function flags. Key - function name, value - flags set. + libCode (Optional[bytes]): Expected library to check if given. + """ + response = cast(TFunctionListResponse, response) + assert len(response) > 0 + has_lib = False + for lib in response: + has_lib = lib.get(b"library_name") == lib_name.encode() + if has_lib: + functions: List[Mapping[bytes, Any]] = cast( + List[Mapping[bytes, Any]], lib.get(b"functions") + ) + assert len(functions) == len(function_descriptions) + for function in functions: + function_name: bytes = cast(bytes, function.get(b"name")) + assert function.get(b"description") == function_descriptions.get( + function_name.decode("utf-8") + ) + assert function.get(b"flags") == function_flags.get( + function_name.decode("utf-8") + ) + + if lib_code: + assert lib.get(b"library_code") == lib_code.encode() + break + + assert has_lib is True