diff --git a/.gitignore b/.gitignore index 0855031e..01aec47d 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ Cargo.lock integration_tests/flask/app/surreal.py/ integration_tests/package-lock.json integration_tests/node_modules/ + +clog/ +manifest/ diff --git a/libsrc/libsurrealdb_c.dylib b/libsrc/libsurrealdb_c.dylib new file mode 100755 index 00000000..73885b12 Binary files /dev/null and b/libsrc/libsurrealdb_c.dylib differ diff --git a/libsrc/surrealdb.h b/libsrc/surrealdb.h new file mode 100644 index 00000000..f27490e2 --- /dev/null +++ b/libsrc/surrealdb.h @@ -0,0 +1,440 @@ +#include +#include +#include +#include + +#define sr_SR_NONE 0 + +#define sr_SR_CLOSED -1 + +#define sr_SR_ERROR -2 + +#define sr_SR_FATAL -3 + +typedef enum sr_action { + SR_ACTION_CREATE, + SR_ACTION_UPDATE, + SR_ACTION_DELETE, +} sr_action; + +typedef struct sr_opaque_object_internal_t sr_opaque_object_internal_t; + +typedef struct sr_RpcStream sr_RpcStream; + +/** + * may be sent across threads, but must not be aliased + */ +typedef struct sr_stream_t sr_stream_t; + +/** + * The object representing a Surreal connection + * + * It is safe to be referenced from multiple threads + * If any operation, on any thread returns SR_FATAL then the connection is poisoned and must not be used again. + * (use will cause the program to abort) + * + * should be freed with sr_surreal_disconnect + */ +typedef struct sr_surreal_t sr_surreal_t; + +/** + * The object representing a Surreal connection + * + * It is safe to be referenced from multiple threads + * If any operation, on any thread returns SR_FATAL then the connection is poisoned and must not be used again. + * (use will cause the program to abort) + * + * should be freed with sr_surreal_disconnect + */ +typedef struct sr_surreal_rpc_t sr_surreal_rpc_t; + +typedef char *sr_string_t; + +typedef struct sr_object_t { + struct sr_opaque_object_internal_t *_0; +} sr_object_t; + +typedef enum sr_number_t_Tag { + SR_NUMBER_INT, + SR_NUMBER_FLOAT, +} sr_number_t_Tag; + +typedef struct sr_number_t { + sr_number_t_Tag tag; + union { + struct { + int64_t sr_number_int; + }; + struct { + double sr_number_float; + }; + }; +} sr_number_t; + +typedef struct sr_duration_t { + uint64_t secs; + uint32_t nanos; +} sr_duration_t; + +typedef struct sr_uuid_t { + uint8_t _0[16]; +} sr_uuid_t; + +typedef struct sr_bytes_t { + uint8_t *arr; + int len; +} sr_bytes_t; + +typedef enum sr_id_t_Tag { + SR_ID_NUMBER, + SR_ID_STRING, + SR_ID_ARRAY, + SR_ID_OBJECT, +} sr_id_t_Tag; + +typedef struct sr_id_t { + sr_id_t_Tag tag; + union { + struct { + int64_t sr_id_number; + }; + struct { + sr_string_t sr_id_string; + }; + struct { + struct sr_array_t *sr_id_array; + }; + struct { + struct sr_object_t sr_id_object; + }; + }; +} sr_id_t; + +typedef struct sr_thing_t { + sr_string_t table; + struct sr_id_t id; +} sr_thing_t; + +typedef enum sr_value_t_Tag { + SR_VALUE_NONE, + SR_VALUE_NULL, + SR_VALUE_BOOL, + SR_VALUE_NUMBER, + SR_VALUE_STRAND, + SR_VALUE_DURATION, + SR_VALUE_DATETIME, + SR_VALUE_UUID, + SR_VALUE_ARRAY, + SR_VALUE_OBJECT, + SR_VALUE_BYTES, + SR_VALUE_THING, +} sr_value_t_Tag; + +typedef struct sr_value_t { + sr_value_t_Tag tag; + union { + struct { + bool sr_value_bool; + }; + struct { + struct sr_number_t sr_value_number; + }; + struct { + sr_string_t sr_value_strand; + }; + struct { + struct sr_duration_t sr_value_duration; + }; + struct { + sr_string_t sr_value_datetime; + }; + struct { + struct sr_uuid_t sr_value_uuid; + }; + struct { + struct sr_array_t *sr_value_array; + }; + struct { + struct sr_object_t sr_value_object; + }; + struct { + struct sr_bytes_t sr_value_bytes; + }; + struct { + struct sr_thing_t sr_value_thing; + }; + }; +} sr_value_t; + +typedef struct sr_array_t { + struct sr_value_t *arr; + int len; +} sr_array_t; + +/** + * when code = 0 there is no error + */ +typedef struct sr_SurrealError { + int code; + sr_string_t msg; +} sr_SurrealError; + +typedef struct sr_arr_res_t { + struct sr_array_t ok; + struct sr_SurrealError err; +} sr_arr_res_t; + +typedef struct sr_option_t { + bool strict; + uint8_t query_timeout; + uint8_t transaction_timeout; +} sr_option_t; + +typedef struct sr_notification_t { + struct sr_uuid_t query_id; + enum sr_action action; + struct sr_value_t data; +} sr_notification_t; + +/** + * connects to a local, remote, or embedded database + * + * if any function returns SR_FATAL, this must not be used (except to drop) (TODO: check this is safe) doing so will cause the program to abort + * + * # Examples + * + * ```c + * sr_string_t err; + * sr_surreal_t *db; + * + * // connect to in-memory instance + * if (sr_connect(&err, &db, "mem://") < 0) { + * printf("error connecting to db: %s\n", err); + * return 1; + * } + * + * // connect to surrealkv file + * if (sr_connect(&err, &db, "surrealkv://test.skv") < 0) { + * printf("error connecting to db: %s\n", err); + * return 1; + * } + * + * // connect to surrealdb server + * if (sr_connect(&err, &db, "wss://localhost:8000") < 0) { + * printf("error connecting to db: %s\n", err); + * return 1; + * } + * + * sr_surreal_disconnect(db); + * ``` + */ +int sr_connect(sr_string_t *err_ptr, + struct sr_surreal_t **surreal_ptr, + const char *endpoint); + +/** + * disconnect a database connection + * note: the Surreal object must not be used after this function has been called + * any object allocations will still be valid, and should be freed, using the appropriate function + * TODO: check if Stream can be freed after disconnection because of rt + * + * # Examples + * + * ```c + * sr_surreal_t *db; + * // connect + * disconnect(db); + * ``` + */ +void sr_surreal_disconnect(struct sr_surreal_t *db); + +/** + * create a record + * + */ +int sr_create(const struct sr_surreal_t *db, + sr_string_t *err_ptr, + struct sr_object_t **res_ptr, + const char *resource, + const struct sr_object_t *content); + +/** + * make a live selection + * if successful sets *stream_ptr to be an exclusive reference to an opaque Stream object + * which can be moved accross threads but not aliased + * + * # Examples + * + * sr_stream_t *stream; + * if (sr_select_live(db, &err, &stream, "foo") < 0) + * { + * printf("%s", err); + * return 1; + * } + * + * sr_notification_t not ; + * if (sr_stream_next(stream, ¬ ) > 0) + * { + * sr_print_notification(¬ ); + * } + * sr_stream_kill(stream); + */ +int sr_select_live(const struct sr_surreal_t *db, + sr_string_t *err_ptr, + struct sr_stream_t **stream_ptr, + const char *resource); + +int sr_query(const struct sr_surreal_t *db, + sr_string_t *err_ptr, + struct sr_arr_res_t **res_ptr, + const char *query, + const struct sr_object_t *vars); + +/** + * select a resource + * + * can be used to select everything from a table or a single record + * writes values to *res_ptr, and returns number of values + * result values are allocated by Surreal and must be freed with sr_free_arr + * + * # Examples + * + * ```c + * sr_surreal_t *db; + * sr_string_t err; + * sr_value_t *foos; + * int len = sr_select(db, &err, &foos, "foo"); + * if (len < 0) { + * printf("%s", err); + * return 1; + * } + * ``` + * for (int i = 0; i < len; i++) + * { + * sr_value_print(&foos[i]); + * } + * sr_free_arr(foos, len); + */ +int sr_select(const struct sr_surreal_t *db, + sr_string_t *err_ptr, + struct sr_value_t **res_ptr, + const char *resource); + +/** + * select database + * NOTE: namespace must be selected first with sr_use_ns + * + * # Examples + * ```c + * sr_surreal_t *db; + * sr_string_t err; + * if (sr_use_db(db, &err, "test") < 0) + * { + * printf("%s", err); + * return 1; + * } + * ``` + */ +int sr_use_db(const struct sr_surreal_t *db, sr_string_t *err_ptr, const char *query); + +/** + * select namespace + * NOTE: database must be selected before use with sr_use_db + * + * # Examples + * ```c + * sr_surreal_t *db; + * sr_string_t err; + * if (sr_use_ns(db, &err, "test") < 0) + * { + * printf("%s", err); + * return 1; + * } + * ``` + */ +int sr_use_ns(const struct sr_surreal_t *db, sr_string_t *err_ptr, const char *query); + +/** + * returns the db version + * NOTE: version is allocated in Surreal and must be freed with sr_free_string + * # Examples + * ```c + * sr_surreal_t *db; + * sr_string_t err; + * sr_string_t ver; + * + * if (sr_version(db, &err, &ver) < 0) + * { + * printf("%s", err); + * return 1; + * } + * printf("%s", ver); + * sr_free_string(ver); + * ``` + */ +int sr_version(const struct sr_surreal_t *db, sr_string_t *err_ptr, sr_string_t *res_ptr); + +int sr_surreal_rpc_new(sr_string_t *err_ptr, + struct sr_surreal_rpc_t **surreal_ptr, + const char *endpoint, + struct sr_option_t options); + +/** + * execute rpc + * + * free result with sr_free_byte_arr + */ +int sr_surreal_rpc_execute(const struct sr_surreal_rpc_t *self, + sr_string_t *err_ptr, + uint8_t **res_ptr, + const uint8_t *ptr, + int len); + +int sr_surreal_rpc_notifications(const struct sr_surreal_rpc_t *self, + sr_string_t *err_ptr, + struct sr_RpcStream **stream_ptr); + +void sr_surreal_rpc_free(struct sr_surreal_rpc_t *ctx); + +void sr_free_arr(struct sr_value_t *ptr, int len); + +void sr_free_bytes(struct sr_bytes_t bytes); + +void sr_free_byte_arr(uint8_t *ptr, int len); + +void sr_print_notification(const struct sr_notification_t *notification); + +const struct sr_value_t *sr_object_get(const struct sr_object_t *obj, const char *key); + +struct sr_object_t sr_object_new(void); + +void sr_object_insert(struct sr_object_t *obj, const char *key, const struct sr_value_t *value); + +void sr_object_insert_str(struct sr_object_t *obj, const char *key, const char *value); + +void sr_object_insert_int(struct sr_object_t *obj, const char *key, int value); + +void sr_object_insert_float(struct sr_object_t *obj, const char *key, float value); + +void sr_object_insert_double(struct sr_object_t *obj, const char *key, double value); + +void sr_free_object(struct sr_object_t obj); + +void sr_free_arr_res(struct sr_arr_res_t res); + +void sr_free_arr_res_arr(struct sr_arr_res_t *ptr, int len); + +/** + * blocks until next item is recieved on stream + * will return 1 and write notification to notification_ptr is recieved + * will return SR_NONE if the stream is closed + */ +int sr_stream_next(struct sr_stream_t *self, struct sr_notification_t *notification_ptr); + +void sr_stream_kill(struct sr_stream_t *stream); + +void sr_free_string(sr_string_t string); + +void sr_value_print(const struct sr_value_t *val); + +bool sr_value_eq(const struct sr_value_t *lhs, const struct sr_value_t *rhs); diff --git a/surrealdb/async_surrealdb.py b/surrealdb/async_surrealdb.py index 0d2c54c9..cae19e67 100644 --- a/surrealdb/async_surrealdb.py +++ b/surrealdb/async_surrealdb.py @@ -18,8 +18,10 @@ await db.use("ns", "db_name") ``` """ + import asyncio import uuid + from typing import Optional, TypeVar, Union, List from surrealdb.constants import DEFAULT_CONNECTION_URL diff --git a/surrealdb/connection.py b/surrealdb/connection.py index cb4ff65f..d3d8c3a5 100644 --- a/surrealdb/connection.py +++ b/surrealdb/connection.py @@ -24,6 +24,7 @@ def __init__( self._auth_token = None self._namespace = None self._database = None + self._locks = { ResponseType.SEND: threading.Lock(), ResponseType.NOTIFICATION: threading.Lock(), diff --git a/surrealdb/connection_clib.py b/surrealdb/connection_clib.py new file mode 100644 index 00000000..32eae53d --- /dev/null +++ b/surrealdb/connection_clib.py @@ -0,0 +1,210 @@ +import logging +import os +import ctypes +import platform +from typing import Tuple + +from surrealdb.errors import SurrealDbConnectionError +from surrealdb.connection import Connection +from surrealdb.constants import CLIB_FOLDER_PATH, ROOT_DIR + + +def get_lib_path() -> str: + if platform.system() == "Linux": + lib_extension = ".so" + elif platform.system() == "Darwin": + lib_extension = ".dylib" + elif platform.system() == "Windows": + lib_extension = ".dll" + else: + raise SurrealDbConnectionError("Unsupported operating system") + + lib_path = os.path.join(CLIB_FOLDER_PATH, f"libsurrealdb_c{lib_extension}") + if os.path.isfile(lib_path) is not True: + raise Exception(f"{lib_path} is missing") + + return lib_path + + +class sr_string_t(ctypes.c_char_p): + pass + + +class sr_option_t(ctypes.Structure): + _fields_ = [("strict", ctypes.c_bool), + ("query_timeout", ctypes.c_uint8), + ("transaction_timeout", ctypes.c_uint8)] + + +class sr_surreal_rpc_t(ctypes.Structure): + pass + + +class sr_RpcStream(ctypes.Structure): + pass + + +class sr_uuid_t(ctypes.Structure): + _fields_ = [("_0", ctypes.c_uint8 * 16)] + + +class sr_value_t_Tag(ctypes.c_int): + SR_VALUE_NONE = 0 + SR_VALUE_NULL = 1 + SR_VALUE_BOOL = 2 + SR_VALUE_NUMBER = 3 + SR_VALUE_STRAND = 4 + SR_VALUE_DURATION = 5 + SR_VALUE_DATETIME = 6 + SR_VALUE_UUID = 7 + SR_VALUE_ARRAY = 8 + SR_VALUE_OBJECT = 9 + SR_VALUE_BYTES = 10 + SR_VALUE_THING = 11 + + +class sr_value_t(ctypes.Structure): + _fields_ = [ + ("tag", sr_value_t_Tag), + ("sr_value_bool", ctypes.c_bool) + ] + + +class sr_notification_t(ctypes.Structure): + _fields_ = [ + ("query_id", sr_uuid_t), + ("action", ctypes.c_int), # sr_action + ("data", sr_value_t) + ] + + +class CLibConnection(Connection): + def __init__(self, base_url: str, logger: logging.Logger): + super().__init__(base_url, logger) + + lib_path = get_lib_path() + self._lib = ctypes.CDLL(lib_path) + + self._c_surreal_rpc = None + self._c_surreal_stream = None + + def set_up_lib(self): + # int sr_surreal_rpc_new( + # sr_string_t *err_ptr, + # struct sr_surreal_rpc_t **surreal_ptr, + # const char *endpoint, + # struct sr_option_t options); + self._lib.sr_surreal_rpc_new.argtypes = [ + ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr + ctypes.POINTER(ctypes.POINTER(sr_surreal_rpc_t)), # struct sr_surreal_rpc_t **surreal_ptr + ctypes.c_char_p, # const char *endpoint + sr_option_t, # struct sr_option_t options + ] + self._lib.sr_surreal_rpc_new.restype = ctypes.c_int + + # int sr_surreal_rpc_notifications( + # const struct sr_surreal_rpc_t *self, + # sr_string_t *err_ptr, + # struct sr_RpcStream **stream_ptr); + self._lib.sr_surreal_rpc_notifications.argtypes = [ + ctypes.POINTER(sr_surreal_rpc_t), # const sr_surreal_rpc_t *self + ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr + ctypes.POINTER(ctypes.POINTER(sr_RpcStream)) # struct sr_RpcStream **stream_ptr + ] + self._lib.sr_surreal_rpc_notifications.restype = ctypes.c_int + + # int sr_surreal_rpc_execute( + # const struct sr_surreal_rpc_t *self, + # sr_string_t *err_ptr, + # uint8_t **res_ptr, + # const uint8_t *ptr, + # int len + # ); + self._lib.sr_surreal_rpc_execute.argtypes = [ + ctypes.POINTER(sr_surreal_rpc_t), # const sr_surreal_rpc_t *self + ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr + ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), # uint8_t **res_ptr + ctypes.POINTER(ctypes.c_uint8), # const uint8_t *ptr + ctypes.c_int # int len + ] + self._lib.sr_surreal_rpc_execute.restype = ctypes.c_int + + # sr_stream_next + self._lib.sr_stream_next.argtypes = [ + ctypes.POINTER(sr_RpcStream), # const sr_stream_t *self + ctypes.POINTER(sr_notification_t) # sr_notification_t *notification_ptr + ] + self._lib.sr_stream_next.restype = ctypes.c_int + + # sr_stream_kill + self._lib.sr_stream_kill.argtypes = [ctypes.POINTER(sr_RpcStream)] + self._lib.sr_stream_kill.restype = None + + # sr_free_string + self._lib.sr_free_string.argtypes = [sr_string_t] + self._lib.sr_free_string.restype = None + + # sr_free_byte_arr + self._lib.sr_free_byte_arr.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.c_int] + self._lib.sr_free_byte_arr.restype = None + + async def connect(self): + c_err = sr_string_t() + c_rpc_connection = ctypes.POINTER(sr_surreal_rpc_t)() + c_endpoint = bytes(self._base_url, "utf-8") + c_options = sr_option_t(strict=True, query_timeout=10, transaction_timeout=20) + + try: + if self._lib.sr_surreal_rpc_new( + ctypes.byref(c_err), + ctypes.byref(c_rpc_connection), + c_endpoint, + c_options + ) < 0: + raise SurrealDbConnectionError(f"Error connecting to RPC. {c_err.value.decode()}") + self._c_surreal_rpc = c_rpc_connection + + except Exception as e: + raise SurrealDbConnectionError('cannot connect db server', e) + finally: + self._lib.sr_free_string(c_err) + + async def close(self): + pass + + async def use(self, namespace: str, database: str) -> None: + self._namespace = namespace + self._database = database + + await self.send("use", namespace, database) + + async def set(self, key: str, value): + await self.send("let", key, value) + + async def unset(self, key: str): + await self.send("unset", key) + + async def _make_request(self, request_payload: bytes) -> Tuple[bool, bytes]: + c_err = sr_string_t() + c_res_ptr = ctypes.POINTER(ctypes.c_uint8)() + payload_len = len(request_payload) + + # Call sr_surreal_rpc_execute + result = self._lib.sr_surreal_rpc_execute( + self._c_surreal_rpc, + ctypes.byref(c_err), + ctypes.byref(c_res_ptr), + (ctypes.c_uint8 * payload_len)(*request_payload), + payload_len + ) + + if result < 0: + raise SurrealDbConnectionError( + f"Error executing RPC: {c_err.value.decode() if c_err.value else 'Unknown error'}") + + # Convert the result pointer to a Python byte array + response = ctypes.string_at(c_res_ptr, result) + + # Free the allocated byte array returned by the C library + self._lib.sr_free_byte_arr(c_res_ptr, result) + return True, response diff --git a/surrealdb/connection_factory.py b/surrealdb/connection_factory.py index 94eaa087..f9187446 100644 --- a/surrealdb/connection_factory.py +++ b/surrealdb/connection_factory.py @@ -1,8 +1,11 @@ import logging + from urllib.parse import urlparse from surrealdb.connection import Connection -from surrealdb.constants import ALLOWED_CONNECTION_SCHEMES, WS_CONNECTION_SCHEMES, HTTP_CONNECTION_SCHEMES +from surrealdb.connection_clib import CLibConnection +from surrealdb.constants import ALLOWED_CONNECTION_SCHEMES, WS_CONNECTION_SCHEMES, HTTP_CONNECTION_SCHEMES, \ + CLIB_CONNECTION_SCHEMES from surrealdb.connection_http import HTTPConnection from surrealdb.connection_ws import WebsocketConnection from surrealdb.errors import SurrealDbConnectionError @@ -23,4 +26,8 @@ def create_connection_factory(url: str) -> Connection: logger.debug("http url detected, creating a http connection") return HTTPConnection(url, logger) + if parsed_url.scheme in CLIB_CONNECTION_SCHEMES: + logger.debug("embedded url detected, creating a clib connection") + return CLibConnection(url, logger) + raise Exception('no connection type available') diff --git a/surrealdb/connection_http.py b/surrealdb/connection_http.py index 86fe1648..6ca1815f 100644 --- a/surrealdb/connection_http.py +++ b/surrealdb/connection_http.py @@ -8,6 +8,7 @@ class HTTPConnection(Connection): + def __init__(self, base_url: str, logger: logging.Logger): super().__init__(base_url, logger) diff --git a/surrealdb/constants.py b/surrealdb/constants.py index bcc6d872..8cb2830b 100644 --- a/surrealdb/constants.py +++ b/surrealdb/constants.py @@ -1,11 +1,20 @@ +import os + REQUEST_ID_LENGTH = 10 -ALLOWED_CONNECTION_SCHEMES = ['http', 'https', 'ws', 'wss'] +# Connection HTTP_CONNECTION_SCHEMES = ['http', 'https'] WS_CONNECTION_SCHEMES = ['ws', 'wss'] +CLIB_CONNECTION_SCHEMES = ['memory', 'surrealkv'] +ALLOWED_CONNECTION_SCHEMES = HTTP_CONNECTION_SCHEMES + WS_CONNECTION_SCHEMES + CLIB_CONNECTION_SCHEMES DEFAULT_CONNECTION_URL = "http://127.0.0.1:8000" -WS_REQUEST_TIMEOUT = 10 # seconds - +# Methods UNSUPPORTED_HTTP_METHODS = ["kill", "live"] + +# Paths +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) +CLIB_FOLDER_PATH = os.path.join(ROOT_DIR, "libsrc") + +WS_REQUEST_TIMEOUT = 10 # seconds diff --git a/surrealdb/surrealdb.py b/surrealdb/surrealdb.py index 2fbdd11d..1adf727e 100644 --- a/surrealdb/surrealdb.py +++ b/surrealdb/surrealdb.py @@ -18,8 +18,10 @@ db.use("ns", "db_name") ``` """ + import asyncio import uuid + from typing import Optional, TypeVar, Union, List from surrealdb.asyncio_runtime import AsyncioRuntime diff --git a/tests/integration/async/test_live.py b/tests/integration/async/test_live.py index 15b534bb..ddec1f7e 100644 --- a/tests/integration/async/test_live.py +++ b/tests/integration/async/test_live.py @@ -1,5 +1,4 @@ import asyncio -import logging from typing import List from unittest import IsolatedAsyncioTestCase diff --git a/tests/unit/test_clib_connection.py b/tests/unit/test_clib_connection.py new file mode 100644 index 00000000..6a7618d0 --- /dev/null +++ b/tests/unit/test_clib_connection.py @@ -0,0 +1,14 @@ +from unittest import IsolatedAsyncioTestCase +from logging import getLogger +from surrealdb.connection_clib import CLibConnection + + +class TestCLibConnection(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.logger = getLogger(__name__) + + self.clib = CLibConnection(base_url='surrealkv://', logger=self.logger) + await self.clib.connect() + + async def test_send(self): + await self.clib.send('use', "test_ns", "test_db") diff --git a/tests/unit/test_http_connection.py b/tests/unit/test_http_connection.py index 2c2e841d..e9da7a03 100644 --- a/tests/unit/test_http_connection.py +++ b/tests/unit/test_http_connection.py @@ -1,4 +1,5 @@ import logging + from unittest import IsolatedAsyncioTestCase from surrealdb.connection_http import HTTPConnection diff --git a/tests/unit/test_ws_connection.py b/tests/unit/test_ws_connection.py index e74e6fbc..57c0f23d 100644 --- a/tests/unit/test_ws_connection.py +++ b/tests/unit/test_ws_connection.py @@ -1,5 +1,6 @@ import asyncio import logging + from unittest import IsolatedAsyncioTestCase from surrealdb.connection_ws import WebsocketConnection @@ -26,5 +27,3 @@ async def test_send(self): notification_data = await asyncio.wait_for(live_queue.get(), 10) # Set timeout self.assertEqual(notification_data.get("id"), live_id) self.assertEqual(notification_data.get("action"), "CREATE") - -