From eb7f9603e74794261bf6fa410662eeba2f4c2eda Mon Sep 17 00:00:00 2001 From: Ajay Jashnani Date: Wed, 2 Aug 2017 16:43:33 -0500 Subject: [PATCH] feat(API): Adding partial database API support --- MANIFEST.in | 1 + docs/api_reference.rst | 1 + docs/api_reference/databases.rst | 7 + docs/api_reference/system.rst | 13 ++ docs/examples.rst | 3 +- docs/examples/programmatic_databases.rst | 8 + nixnet/_funcs.py | 42 +++++ nixnet/nxdb.py | 22 --- nixnet/system/databases.py | 145 ++++++++++++++++++ nixnet/system/system.py | 7 + nixnet_examples/databases/custom_database.dbc | 116 ++++++++++++++ .../programmatic_database_usage.py | 68 ++++++++ tests/test_examples.py | 18 +++ tests/test_system.py | 27 ++++ 14 files changed, 455 insertions(+), 23 deletions(-) create mode 100644 docs/api_reference/databases.rst create mode 100644 docs/api_reference/system.rst create mode 100644 docs/examples/programmatic_databases.rst create mode 100644 nixnet/system/databases.py create mode 100644 nixnet_examples/databases/custom_database.dbc create mode 100644 nixnet_examples/programmatic_database_usage.py diff --git a/MANIFEST.in b/MANIFEST.in index d9b0d482..285c5644 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,4 +3,5 @@ include LICENSE include nixnet/VERSION recursive-include nixnet *.py recursive-include nixnet_examples *.py +include nixnet_examples/databases/custom_database.dbc recursive-include tests *.py diff --git a/docs/api_reference.rst b/docs/api_reference.rst index c578d9c1..325e1a00 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -9,6 +9,7 @@ API Reference :caption: Table of Contents: api_reference/session + api_reference/system api_reference/convert api_reference/constants api_reference/types diff --git a/docs/api_reference/databases.rst b/docs/api_reference/databases.rst new file mode 100644 index 00000000..99e912c2 --- /dev/null +++ b/docs/api_reference/databases.rst @@ -0,0 +1,7 @@ +nixnet.session.databases +======================== + +.. automodule:: nixnet.system.databases + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/api_reference/system.rst b/docs/api_reference/system.rst new file mode 100644 index 00000000..6a8e0e0b --- /dev/null +++ b/docs/api_reference/system.rst @@ -0,0 +1,13 @@ +nixnet.system +============= + +.. automodule:: nixnet.system + :members: + :show-inheritance: + :inherited-members: + +.. toctree:: + :maxdepth: 3 + :caption: API Reference: + + databases diff --git a/docs/examples.rst b/docs/examples.rst index be6b8bfd..1db3145b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -12,4 +12,5 @@ Examples examples/stream_io examples/single_point_io examples/conversion - examples/can_lin_diff \ No newline at end of file + examples/can_lin_diff + examples/programmatic_databases diff --git a/docs/examples/programmatic_databases.rst b/docs/examples/programmatic_databases.rst new file mode 100644 index 00000000..d31d5cec --- /dev/null +++ b/docs/examples/programmatic_databases.rst @@ -0,0 +1,8 @@ +Programmatic Database Usage +=========================== + +This example uses :any:`nixnet.system.databases.Databases` to demonstrate how +databases can be programmatically added and used in a system. + +.. literalinclude:: ../../nixnet_examples/programmatic_database_usage.py + :pyobject: main diff --git a/nixnet/_funcs.py b/nixnet/_funcs.py index a5cd192e..821a720e 100644 --- a/nixnet/_funcs.py +++ b/nixnet/_funcs.py @@ -712,3 +712,45 @@ def nxdb_undeploy( database_alias_ctypes, ) _errors.check_for_error(result.value) + + +def nxdb_get_database_list( + ip_address, # type: typing.Text + size_of_alias_buffer, # type: int + size_of_filepath_buffer, # type: int +): + # type: (...) -> typing.Tuple[typing.List[typing.Text], typing.List[typing.Text], int] + ip_address_ctypes = _ctypedefs.char_p(ip_address.encode('ascii')) + size_of_alias_buffer_ctypes = _ctypedefs.u32(size_of_alias_buffer) + size_of_filepath_buffer_ctypes = _ctypedefs.u32(size_of_filepath_buffer) + alias_buffer_ctypes = ctypes.create_string_buffer(size_of_alias_buffer) + filepath_buffer_ctypes = ctypes.create_string_buffer(size_of_filepath_buffer) + number_of_databases_ctypes = _ctypedefs.u32() + result = _cfuncs.lib.nxdb_get_database_list( + ip_address_ctypes, + size_of_alias_buffer_ctypes, + alias_buffer_ctypes, + size_of_filepath_buffer_ctypes, + filepath_buffer_ctypes, + number_of_databases_ctypes, + ) + _errors.check_for_error(result.value) + alias_buffer = alias_buffer_ctypes.value.decode("ascii") + filepath_buffer = filepath_buffer_ctypes.value.decode("ascii") + return alias_buffer, filepath_buffer, number_of_databases_ctypes.value + + +def nxdb_get_database_list_sizes( + ip_address, # type: typing.Text +): + # type: (...) -> typing.Tuple[int, int] + ip_address_ctypes = _ctypedefs.char_p(ip_address.encode('ascii')) + size_of_alias_buffer_ctypes = _ctypedefs.u32() + size_of_filepath_buffer_ctypes = _ctypedefs.u32() + result = _cfuncs.lib.nxdb_get_database_list_sizes( + ip_address_ctypes, + size_of_alias_buffer_ctypes, + size_of_filepath_buffer_ctypes, + ) + _errors.check_for_error(result.value) + return size_of_alias_buffer_ctypes.value, size_of_filepath_buffer_ctypes.value diff --git a/nixnet/nxdb.py b/nixnet/nxdb.py index 6e2664f3..6615038a 100644 --- a/nixnet/nxdb.py +++ b/nixnet/nxdb.py @@ -23,18 +23,6 @@ def delete_object(db_object_ref): _funcs.nxdb_delete_object(db_object_ref) -def add_alias64( - database_alias, - database_filepath, - default_baud_rate): - _funcs.nxdb_add_alias64(database_alias, database_filepath, default_baud_rate) - - -def remove_alias( - database_alias): - _funcs.nxdb_remove_alias(database_alias) - - def deploy( ip_address, database_alias, @@ -46,13 +34,3 @@ def undeploy( ip_address, database_alias): _funcs.nxdb_undeploy(ip_address, database_alias) - - -def get_database_list( - ip_address, - size_of_alias_buffer, - alias_buffer, - size_of_filepath_buffer, - filepath_buffer, - number_of_databases): - raise NotImplementedError("Placeholder") diff --git a/nixnet/system/databases.py b/nixnet/system/databases.py new file mode 100644 index 00000000..785fa512 --- /dev/null +++ b/nixnet/system/databases.py @@ -0,0 +1,145 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import typing # NOQA: F401 + +import six + +from nixnet import _funcs + + +class Databases(collections.Mapping): + """Database aliases.""" + + def __init__(self, handle): + self._handle = handle + + def __repr__(self): + return 'System.Databases(handle={0})'.format(self._handle) + + def __get_database_list(self, ip_address): + alias_buffer_size, filepath_buffer_size = _funcs.nxdb_get_database_list_sizes(ip_address) + aliases, filepaths, _ = _funcs.nxdb_get_database_list(ip_address, alias_buffer_size, filepath_buffer_size) + return list(zip(aliases.split(","), filepaths.split(","))) + + def __len__(self): + return len(self.__get_database_list('')) + + def __iter__(self): + return self.keys() + + def __getitem__(self, index): + # type: (str) -> Database + """Return the Database object associated with the specified alias. + + Args: + index(str): The value of the index (alias name). + """ + if isinstance(index, six.string_types): + for alias, filepath in self.__get_database_list(''): + if alias == index: + return self._create_item(alias, filepath) + else: + raise KeyError('Database alias %s not found in the system' % index) + else: + raise TypeError(index) + + def __delitem__(self, index): + # type: (str) -> None + """Delete/Remove a database alias from the system. + + This function removes the alias from NI-XNET, but does not affect the + database text file. It just removes the alias association to the + database file path. + + This function is supported on Windows only, and the alias is removed + from Windows only (not RT targets). Use 'undeploy' to remove an alias + from a Real-Time (RT) target. + + Args: + index(str): The name of the alias to delete. + """ + _funcs.nxdb_remove_alias(index) + + def _create_item(self, database_alias, database_filepath): + return Database(database_alias, database_filepath) + + def keys(self): + """Return all keys (alias names) used in the Databases object. + + Yields: + An iterator to all the keys in the Database object. + """ + for alias, _ in self.__get_database_list(''): + yield alias + + def values(self): + """Return all Database objects in the system. + + Yields: + An iterator to all the values in the Databases object. + """ + for alias, filepath in self.__get_database_list(''): + yield self._create_item(alias, filepath) + + def items(self): + """Return all aliases and database objects associated with those aliases in the system. + + Yields: + An iterator to tuple pairs of alias and database objects in the system. + """ + for alias, filepath in self.__get_database_list(''): + yield alias, self._create_item(alias, filepath) + + def add_alias(self, database_alias, database_filepath, default_baud_rate): + # type: (str, str, int) -> None + """Add a new alias with baud rate size of up to 64 bits to a database file. + + NI-XNET uses alias names for database files. The alias names provide a + shorter name for display, allow for changes to the file system without + changing the application. + + This function is supported on Windows only. + + Args: + database_alias(str): Provides the desired alias name. Unlike the names of + other XNET database objects, the alias name can use special + characters such as space and dash. Commas are not allowed in the + alias name. If the alias name already exists, this function + changes the previous filepath to the specified filepath. + database_filepath(str): Provides the path to the CANdb, FIBEX, or LDF + file. Commas are not allowed in the filepath name. + default_baud_rate(int): Provides the default baud rate, used when + filepath refers to a CANdb database (.dbc) or an NI-CAN database + (.ncd). These database formats are specific to CAN and do not + specify a cluster baud rate. Use this default baud rate to + specify a default CAN baud rate to use with this alias. If + database_filepath refers to a FIBEX database (.xml) or LIN LDF + file, the default_baud_rate parameter is ignored. The FIBEX and + LDF database formats require a valid baud rate for every + cluster, and NI-XNET uses that baud rate as the default. + """ + _funcs.nxdb_add_alias64(database_alias, database_filepath, default_baud_rate) + + +class Database(object): + """Database alias.""" + + def __init__( + self, + database_alias, + database_filepath, + ): + self._database_alias = database_alias + self._database_filepath = database_filepath + + def __repr__(self): + return 'System.Database(alias={}, filepath={})'.format(self._database_alias, self._database_filepath) + + @property + def filepath(self): + # type: () -> str + """str: Get the filepath associated with the Database object""" + return self._database_filepath diff --git a/nixnet/system/system.py b/nixnet/system/system.py index 3441debc..168c08a4 100644 --- a/nixnet/system/system.py +++ b/nixnet/system/system.py @@ -13,6 +13,7 @@ from nixnet.system import _device from nixnet.system import _interface +from nixnet.system import databases class System(object): @@ -21,6 +22,7 @@ def __init__(self): # type: () -> None self._handle = None # To satisfy `__del__` in case nx_system_open throws self._handle = _funcs.nx_system_open() + self._databases = databases.Databases(self._handle) def __del__(self): if self._handle is not None: @@ -67,6 +69,11 @@ def close(self): self._handle = None + @property + def databases(self): + """:any:`nixnet.system.databases.Databases`: Operate on systems's database's aliases""" + return self._databases + @property def dev_refs(self): # type: () -> typing.Iterable[_device.Device] diff --git a/nixnet_examples/databases/custom_database.dbc b/nixnet_examples/databases/custom_database.dbc new file mode 100644 index 00000000..68bd9b98 --- /dev/null +++ b/nixnet_examples/databases/custom_database.dbc @@ -0,0 +1,116 @@ +VERSION "" + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_ : + +BU_ : + +BO_ 64 CANCyclicFrame1: 8 Vector__XXX + SG_ CANCyclicSignal1 : 0|32@1- (1,0) [0|10000] "" Vector__XXX + SG_ CANCyclicSignal2 : 32|32@1- (1,0) [0|10000] "" Vector__XXX + +BO_ 65 CANCyclicFrame2: 2 Vector__XXX + SG_ CANCyclicSignal3 : 0|8@1- (1,0) [0|255] "" Vector__XXX + SG_ CANCyclicSignal4 : 8|8@1- (1,0) [0|255] "" Vector__XXX + +BO_ 66 CANEventFrame1: 8 Vector__XXX + SG_ CANEventSignal1 : 0|32@1- (1,0) [0|10000] "" Vector__XXX + SG_ CANEventSignal2 : 32|32@1- (1,0) [0|10000] "" Vector__XXX + +BO_ 67 CANEventFrame2: 2 Vector__XXX + SG_ CANEventSignal3 : 0|8@1- (1,0) [0|255] "" Vector__XXX + SG_ CANEventSignal4 : 8|8@1- (1,0) [0|255] "" Vector__XXX + +BO_ 133 InstrumentPanel: 8 Vector__XXX + SG_ ABSWarning : 49|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ AirBagIndicator : 60|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ BrakeSystemWarning : 48|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ ChargingSystemWarning : 51|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ CruiseActiveIndicator : 59|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ FuelGauge : 32|8@1+ (0.40000000000000002,0) [0|100] "%" Vector__XXX + SG_ HighBeamIndicator : 58|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ LeftTurnSignalIndicator : 56|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ OilPressureWarning : 50|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ RightTurnSignalIndicator : 57|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ SeatBeltWarning : 52|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ ServiceEngineWarning : 53|1@1+ (1,0) [0|1] "" Vector__XXX + SG_ Speedometer : 0|16@1+ (0.0039060000000000002,0) [0|251] "km/h" Vector__XXX + SG_ Tachometer : 16|16@1+ (0.125,0) [0|8031.0000000000045] "rpm" Vector__XXX + SG_ TemperatureGauge : 40|8@1+ (0.40000000000000002,0) [0|100] "% C-to-H" Vector__XXX + +BO_ 82 TransmissionFluids: 6 Vector__XXX + SG_ ClutchPressure : 0|8@1+ (16,0) [0|4000] "kPa" Vector__XXX + SG_ TransmissionFilterPressure : 16|8@1+ (0.16,0) [0|40] "bar" Vector__XXX + SG_ TransmissionOilLevel : 8|8@1+ (0.40000000000000002,0) [0|100] "%" Vector__XXX + SG_ TransmissionOilPressure : 24|8@1+ (0.16,0) [0|40] "bar" Vector__XXX + SG_ TransmissionOilTemp : 32|16@1+ (0.03125,-273) [-273|1735] "°C" Vector__XXX + +CM_ "This is an example CAN cluster. The CAN cluster defines what baudrate is used for all nodes on the network."; +CM_ BO_ 64 "This is an example of a periodic frame, or cyclic frame."; +CM_ BO_ 65 "This is an example of a periodic frame, or cyclic frame. This frame has a faster transmit time than CANCyclicFrame1 and less data."; +CM_ BO_ 66 "This is an example of an event frame. You can still provide a \"Transmit Time\" which will be used for re-sampling for waveform sessions."; +CM_ BO_ 67 "This is an example of an event frame. You can still provide a \"Transmit Time\" which will be used for re-sampling for waveform sessions."; +CM_ BO_ 133 "The InstrumentPanel is an example of a \"real\" CAN frame with actual signal values."; +CM_ BO_ 82 "The TransmissionFluids is an example of a \"real\" CAN frame with actual signal values."; +BA_DEF_ "DBName" STRING; +BA_DEF_ "BusType" STRING; +BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","reserved","J1939PG","reserved","reserved","reserved","reserved","reserved","reserved","reserved","reserved","reserved","reserved","StandardCAN_FD","ExtendedCAN_FD"; +BA_DEF_ BO_ "GenMsgSendType" ENUM "Cyclic","Event","CyclicIfActive","SpontanWithDelay","CyclicAndSpontan","CyclicAndSpontanWithDelay"; +BA_DEF_ BO_ "GenMsgCycleTime" INT 0 0; +BA_DEF_ BO_ "GenMsgDelayTime" INT 0 0; +BA_DEF_ SG_ "GenSigStartValue" FLOAT 0 0; +BA_DEF_DEF_ "DBName" "CAN_Cluster"; +BA_DEF_DEF_ "BusType" "CAN"; +BA_DEF_DEF_ "VFrameFormat" "StandardCAN"; +BA_DEF_DEF_ "GenMsgSendType" "Cyclic"; +BA_DEF_DEF_ "GenMsgCycleTime" 0; +BA_DEF_DEF_ "GenMsgDelayTime" 0; +BA_DEF_DEF_ "GenSigStartValue" 0; +BA_ "VFrameFormat" BO_ 64 0; +BA_ "GenMsgSendType" BO_ 64 0; +BA_ "GenMsgCycleTime" BO_ 64 10; +BA_ "VFrameFormat" BO_ 65 0; +BA_ "GenMsgSendType" BO_ 65 0; +BA_ "GenMsgCycleTime" BO_ 65 1; +BA_ "VFrameFormat" BO_ 66 0; +BA_ "GenMsgSendType" BO_ 66 3; +BA_ "GenMsgDelayTime" BO_ 66 1; +BA_ "VFrameFormat" BO_ 67 0; +BA_ "GenMsgSendType" BO_ 67 3; +BA_ "GenMsgDelayTime" BO_ 67 1; +BA_ "VFrameFormat" BO_ 133 0; +BA_ "GenMsgSendType" BO_ 133 3; +BA_ "GenMsgDelayTime" BO_ 133 1000; +BA_ "VFrameFormat" BO_ 82 0; +BA_ "GenMsgSendType" BO_ 82 3; +BA_ "GenMsgDelayTime" BO_ 82 1000; +BA_ "GenSigStartValue" SG_ 82 TransmissionOilTemp 8736; diff --git a/nixnet_examples/programmatic_database_usage.py b/nixnet_examples/programmatic_database_usage.py new file mode 100644 index 00000000..c76b3412 --- /dev/null +++ b/nixnet_examples/programmatic_database_usage.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +import six + +import nixnet +from nixnet import constants +from nixnet.system import system +from nixnet import types + + +def main(): + with system.System() as my_system: + database_alias = 'custom_database' + database_filepath = os.path.join(os.path.dirname(__file__), 'nixnet_examples\databases\custom_database.dbc') + default_baud_rate = 500000 + my_system.databases.add_alias(database_alias, database_filepath, default_baud_rate) + + database_name = 'custom_database' + cluster_name = 'CAN_Cluster' + output_frame = 'CANEventFrame1' + interface = 'CAN1' + + with nixnet.FrameOutQueuedSession( + interface, + database_name, + cluster_name, + output_frame) as output_session: + terminated_cable = six.moves.input('Are you using a terminated cable (Y or N)? ') + if terminated_cable.lower() == "y": + output_session.intf.can_term = constants.CanTerm.OFF + elif terminated_cable.lower() == "n": + output_session.intf.can_term = constants.CanTerm.ON + else: + print("Unrecognised input ({}), assuming 'n'".format(terminated_cable)) + output_session.intf.can_term = constants.CanTerm.ON + + user_value = six.moves.input('Enter payload [int, int]: ') + try: + payload_list = [int(x.strip()) for x in user_value.split(",")] + except ValueError: + payload_list = [2, 4, 8, 16] + print('Unrecognized input ({}). Setting data buffer to {}'.format(user_value, payload_list)) + + id = types.CanIdentifier(0) + payload = bytearray(payload_list) + frame = types.CanFrame(id, constants.FrameType.CAN_DATA, payload) + + print("Writing CAN frames using {} alias:".format(database_name)) + + i = 0 + while i < 3: + for index, byte in enumerate(payload): + payload[index] = byte + i + + frame.payload = payload + output_session.frames.write([frame]) + print('Sent frame with ID %s payload: %s' % (id, payload)) + i += 1 + + with system.System() as my_system: + del my_system.databases[database_name] + +if __name__ == '__main__': + main() diff --git a/tests/test_examples.py b/tests/test_examples.py index 14fe0acd..5a36ffd3 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -15,6 +15,7 @@ from nixnet_examples import can_signal_conversion from nixnet_examples import can_signal_single_point_io from nixnet_examples import lin_frame_stream_io +from nixnet_examples import programmatic_database_usage MockXnetLibrary = mock.create_autospec(_cfuncs.XnetLibrary, spec_set=True, instance=True) @@ -31,6 +32,10 @@ MockXnetLibrary.nx_convert_signals_to_frames_single_point.return_value = _ctypedefs.u32(0) MockXnetLibrary.nx_stop.return_value = _ctypedefs.u32(0) MockXnetLibrary.nx_clear.return_value = _ctypedefs.u32(0) +MockXnetLibrary.nx_system_open.return_value = _ctypedefs.u32(0) +MockXnetLibrary.nxdb_add_alias64.return_value = _ctypedefs.u32(0) +MockXnetLibrary.nxdb_remove_alias.return_value = _ctypedefs.u32(0) +MockXnetLibrary.nx_system_close.return_value = _ctypedefs.u32(0) def six_input(queue): @@ -113,3 +118,16 @@ def test_can_signal_conversion_empty_session(input_values): def test_lin_frame_stream_empty_session(input_values): with mock.patch('six.moves.input', six_input(input_values)): lin_frame_stream_io.main() + + +@pytest.mark.parametrize("input_values", [ + ['y', '1, 2'], + ['n', '1, 2'], + ['y', 'invalid'], + ['invalid', 'invalid'], +]) +@mock.patch('nixnet._cfuncs.lib', MockXnetLibrary) +@mock.patch('time.sleep', lambda time: None) +def test_programmatic_database_usage(input_values): + with mock.patch('six.moves.input', six_input(input_values)): + programmatic_database_usage.main() diff --git a/tests/test_system.py b/tests/test_system.py index 7c711c46..f43fb639 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -2,6 +2,7 @@ from __future__ import division from __future__ import print_function +import os import time import pytest # type: ignore @@ -202,3 +203,29 @@ def test_intf_properties(can_in_interface): print(in_intf.dongle_firmware_version) print(in_intf.dongle_compatible_revision) print(in_intf.dongle_compatible_firmware_version) + + +@pytest.mark.integration +def test_database_aliases(): + with system.System() as sys: + print(list(sys.databases)) + print(len(sys.databases)) + database_alias = 'test_database' + dir_name = os.path.dirname(__file__) + database_filepath = os.path.join(dir_name, '..', 'nixnet_examples\databases\custom_database.dbc') + default_baud_rate = 750000 + sys.databases.add_alias(database_alias, database_filepath, default_baud_rate) + print(len(sys.databases)) + print(list(sys.databases)) + print(sys.databases['test_database'].filepath) + + del sys.databases['test_database'] + print(len(sys.databases)) + print(list(sys.databases)) + + print(list(sys.databases.keys())) + print(list(sys.databases.values())) + print(list(sys.databases.items())) + + for database in sys.databases: + print(database)