From b78d55a851ef119f83a6a4b56aee128f77dc602b Mon Sep 17 00:00:00 2001 From: The many faced demon <154847721+themanyfaceddemon@users.noreply.github.com> Date: Wed, 11 Sep 2024 01:02:28 +0300 Subject: [PATCH] =?UTF-8?q?"=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=200.2.0"?= =?UTF-8?q?=20=D0=B8=D0=BB=D0=B8=20=D0=B6=D0=B5=20"=D0=AF=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=D0=B2=D0=B8=D0=B6=D1=83=20=D1=81=D0=B5=D0=B1=D1=8F?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=20=D1=8D=D1=82=D0=BE=D1=82=20=D0=B5?= =?UTF-8?q?=D0=B1=D1=83=D1=87=D0=B8=D0=B9=20=D0=BD=D1=8D=D1=82=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4"=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправил баг с кривой загрузкой файликов - Оптимизировал всё и отрефакторил - Регестрация классов только руками. Ебал --- .gitignore | 6 +- .vscode/launch.json | 34 +++ DMBotNetwork/__init__.py | 10 +- DMBotNetwork/main/__init__.py | 0 DMBotNetwork/main/client.py | 309 ++++++++++++++++++++ DMBotNetwork/main/server.py | 245 ++++++++++++++++ DMBotNetwork/main/utils/__init__.py | 5 + DMBotNetwork/main/utils/cl_unit.py | 208 ++++++++++++++ DMBotNetwork/main/utils/response_code.py | 48 ++++ DMBotNetwork/{ => main}/utils/server_db.py | 74 ++--- DMBotNetwork/side/__init__.py | 4 - DMBotNetwork/side/side_client.py | 317 --------------------- DMBotNetwork/side/side_server.py | 261 ----------------- DMBotNetwork/units/__init__.py | 3 - DMBotNetwork/units/client_unit.py | 121 -------- DMBotNetwork/utils/__init__.py | 19 -- DMBotNetwork/utils/net_code_encode.py | 26 -- Tests/ServerDB.py | 2 +- client_test.py | 27 ++ requirements.txt | 1 + server_test.py | 38 +++ setup.py | 4 +- test.py | 0 23 files changed, 955 insertions(+), 807 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 DMBotNetwork/main/__init__.py create mode 100644 DMBotNetwork/main/client.py create mode 100644 DMBotNetwork/main/server.py create mode 100644 DMBotNetwork/main/utils/__init__.py create mode 100644 DMBotNetwork/main/utils/cl_unit.py create mode 100644 DMBotNetwork/main/utils/response_code.py rename DMBotNetwork/{ => main}/utils/server_db.py (80%) delete mode 100644 DMBotNetwork/side/__init__.py delete mode 100644 DMBotNetwork/side/side_client.py delete mode 100644 DMBotNetwork/side/side_server.py delete mode 100644 DMBotNetwork/units/__init__.py delete mode 100644 DMBotNetwork/units/client_unit.py delete mode 100644 DMBotNetwork/utils/__init__.py delete mode 100644 DMBotNetwork/utils/net_code_encode.py create mode 100644 client_test.py create mode 100644 server_test.py create mode 100644 test.py diff --git a/.gitignore b/.gitignore index 5106b86..7386aab 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,6 @@ ENV/ env.bak/ venv.bak/ -# VS and VSC -.vs/* -.vscode/* - # build build/* dist/* @@ -31,3 +27,5 @@ dist/* # tests test_dir/* +test_db_path/* +client_path/* diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0ebde75 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Start client", + "type": "debugpy", + "request": "launch", + "program": "client_test.py", + "console": "integratedTerminal" + }, + { + "name": "Start server", + "type": "debugpy", + "request": "launch", + "program": "server_test.py", + "console": "integratedTerminal" + }, + { + "name": "Start tests", + "type": "debugpy", + "request": "launch", + "module": "unittest", + "args": [ + "discover", + "-s", + "Tests", + "-p", + "*.py", + "-v" + ], + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/DMBotNetwork/__init__.py b/DMBotNetwork/__init__.py index 1f0d2ce..2aa9e82 100644 --- a/DMBotNetwork/__init__.py +++ b/DMBotNetwork/__init__.py @@ -1,6 +1,6 @@ -from .side import Client, Server -from .units import ClientUnit -from .utils import ServerDB +from .main.client import Client +from .main.server import Server +from .main.utils.cl_unit import ClUnit -__all__ = ["Client", "Server", "ClientUnit", "ServerDB"] -__version__ = "0.1.5" +__all__ = ["Client", "Server", "ClUnit"] +__version__ = "0.2.0" diff --git a/DMBotNetwork/main/__init__.py b/DMBotNetwork/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DMBotNetwork/main/client.py b/DMBotNetwork/main/client.py new file mode 100644 index 0000000..ab378f1 --- /dev/null +++ b/DMBotNetwork/main/client.py @@ -0,0 +1,309 @@ +import asyncio +import inspect +import json +import logging +from collections.abc import Callable +from pathlib import Path +from typing import Any, Dict, Optional, get_type_hints + +import aiofiles + +from .utils import ResponseCode + +logger = logging.getLogger("DMBN:Client") + + +class Client: + _network_funcs: Dict[str, Callable] = {} + _server_handler_task: Optional[asyncio.Task] = None + _disconnect_lock = asyncio.Lock() + + _server_name: str = "dev_server" + _reader: Optional[asyncio.StreamReader] = None + _writer: Optional[asyncio.StreamWriter] = None + + _is_auth: bool = False + _is_connected: bool = False + + _login: str = "owner" + _password: str = "owner_password" + _use_registration: bool = False + _content_path: Path = Path("") + + @classmethod + def register_methods_from_class(cls, external_class): + """Регистрация методов с префиксом 'net_' из внешнего класса.""" + for name, func in inspect.getmembers( + external_class, predicate=inspect.isfunction + ): + if name.startswith("net_"): + method_name = name[4:] + cls._network_funcs[method_name] = func + logger.debug( + f"Registered method '{name}' from {external_class.__name__} as '{method_name}'" + ) + + @classmethod + async def _call_func( + cls, + func_name: str, + **kwargs, + ) -> None: + func = cls._network_funcs.get(func_name) + if func is None: + logger.debug(f"Network func '{func_name}' not found.") + return + + sig = inspect.signature(func) + valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} + + type_hints = get_type_hints(func) + + for arg_name, arg_value in valid_kwargs.items(): + expected_type = type_hints.get(arg_name, Any) + if not isinstance(arg_value, expected_type) and expected_type is not Any: + logger.error( + f"Type mismatch for argument '{arg_name}': expected {expected_type}, got {type(arg_value)}." + ) + return + + try: + if inspect.iscoroutinefunction(func): + await func(cls, **valid_kwargs) + + else: + func(cls, **valid_kwargs) + + except Exception as e: + logger.error(f"Error calling method '{func_name}' in {cls.__name__}: {e}") + + @classmethod + async def send_package(cls, code: ResponseCode, **kwargs) -> None: + payload = {"code": code.value, **kwargs} + en_data = cls._encode_data(payload) + await cls._send_raw_data(en_data) + + @classmethod + async def req_net_func(cls, func_name: str, **kwargs) -> None: + await cls.send_package(ResponseCode.NET_REQ, net_func_name=func_name, **kwargs) + + @classmethod + def is_connected(cls) -> bool: + return cls._is_auth and cls._is_connected + + @classmethod + def setup( + cls, login: str, password: str, use_registration: bool, content_path: str | Path + ) -> None: + """Настройка клиента перед подключением. + + Args: + login (str): Логин пользователя. + password (str): Пароль пользователя. + use_registration (bool): Флаг использования регистрации вместо авторизации. + content_path (str | Path): Путь для сохранения файлов. + + Raises: + ValueError: Если один из параметров некорректен. + """ + if not all([login, password]): + raise ValueError("Login, password cannot be empty") + + cls._login = login + cls._password = password + cls._use_registration = use_registration + + content_path = Path(content_path) + if content_path.exists() and not content_path.is_dir(): + raise ValueError(f"{content_path} not a dir") + + content_path.mkdir(parents=True, exist_ok=True) + cls._content_path = content_path + + @classmethod + async def connect(cls, host, port) -> None: + try: + cls._reader, cls._writer = await asyncio.open_connection(host, port) + cls._is_connected = True + + logger.info(f"Connected to {host}:{port}") + + cls._server_handler_task = asyncio.create_task(cls._server_handler()) + + except Exception as err: + logger.error(f"Error while connect to sever: {err}") + await cls.disconnect() + + @classmethod + async def disconnect(cls) -> None: + async with cls._disconnect_lock: + cls._is_connected = False + cls._is_auth = False + + if cls._writer: + try: + cls._writer.close() + await cls._writer.wait_closed() + + except ConnectionAbortedError: + pass + + except Exception as err: + logger.error(f"Error during disconnect: {err}") + + if cls._server_handler_task: + cls._server_handler_task.cancel() + cls._server_handler_task = None + + download_files = cls._content_path.glob("**/*.download") + for file in download_files: + file.unlink() + + logger.info("Disconnected from server") + + @classmethod + async def _server_handler(cls) -> None: + try: + while cls._is_connected: + receive_package = await cls._receive_package() + + code = receive_package.pop("code", None) + if not code: + logger.error(f"Receive data must has 'code' key: {receive_package}") + continue + + if ResponseCode.is_net(code): + await cls._call_func( + receive_package.pop("net_func_name", None), + **receive_package, + ) + + elif ResponseCode.is_log(code): + cls._log_handler(code, receive_package) + + elif ResponseCode.is_auth(code): + await cls._auth_handler(code, receive_package) + + elif ResponseCode.is_file(code): + await cls._file_handler(code, receive_package) + + else: + logger.error(f"Unknown 'code' for net type: {receive_package}") + + except ( + asyncio.CancelledError, + ConnectionAbortedError, + asyncio.exceptions.IncompleteReadError, + ): + pass + + except Exception as err: + logger.error(str(err)) + + finally: + await cls.disconnect() + + @classmethod + def _log_handler(cls, code: int, receive_package: dict) -> None: + message = receive_package.get("message", None) + message = f"Server log: {message}" + + if code == ResponseCode.LOG_DEB: + logger.debug(message) + + elif code == ResponseCode.LOG_INF: + logger.info(message) + + elif code == ResponseCode.LOG_WAR: + logger.warning(message) + + elif code == ResponseCode.LOG_ERR: + logger.warning(message) + + else: + logger.warning(f"Unknown 'code': {receive_package}") + + @classmethod + async def _auth_handler(cls, code: int, receive_package: dict) -> None: + if code == ResponseCode.AUTH_REQ: + await cls.send_package( + ResponseCode.AUTH_ANS_REGIS + if cls._use_registration + else ResponseCode.AUTH_ANS_LOGIN, + login=cls._login, + password=cls._password, + ) + + elif code == ResponseCode.AUTH_ANS_SERVE: + server_name = receive_package.get("server_name", None) + if not server_name: + return + + cls._is_auth = True + cls._server_name = server_name + + @classmethod + async def _file_handler(cls, code: int, receive_package: dict) -> None: + if code == ResponseCode.FIL_REQ: + name = receive_package.get("name", None) + chunk = receive_package.get("chunk", None) + + if not all([name, chunk]): + return + + file_path: Path = ( + cls._content_path / cls._server_name / (name + ".download") + ) + file_path.parent.mkdir(parents=True, exist_ok=True) + + async with aiofiles.open(file_path, "ab") as file: + await file.write(chunk) + + elif code == ResponseCode.FIL_END: + name = receive_package.get("name", None) + if not name: + return + + file_path: Path = ( + cls._content_path / cls._server_name / (name + ".download") + ) + final_file_path: Path = cls._content_path / cls._server_name / name + + if file_path.exists(): + file_path.rename(final_file_path) + + @classmethod + async def _receive_package(cls) -> dict: + raw_data = await cls._receive_raw_data() + return cls._decode_data(raw_data) + + @classmethod + def _encode_data(cls, data: dict) -> bytes: + json_data = json.dumps(data, ensure_ascii=False) + return json_data.encode("utf-8") + + @classmethod + def _decode_data(cls, encoded_data: bytes) -> dict: + json_data = encoded_data.decode("utf-8") + return json.loads(json_data) + + @classmethod + async def _send_raw_data(cls, data: bytes) -> None: + if not cls._writer: + raise RuntimeError("Is not connected") + + cls._writer.write(len(data).to_bytes(4, "big")) + await cls._writer.drain() + + cls._writer.write(data) + await cls._writer.drain() + + @classmethod + async def _receive_raw_data(cls) -> bytes: + if not cls._reader: + raise RuntimeError("Is not connected") + + data_length_bytes = await cls._reader.readexactly(4) + data_length = int.from_bytes(data_length_bytes, "big") + + return await cls._reader.readexactly(data_length) diff --git a/DMBotNetwork/main/server.py b/DMBotNetwork/main/server.py new file mode 100644 index 0000000..cc3c3a1 --- /dev/null +++ b/DMBotNetwork/main/server.py @@ -0,0 +1,245 @@ +import asyncio +import inspect +import logging +from collections.abc import Callable +from pathlib import Path +from typing import Any, Dict, Optional, get_type_hints + +from .utils import ClUnit, ResponseCode, ServerDB + +logger = logging.getLogger("DMBN:Server") + + +class Server: + _network_funcs: Dict[str, Callable] = {} + _cl_units: Dict[str, ClUnit] = {} + _server: Optional[asyncio.AbstractServer] = None + + _is_online: bool = False + + _server_name: str = "Dev_Server" + _allow_registration: bool = True + _timeout: float = 30.0 + + @classmethod + def register_methods_from_class(cls, external_class): + """Регистрация методов с префиксом 'net_' из внешнего класса.""" + for name, func in inspect.getmembers( + external_class, predicate=inspect.isfunction + ): + if name.startswith("net_"): + method_name = name[4:] + cls._network_funcs[method_name] = func + logger.debug( + f"Registered method '{name}' from {external_class.__name__} as '{method_name}'" + ) + + @classmethod + async def _call_func( + cls, + func_name: str, + **kwargs, + ) -> None: + func = cls._network_funcs.get(func_name) + if func is None: + logger.debug(f"Network func '{func_name}' not found.") + return + + sig = inspect.signature(func) + valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} + + type_hints = get_type_hints(func) + + for arg_name, arg_value in valid_kwargs.items(): + expected_type = type_hints.get(arg_name, Any) + if not isinstance(arg_value, expected_type) and expected_type is not Any: + logger.error( + f"Type mismatch for argument '{arg_name}': expected {expected_type}, got {type(arg_value)}." + ) + return + + try: + if inspect.iscoroutinefunction(func): + await func(cls, **valid_kwargs) + + else: + func(cls, **valid_kwargs) + + except Exception as e: + logger.error(f"Error calling method '{func_name}' in {cls.__name__}: {e}") + + @classmethod + async def setup_server( + cls, + server_name: str, + host: str, + port: int, + db_path: str | Path, + init_owner_password: str, + base_access: Dict[str, bool], + allow_registration: bool, + timeout: float, + ) -> None: + cls._server_name = server_name + cls._allow_registration = allow_registration + cls._timeout = timeout + + ServerDB.set_db_path(db_path) + ServerDB.set_owner_base_password(init_owner_password) + ServerDB.set_base_access(base_access) + + cls._server = await asyncio.start_server(cls._cl_handler, host, port) + logger.info(f"Server setup. Host: {host}, port:{port}.") + + @classmethod + async def start(cls) -> None: + if not cls._server: + raise RuntimeError("Server is not initialized.") + + if cls._is_online: + raise RuntimeError("Server already start.") + + await ServerDB.start() + + try: + async with cls._server: + cls._is_online = True + logger.info("Server start.") + await cls._server.serve_forever() + + except asyncio.CancelledError: + pass + + except Exception as err: + logger.error(f"Error starting server: {err}") + + finally: + await cls.stop() + + @classmethod + async def stop(cls) -> None: + if not cls._is_online: + raise RuntimeError("Server is not working.") + + cls._is_online = False + + asyncio.gather(*(cl_unit.disconnect() for cl_unit in cls._cl_units.values())) + cls._cl_units.clear() + + if cls._server: + cls._server.close() + await cls._server.wait_closed() + + await ServerDB.stop() + logger.info("Server stop.") + + @classmethod + async def broadcast(cls, func_name: str, *args, **kwargs) -> None: + tasks = [] + for cl_unit in cls._cl_units.values(): + func = getattr(cl_unit, func_name, None) + if callable(func): + tasks.append(func(*args, **kwargs)) + + else: + logger.error(f"{func_name} is not a callable method of {cl_unit}") + + if tasks: + await asyncio.gather(*tasks) + + @classmethod + async def _cl_handler( + cls, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + cl_unit = ClUnit("init", reader, writer) + + try: + await cls._auth(cl_unit) + + except TimeoutError: + await cl_unit.send_log_error("Timeout for auth.") + await cl_unit.disconnect() + return + + except ValueError as err: + await cl_unit.send_log_error(str(err)) + await cl_unit.disconnect() + return + + except Exception as err: + await cl_unit.send_log_error(f"An unexpected error occurred: {err}") + await cl_unit.disconnect() + return + + cls._cl_units[cl_unit.login] = cl_unit + logger.info(f"{cl_unit.login} is connected.") + + try: + while cls._is_online: + receive_package = await cl_unit.receive_package() + if not isinstance(receive_package, dict): + await cl_unit.send_log_error("Receive data type expected dict.") + continue + + code = receive_package.pop("code", None) + if not code: + await cl_unit.send_log_error("Receive data must has 'code' key.") + continue + + if ResponseCode.is_net(code): + func_name = receive_package.pop("net_func_name", None) + await cls._call_func( + func_name, + cl_unit=cl_unit, + **receive_package, + ) + + else: + await cl_unit.send_log_error("Unknown 'code' for net type.") + + except Exception as err: + await cl_unit.send_log_error(f"An unexpected error occurred: {err}") + + finally: + cls._cl_units.pop(cl_unit.login, None) + await cl_unit.disconnect() + logger.info(f"{cl_unit.login} is disconected.") + + @classmethod + async def _auth(cls, cl_unit: ClUnit) -> None: + await cl_unit.send_package(ResponseCode.AUTH_REQ) + receive_package = await asyncio.wait_for( + cl_unit.receive_package(), cls._timeout + ) + + if not isinstance(receive_package, dict): + raise ValueError("Receive data type expected dict.") + + code = receive_package.get("code", None) + if not code: + raise ValueError("Receive data must has 'code' key.") + + code = ResponseCode(code) + + if not ResponseCode.is_client_auth(code): + raise ValueError("Unknown 'code' for auth type.") + + login = receive_package.get("login", None) + password = receive_package.get("password", None) + if not all([login, password]): + raise ValueError("Receive data must has 'login' and 'password' keys.") + + if code == ResponseCode.AUTH_ANS_REGIS: + if not cls._allow_registration: + raise ValueError("Registration is not allowed.") + + await ServerDB.add_user(login, password) + cl_unit.login = login + + else: + await ServerDB.login_user(login, password) + cl_unit.login = login + + await cl_unit.send_package( + ResponseCode.AUTH_ANS_SERVE, server_name=cls._server_name + ) diff --git a/DMBotNetwork/main/utils/__init__.py b/DMBotNetwork/main/utils/__init__.py new file mode 100644 index 0000000..316c101 --- /dev/null +++ b/DMBotNetwork/main/utils/__init__.py @@ -0,0 +1,5 @@ +from .cl_unit import ClUnit +from .response_code import ResponseCode +from .server_db import ServerDB + +__all__ = ["ClUnit", "ResponseCode", "ServerDB"] diff --git a/DMBotNetwork/main/utils/cl_unit.py b/DMBotNetwork/main/utils/cl_unit.py new file mode 100644 index 0000000..4fd7643 --- /dev/null +++ b/DMBotNetwork/main/utils/cl_unit.py @@ -0,0 +1,208 @@ +import asyncio +import json +from pathlib import Path + +import aiofiles + +from .response_code import ResponseCode + + +class ClUnit: + __slots__ = ["login", "_reader", "_writer"] + + def __init__( + self, login, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + """Инициализация объекта ClUnit. + + Args: + login (str): Логин пользователя. + reader (StreamReader): Асинхронный поток для чтения данных. + writer (StreamWriter): Асинхронный поток для записи данных. + """ + self.login = login + self._reader = reader + self._writer = writer + + def __eq__(self, value: object) -> bool: + if isinstance(value, str): + return value == self.login + + elif isinstance(value, ClUnit): + return value.login == self.login + + else: + return False + + def __hash__(self) -> int: + return hash(self.login) + + @property + def reader(self) -> asyncio.StreamReader: + """Возвращает асинхронный поток для чтения данных. + + Returns: + StreamReader: Поток для чтения данных. + """ + return self._reader + + @property + def writer(self) -> asyncio.StreamWriter: + """Возвращает асинхронный поток для записи данных. + + Returns: + StreamWriter: Поток для записи данных. + """ + return self._writer + + async def send_package(self, code: ResponseCode, **kwargs) -> None: + """Отправка пакета данных. + + Args: + code (ResponseCode): Код ответа для отправки. + **kwargs: Дополнительные данные для передачи. + """ + payload = {"code": code.value, **kwargs} + en_data = self._encode_data(payload) + await self._send_raw_data(en_data) + + async def receive_package(self) -> dict: + """Получение и декодирование пакета данных. + + Returns: + dict: Декодированные данные из пакета. + """ + raw_data = await self._receive_raw_data() + return self._decode_data(raw_data) + + async def send_file( + self, file_path: Path | str, file_name: str, chunk_size: int = 8192 + ) -> None: + """Асинхронная отправка файла по частям. + + Args: + file_path (Path | str): Путь к файлу для отправки. + file_name (str): Имя файла для отправки. + chunk_size (int, optional): Размер блока для отправки файла. По умолчанию 8192 байт. + + Raises: + ValueError: Если файл не существует или это не файл. + """ + file_path = Path(file_path) + + if not file_path.exists() or not file_path.is_file(): + raise ValueError( + "Invalid file_path. File doesn't exist or it is not a file." + ) + + async with aiofiles.open(file_path, "rb") as file: + while True: + chunk = await file.read(chunk_size) + if not chunk: + await self.send_package(ResponseCode.FIL_END, name=file_name) + break + + await self.send_package( + ResponseCode.FIL_REQ, name=file_name, chunk=chunk + ) + + async def send_log_debug(self, message: str) -> None: + """Отправка сообщения с уровнем логирования DEBUG. + + Args: + message (str): Сообщение для отправки. + """ + await self.send_package(ResponseCode.LOG_DEB, message=message) + + async def send_log_info(self, message: str) -> None: + """Отправка сообщения с уровнем логирования INFO. + + Args: + message (str): Сообщение для отправки. + """ + await self.send_package(ResponseCode.LOG_INF, message=message) + + async def send_log_warning(self, message: str) -> None: + """Отправка сообщения с уровнем логирования WARNING. + + Args: + message (str): Сообщение для отправки. + """ + await self.send_package(ResponseCode.LOG_WAR, message=message) + + async def send_log_error(self, message: str) -> None: + """Отправка сообщения с уровнем логирования ERROR. + + Args: + message (str): Сообщение для отправки. + """ + await self.send_package(ResponseCode.LOG_ERR, message=message) + + async def req_net_func(self, func_name: str, **kwargs) -> None: + """Отправка запроса на выполнение сетевой функции. + + Args: + func_name (str): Имя функции, которую нужно вызвать на сервере. + **kwargs: Дополнительные аргументы для функции. + """ + await self.send_package(ResponseCode.NET_REQ, net_func_name=func_name, **kwargs) + + def _encode_data(self, data: dict) -> bytes: + """Кодирование данных в JSON-формат. + + Args: + data (dict): Данные для кодирования. + + Returns: + bytes: Закодированные данные в формате байт. + """ + json_data = json.dumps(data, ensure_ascii=False) + return json_data.encode("utf-8") + + def _decode_data(self, encoded_data: bytes) -> dict: + """Декодирование данных из байт в JSON-формат. + + Args: + encoded_data (bytes): Закодированные данные. + + Returns: + dict: Декодированные данные в виде словаря. + """ + json_data = encoded_data.decode("utf-8") + return json.loads(json_data) + + async def _send_raw_data(self, data: bytes) -> None: + """Асинхронная отправка сырых данных. + + Args: + data (bytes): Данные для отправки. + """ + self._writer.write(len(data).to_bytes(4, "big")) + await self._writer.drain() + + self._writer.write(data) + await self._writer.drain() + + async def _receive_raw_data(self) -> bytes: + """Асинхронное получение сырых данных. + + Returns: + bytes: Полученные данные в байтовом формате. + """ + data_length_bytes = await self._reader.readexactly(4) + data_length = int.from_bytes(data_length_bytes, "big") + + return await self._reader.readexactly(data_length) + + async def disconnect(self) -> None: + """Отключение соединения.""" + if self._writer: + try: + self._writer.close() + await self._writer.wait_closed() + + except ConnectionAbortedError: + pass + + except Exception as err: + raise err diff --git a/DMBotNetwork/main/utils/response_code.py b/DMBotNetwork/main/utils/response_code.py new file mode 100644 index 0000000..43087e6 --- /dev/null +++ b/DMBotNetwork/main/utils/response_code.py @@ -0,0 +1,48 @@ +from enum import IntEnum + + +class ResponseCode(IntEnum): + # Авторизация + AUTH_REQ = 10 # Запрос авторизации от сервера + AUTH_ANS_LOGIN = 11 # Клиент отправляет запрос на авторизацию + AUTH_ANS_REGIS = 12 # Клиент отправляет запрос на регистрацию + AUTH_ANS_SERVE = 19 # Ответ сервера на регистрацию + + # Сетевые запросы + NET_REQ = 20 # Запрос сетевого метода + + # Файловые операции + FIL_REQ = 30 # Запрос на отправку фрагмента файла + FIL_END = 31 # Завершение передачи файла + + # Логирование + LOG_DEB = 41 # Запрос на отправку логов уровня DEBUG + LOG_INF = 42 # Запрос на отправку логов уровня INFO + LOG_WAR = 43 # Запрос на отправку логов уровня WARNING + LOG_ERR = 44 # Запрос на отправку логов уровня ERROR + + # Методы для проверки типа кода + @classmethod + def is_auth(cls, code) -> bool: + return code in { + cls.AUTH_REQ, + cls.AUTH_ANS_LOGIN, + cls.AUTH_ANS_REGIS, + cls.AUTH_ANS_SERVE, + } + + @classmethod + def is_client_auth(cls, code) -> bool: + return code in {cls.AUTH_ANS_LOGIN, cls.AUTH_ANS_REGIS} + + @classmethod + def is_net(cls, code) -> bool: + return code == cls.NET_REQ + + @classmethod + def is_file(cls, code) -> bool: + return code in {cls.FIL_REQ, cls.FIL_END} + + @classmethod + def is_log(cls, code) -> bool: + return code in {cls.LOG_DEB, cls.LOG_INF, cls.LOG_WAR, cls.LOG_ERR} diff --git a/DMBotNetwork/utils/server_db.py b/DMBotNetwork/main/utils/server_db.py similarity index 80% rename from DMBotNetwork/utils/server_db.py rename to DMBotNetwork/main/utils/server_db.py index 56f0942..2b94f4a 100644 --- a/DMBotNetwork/utils/server_db.py +++ b/DMBotNetwork/main/utils/server_db.py @@ -7,7 +7,7 @@ import bcrypt import msgpack -logger = logging.getLogger("DMBotNetwork Server db") +logger = logging.getLogger("DMBN:ServerDB") class ServerDB: @@ -63,6 +63,8 @@ async def _init_db(cls) -> None: if cls._db_path is None: raise ValueError("Database path is not set.") + cls._db_path.mkdir(parents=True, exist_ok=True) + cls._connection = await aiosqlite.connect(cls._db_path / "server.db") await cls._connection.execute(""" CREATE TABLE IF NOT EXISTS users ( @@ -150,22 +152,18 @@ async def login_user(cls, login: str, password: str) -> Optional[str]: if login not in cls._exist_user: raise ValueError(f"User '{login}' not found.") - try: - async with cls._connection.execute( - "SELECT password FROM users WHERE username = ?", (login,) - ) as cursor: - row = await cursor.fetchone() - if row is None: - raise ValueError(f"User '{login}' not found in database.") + async with cls._connection.execute( + "SELECT password FROM users WHERE username = ?", (login,) + ) as cursor: + row = await cursor.fetchone() - if not await cls._check_password(password, row[0]): - raise ValueError("Incorrect password.") + if row is None: + raise ValueError(f"User '{login}' not found in database.") - return login + if not await cls._check_password(password, row[0]): + raise ValueError("Incorrect password.") - except Exception as e: - logger.error(f"Error logging in user {login}: {e}") - return None + return login @classmethod async def add_user( @@ -185,18 +183,13 @@ async def add_user( packed_access = msgpack.packb(access) - try: - await cls._connection.execute( - "INSERT INTO users (username, password, access) VALUES (?, ?, ?)", - (username, hashed_password, packed_access), - ) - await cls._connection.commit() - cls._exist_user.add(username) - logger.info(f"User '{username}' successfully added.") - - except Exception as err: - logger.error(f"Error adding user {username}: {err}") - raise err + await cls._connection.execute( + "INSERT INTO users (username, password, access) VALUES (?, ?, ?)", + (username, hashed_password, packed_access), + ) + await cls._connection.commit() + cls._exist_user.add(username) + logger.info(f"User '{username}' successfully added.") @classmethod async def delete_user(cls, username: str) -> None: @@ -207,17 +200,13 @@ async def delete_user(cls, username: str) -> None: if username not in cls._exist_user: return - try: - await cls._connection.execute( - "DELETE FROM users WHERE username = ?", (username,) - ) - await cls._connection.commit() - - cls._access_cache.pop(username, None) - cls._exist_user.discard(username) + await cls._connection.execute( + "DELETE FROM users WHERE username = ?", (username,) + ) + await cls._connection.commit() - except Exception as e: - logger.error(f"Error deleting user {username}: {e}") + cls._access_cache.pop(username, None) + cls._exist_user.discard(username) @classmethod async def change_user_password(cls, username: str, new_password: str) -> None: @@ -230,15 +219,12 @@ async def change_user_password(cls, username: str, new_password: str) -> None: hashed_password = await cls._hash_password(new_password) - try: - await cls._connection.execute( - "UPDATE users SET password = ? WHERE username = ?", - (hashed_password, username), - ) - await cls._connection.commit() + await cls._connection.execute( + "UPDATE users SET password = ? WHERE username = ?", + (hashed_password, username), + ) - except Exception as e: - logger.error(f"Error changing password for user {username}: {e}") + await cls._connection.commit() @classmethod async def change_user_access( diff --git a/DMBotNetwork/side/__init__.py b/DMBotNetwork/side/__init__.py deleted file mode 100644 index 6e796c1..0000000 --- a/DMBotNetwork/side/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .side_client import Client -from .side_server import Server - -__all__ = ["Client", "Server"] diff --git a/DMBotNetwork/side/side_client.py b/DMBotNetwork/side/side_client.py deleted file mode 100644 index be1acdf..0000000 --- a/DMBotNetwork/side/side_client.py +++ /dev/null @@ -1,317 +0,0 @@ -import asyncio -import inspect -import logging -from asyncio import StreamReader, StreamWriter -from pathlib import Path -from typing import Any, Dict, Optional, get_type_hints - -import msgpack - -from ..utils import NCAnyType, NCLogType, NetCode - -logger = logging.getLogger("DMBotNetwork Client") - - -class Client: - _network_methods: Dict[str, Any] = {} - _ear_task: Optional[asyncio.Task] = None # lol - - _viva_alp: bool = True - _login: Optional[str] = None - _password: Optional[str] = None - _content_path: Optional[Path] = None - _temp_fold: Optional[Path] = None - _server_name: Optional[str] = None - - _is_connected: bool = False - _is_auth: bool = False - _reader: Optional[StreamReader] = None - _writer: Optional[StreamWriter] = None - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - - cls._network_methods = { - method[4:]: getattr(cls, method) - for method in dir(cls) - if callable(getattr(cls, method)) and method.startswith("net_") - } - - @classmethod - async def _call_method( - cls, - method_name: str, - **kwargs, - ) -> None: - method = cls._network_methods.get(method_name) - if method is None: - logger.error(f"Network method '{method_name}' not found.") - return - - sig = inspect.signature(method) - valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - - type_hints = get_type_hints(method) - - for arg_name, arg_value in valid_kwargs.items(): - expected_type = type_hints.get(arg_name, Any) - if not isinstance(arg_value, expected_type) and expected_type is not Any: - logger.error( - f"Type mismatch for argument '{arg_name}': expected {expected_type}, got {type(arg_value)}." - ) - return - - try: - if inspect.iscoroutinefunction(method): - await method(cls, **valid_kwargs) - - else: - method(cls, **valid_kwargs) - - except Exception as e: - logger.error(f"Error calling method '{method_name}' in {cls.__name__}: {e}") - - @classmethod - async def connect(cls, host: str, port: int) -> None: - if not all((cls._login, cls._password, cls._content_path)): - logger.warning("Login, password or content_path not set, abort connect") - return - - cls._reader, cls._writer = await asyncio.open_connection(host, port) - cls._is_connected = True - - cls._ear_task = asyncio.create_task(cls._ear()) - - @classmethod - def is_connected(cls) -> bool: - return cls._is_auth and cls._is_connected - - @classmethod - def get_auth_lp(cls) -> bool: - return cls._viva_alp - - @classmethod - def set_auth_lp(cls, value: bool) -> None: - cls._viva_alp = value - - @classmethod - def get_login(cls) -> Optional[str]: - return cls._login - - @classmethod - def set_login(cls, value: str) -> None: - cls._login = value - - @classmethod - def set_password(cls, value: str) -> None: - cls._password = value - - @classmethod - def set_up_content_path(cls, value: Path | str) -> None: - cls._content_path = Path(value) - cls._temp_fold = cls._content_path / "temp" - cls._temp_fold.mkdir(exist_ok=True, parents=True) - - @classmethod - async def disconnect(cls) -> None: - cls._is_connected = False - - if cls._writer: - cls._writer.close() - await cls._writer.wait_closed() - - if cls._ear_task: - cls._ear_task.cancel() - try: - await cls._ear_task - - except asyncio.CancelledError: - pass - - cls._writer = None - cls._reader = None - - cls._is_auth = False - - @classmethod - async def _ear(cls) -> None: - try: - while cls._is_connected: - receive_packet = await cls._receive_packet() - if not isinstance(receive_packet, dict): - logger.error("From server data type expected dict") - continue - - code = receive_packet.get("code", None) - if not code: - logger.error("From server data must has 'code' key") - continue - - if not isinstance(code, int): - logger.error("From server 'code' type expected int") - continue - - if code == NetCode.REQ_NET.value: - await cls._call_method( - receive_packet.get("type", None), **receive_packet - ) - - if code in ( - NetCode.REQ_LOG_DEBUG.value, - NetCode.REQ_LOG_INFO.value, - NetCode.REQ_LOG_WARNING.value, - NetCode.REQ_LOG_ERROR.value, - ): - cls._log(code, receive_packet) - - elif code == NetCode.REQ_AUTH.value: - cls._server_name = receive_packet.get( - "server_name", "Not_Set_Server_Name" - ) - Path(cls._content_path / cls._server_name).mkdir( # type: ignore - exist_ok=True, parents=True - ) - await cls._auth() - - elif code == NetCode.REQ_FILE_DOWNLOAD.value: - cls._download_file(receive_packet) - - elif code == NetCode.END_FILE_DOWNLOAD.value: - cls._move_file(receive_packet) - - else: - logger.error("Unknown 'code' type from server") - - except Exception as err: - logger.debug(err) - - finally: - await cls.disconnect() - - @classmethod - async def req_net(cls, type: str, **kwargs: Any) -> None: - await cls.send_packet(NetCode.REQ_NET.value, type=type, **kwargs) - - @classmethod - async def send_packet(cls, code: NCAnyType, **kwargs: Any) -> None: - payload = {"code": code, **kwargs} - - await cls._send_raw(msgpack.packb(payload)) # type: ignore - - @classmethod - async def _send_raw(cls, data: bytes) -> None: - if cls._writer is None: - raise ValueError("StreamWriter is not set") - - cls._writer.write(len(data).to_bytes(4, byteorder="big")) - await cls._writer.drain() - - cls._writer.write(data) - await cls._writer.drain() - - @classmethod - async def _receive_packet(cls) -> Any: - if not cls._reader: - return - - data_size_bytes = await cls._reader.readexactly(4) - data_size = int.from_bytes(data_size_bytes, "big") - - packed_data = await cls._reader.readexactly(data_size) - return msgpack.unpackb(packed_data) - - @classmethod - def _log(cls, code: NCLogType, receive_packet: dict) -> None: - msg = receive_packet.get("message", "Not set") - - if code == NetCode.REQ_LOG_DEBUG.value: - logger.debug(msg) - - elif code == NetCode.REQ_LOG_INFO.value: - logger.info(msg) - - elif code == NetCode.REQ_LOG_WARNING.value: - logger.warning(msg) - - elif code == NetCode.REQ_LOG_ERROR.value: - logger.error(msg) - - else: - logger.warning(f"Unknown code for log: {receive_packet}") - - @classmethod - async def _auth(cls) -> None: - if cls._viva_alp: - await cls.send_packet( - NetCode.ANSWER_AUTH_ALP.value, login=cls._login, password=cls._password - ) - - else: - await cls.send_packet( - NetCode.ANSWER_AUTH_REG.value, login=cls._login, password=cls._password - ) - - @classmethod - def _download_file(cls, receive_packet: dict) -> None: - """Данный метод, как и следующий не являются безопасными. - В случае если мы потеряем индекс мы его не найдём. - TODO: Сделать передачу файлов более стабильной. А пока что так покатит - """ - try: - file_name = receive_packet.get("file_name", None) - chunk = receive_packet.get("chunk", None) - index = receive_packet.get("index", None) - - if chunk is None or file_name is None or index is None: - return - - file_root_path: Path = cls._content_path / cls._temp_fold / cls._server_name # type: ignore - file_root_path.mkdir(exist_ok=True, parents=True) - - file_path: Path = file_root_path / f"{file_name}_{index}.tmp" - - with file_path.open("wb") as file: - file.write(chunk) - - except Exception as e: - logger.error(f"Error receiving file: {e}") - - @classmethod - def _move_file(cls, receive_packet: dict) -> None: - try: - file_name = receive_packet.get("file_name", None) - if not file_name: - logger.error("No file_name provided in receive_packet.") - return - - temp_folder_path: Path = ( - cls._content_path / cls._temp_fold / cls._server_name # type: ignore - ) - if not temp_folder_path.exists(): - logger.error(f"Temp folder {temp_folder_path} does not exist.") - return - - file_parts = sorted(temp_folder_path.glob(f"{file_name}_*.tmp")) - if not file_parts: - logger.error(f"No parts found for {file_name}.") - return - - assembled_file_path: Path = temp_folder_path / file_name - - with assembled_file_path.open("wb") as assembled_file: - for part in file_parts: - with part.open("rb") as chunk_file: - assembled_file.write(chunk_file.read()) - - for part in file_parts: - part.unlink() - - target_folder_path: Path = cls._content_path / cls._server_name # type: ignore - target_folder_path.mkdir(parents=True, exist_ok=True) - - destination_path: Path = target_folder_path / file_name - - assembled_file_path.rename(destination_path) - - except Exception as e: - logger.error(f"Error moving assembled file {file_name}: {e}") # type: ignore diff --git a/DMBotNetwork/side/side_server.py b/DMBotNetwork/side/side_server.py deleted file mode 100644 index 03d728d..0000000 --- a/DMBotNetwork/side/side_server.py +++ /dev/null @@ -1,261 +0,0 @@ -import asyncio -import inspect -import logging -from asyncio import StreamReader, StreamWriter -from pathlib import Path -from typing import Any, Dict, Optional, get_type_hints - -from ..units import ClientUnit -from ..utils import NetCode, ServerDB - -logger = logging.getLogger("DMBotNetwork.Server") - - -class Server: - _network_methods: Dict[str, Any] = {} - - _connections: Dict[str, ClientUnit] = {} - - _allow_registration: bool = True - _timeout: float = 30.0 - _server_name: Optional[str] = None - - _is_online: bool = False - _main_server: Optional[asyncio.AbstractServer] = None - - # network methods managment - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - - cls._network_methods = { - method[4:]: getattr(cls, method) - for method in dir(cls) - if callable(getattr(cls, method)) and method.startswith("net_") - } - - @classmethod - async def _call_method( - cls, - method_name: str, - cl_unit: ClientUnit, - **kwargs, - ) -> None: - method = cls._network_methods.get(method_name) - if method is None: - await cl_unit.log_error(f"Network method '{method_name}' not found.") - return - - sig = inspect.signature(method) - valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - - type_hints = get_type_hints(method) - - for arg_name, arg_value in valid_kwargs.items(): - expected_type = type_hints.get(arg_name, Any) - if not isinstance(arg_value, expected_type) and expected_type is not Any: - await cl_unit.log_error( - f"Type mismatch for argument '{arg_name}': expected {expected_type}, got {type(arg_value)}." - ) - return - - try: - if inspect.iscoroutinefunction(method): - await method(cls, **valid_kwargs) - - else: - method(cls, **valid_kwargs) - - except Exception as e: - logger.error(f"Error calling method '{method_name}' in {cls.__name__}: {e}") - - # Status control - @classmethod - async def start( - cls, - host: str = "localhost", - main_port: int = 5000, - db_file_path: Path | str = "", - base_owner_password: str = "owner_password", - timeout: float = 30.0, - allow_registration: bool = True, - base_access_flags: Dict[str, bool] = {}, - server_name: str = "dev_bot", - ) -> None: - if cls._is_online: - logger.warning("Server is already working") - return - - ServerDB.set_base_access(base_access_flags) - ServerDB.set_owner_base_password(base_owner_password) - ServerDB.set_db_path(db_file_path) - - await ServerDB.start() - - cls._allow_registration = allow_registration - cls._timeout = timeout - cls._server_name = server_name - - cls._main_server = await asyncio.start_server( - cls._main_client_handler, host, main_port - ) - - cls._is_online = True - try: - async with cls._main_server: - logger.info(f"Server setup. Host: {host}, main_port:{main_port}") - - await cls._main_server.serve_forever() - - except asyncio.CancelledError: - await cls.stop() - - except Exception as err: - logger.error(f"Error starting server: {err}") - await cls.stop() - - @classmethod - async def stop(cls) -> None: - if not cls._is_online: - logger.warning("Server is not working") - return - - await asyncio.gather( - *(cl_unit.close() for cl_unit in cls._connections.values()) - ) - - if cls._main_server: - cls._main_server.close() - await cls._main_server.wait_closed() - - cls._connections.clear() - await ServerDB.stop() - - logger.info("Server stop") - - # SetGet allow_registration & timeout - @classmethod - def get_allow_registration(cls) -> bool: - return cls._allow_registration - - @classmethod - def set_allow_registration(cls, value: bool) -> None: - cls._allow_registration = value - - @classmethod - def get_timeout(cls) -> float: - return cls._timeout - - @classmethod - def set_timeout(cls, value: float) -> None: - cls._timeout = value - - # Server client handlers - @classmethod - async def _main_client_handler( - cls, reader: StreamReader, writer: StreamWriter - ) -> None: - cl_unit = ClientUnit("init...", writer, reader) - try: - await cls._auth(cl_unit) - - except TimeoutError: - await cl_unit.log_error("Timeout while auth") - await cl_unit.close() - return - - except ValueError as err: - await cl_unit.log_error(str(err)) - await cl_unit.close() - return - - except Exception as err: - await cl_unit.log_error(f"Unexpected error: {err}") - await cl_unit.close() - return - - cls._connections[cl_unit.login] = cl_unit - - try: - while cls._is_online: - receive_packet = await cl_unit._receive_packet() - if not isinstance(receive_packet, dict): - await cl_unit.log_error("Get data type expected dict") - continue - - code = receive_packet.get("code", None) - if not code: - await cl_unit.log_error("Get data must has 'code' key") - continue - - if not isinstance(code, int): - await cl_unit.log_error("'code' type expected int") - continue - - if code == NetCode.REQ_NET.value: - await cls._call_method( - receive_packet.get("type", None), cl_unit, **receive_packet - ) - - else: - await cl_unit.log_error("Unknown 'code' type") - - except Exception as err: - await cl_unit.log_error(str(err)) - - finally: - await cl_unit.close() - del cls._connections[cl_unit.login] - - @classmethod - async def broadcast(cls, func_name: str, *args, **kwargs) -> None: - if not cls._connections: - logger.warning("No active connections to broadcast") - return - - tasks = [] - for cl_unit in cls._connections.values(): - func = getattr(cl_unit, func_name, None) - if callable(func): - tasks.append(func(*args, **kwargs)) - - else: - logger.error(f"{func_name} is not a callable method of {cl_unit}") - - if tasks: - await asyncio.gather(*tasks) - - # Auth - @classmethod - async def _auth(cls, cl_unit: ClientUnit) -> None: - await cl_unit.send_packet(NetCode.REQ_AUTH.value, server_name=cls._server_name) - receive_packet = await asyncio.wait_for(cl_unit._receive_packet(), cls._timeout) - - if not isinstance(receive_packet, dict): - raise ValueError("Get data type expected dict") - - code = receive_packet.get("code", None) - if not code: - raise ValueError("Get data must has 'code' key") - - if not isinstance(code, int): - raise ValueError("'code' type expected int") - - if "login" not in receive_packet or "password" not in receive_packet: - raise ValueError("Get data must has 'login' and 'password' keys") - - if code == NetCode.ANSWER_AUTH_ALP.value: - await ServerDB.login_user( - receive_packet["login"], receive_packet["password"] - ) - cl_unit.login = receive_packet["login"] - return - - if code == NetCode.ANSWER_AUTH_REG.value: - if not cls._allow_registration: - raise ValueError("Registration is not allowed") - - await ServerDB.add_user(receive_packet["login"], receive_packet["password"]) - cl_unit.login = receive_packet["login"] - return - - raise ValueError("Unknown 'code' type") diff --git a/DMBotNetwork/units/__init__.py b/DMBotNetwork/units/__init__.py deleted file mode 100644 index e219613..0000000 --- a/DMBotNetwork/units/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .client_unit import ClientUnit - -__all__ = ["ClientUnit"] diff --git a/DMBotNetwork/units/client_unit.py b/DMBotNetwork/units/client_unit.py deleted file mode 100644 index 5997eca..0000000 --- a/DMBotNetwork/units/client_unit.py +++ /dev/null @@ -1,121 +0,0 @@ -from asyncio import StreamReader, StreamWriter -from pathlib import Path -from typing import Any - -import msgpack - -from ..utils import NCAnyType, NetCode - - -class ClientUnit: - __slots__ = ["_login", "_writer", "_reader"] - - def __init__(self, login: str, writer: StreamWriter, reader: StreamReader) -> None: - self._login: str = login - self._writer: StreamWriter = writer - self._reader: StreamReader = reader - - def __eq__(self, value: object) -> bool: - if isinstance(value, str): - return value == self._login - - elif isinstance(value, ClientUnit): - return value._login == self._login - - return False - - def __hash__(self) -> int: - return hash(self._login) - - @property - def login(self) -> str: - return self._login - - @login.setter - def login(self, value: str) -> None: - self._login = value - - @property - def writer(self) -> StreamWriter: - return self._writer - - @property - def reader(self) -> StreamReader: - return self._reader - - # Net - async def req_net(self, type: str, **kwargs: Any) -> None: - await self.send_packet(NetCode.REQ_NET.value, type=type, **kwargs) - - # Logs - async def log_debug(self, message: str) -> None: - await self.send_packet(NetCode.REQ_LOG_DEBUG.value, message=message) - - async def log_info(self, message: str) -> None: - await self.send_packet(NetCode.REQ_LOG_INFO.value, message=message) - - async def log_warning(self, message: str) -> None: - await self.send_packet(NetCode.REQ_LOG_WARNING.value, message=message) - - async def log_error(self, message: str) -> None: - await self.send_packet(NetCode.REQ_LOG_ERROR.value, message=message) - - # Send - async def send_packet(self, code: NCAnyType, **kwargs: Any) -> None: - payload = {"code": code, **kwargs} - - await self.send_raw(msgpack.packb(payload)) # type: ignore - - async def send_raw(self, data: bytes) -> None: - if self._writer is None: - raise ValueError("StreamWriter is not set") - - self._writer.write(len(data).to_bytes(4, byteorder="big")) - await self._writer.drain() - - self._writer.write(data) - await self._writer.drain() - - # File send - async def send_file( - self, file_path: Path, file_name: str, chunk_size: int = 8192 - ) -> None: - if self._writer is None: - raise ValueError("StreamWriter is not set") - - try: - index = 0 - - with file_path.open("rb") as file: - while True: - chunk = file.read(chunk_size) - if not chunk: - await self.send_packet( - NetCode.END_FILE_DOWNLOAD.value, file_name=file_name - ) - break - - await self.send_packet( - NetCode.REQ_FILE_DOWNLOAD.value, - file_name=file_name, - chunk=chunk, - index=index, - ) - index += 1 - - except Exception as e: - await self.log_error(f"Error sending file: {e}") - - # Receive - async def _receive_packet(self) -> Any: - data_size_bytes = await self._reader.readexactly(4) - data_size = int.from_bytes(data_size_bytes, "big") - - packed_data = await self._reader.readexactly(data_size) - return msgpack.unpackb(packed_data) - - # Kill - async def close(self) -> None: - if self._writer: - self._writer.close() - await self._writer.wait_closed() diff --git a/DMBotNetwork/utils/__init__.py b/DMBotNetwork/utils/__init__.py deleted file mode 100644 index 149a8ff..0000000 --- a/DMBotNetwork/utils/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from .net_code_encode import ( - NCAnyType, - NCLogType, - NCReqAnsewerType, - NCReqNet, - NCReqType, - NetCode, -) -from .server_db import ServerDB - -__all__ = [ - "NetCode", - "ServerDB", - "NCReqType", - "NCReqNet", - "NCAnyType", - "NCLogType", - "NCReqAnsewerType", -] diff --git a/DMBotNetwork/utils/net_code_encode.py b/DMBotNetwork/utils/net_code_encode.py deleted file mode 100644 index f10cd1c..0000000 --- a/DMBotNetwork/utils/net_code_encode.py +++ /dev/null @@ -1,26 +0,0 @@ -from enum import Enum -from typing import Literal - - -class NetCode(Enum): - REQ_AUTH = 10 # Запрос аунтификации - ANSWER_AUTH_ALP = 11 # Ответ запроса аунтификации через логин пароль - ANSWER_AUTH_REG = 12 # Ответ запроса регестрации - - REQ_NET = 20 # Запрос net метода клиента - REQ_FILE_DOWNLOAD = 21 - END_FILE_DOWNLOAD = 22 - - # Лог ивенты - REQ_LOG_DEBUG = 30 - REQ_LOG_INFO = 31 - REQ_LOG_WARNING = 32 - REQ_LOG_ERROR = 33 - - -NCReqType = Literal[10] -NCReqAnsewerType = Literal[11, 12] -NCReqNet = Literal[20] -NCLogType = Literal[30, 31, 32, 33] - -NCAnyType = Literal[10, 11, 12, 20, 21, 22, 30, 31, 32, 33] diff --git a/Tests/ServerDB.py b/Tests/ServerDB.py index a70bdf4..1357f52 100644 --- a/Tests/ServerDB.py +++ b/Tests/ServerDB.py @@ -1,6 +1,6 @@ import unittest from pathlib import Path -from DMBotNetwork.utils.server_db import ServerDB +from DMBotNetwork.main.utils.server_db import ServerDB class TestServerDB(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): diff --git a/client_test.py b/client_test.py new file mode 100644 index 0000000..8946804 --- /dev/null +++ b/client_test.py @@ -0,0 +1,27 @@ +import asyncio +import logging + +from DMBotNetwork import Client + + +async def main(): + Client.setup( + login="owner", + password="owner_password", + use_registration=False, + content_path="./client_path", + ) + + await Client.connect("localhost", 5000) + await asyncio.sleep(2) + await Client.req_net_func("ping") + await asyncio.sleep(2) + await Client.disconnect() + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt index 11b9d14..814fa44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiosqlite +aiofiles bcrypt msgpack setuptools diff --git a/server_test.py b/server_test.py new file mode 100644 index 0000000..1e2006f --- /dev/null +++ b/server_test.py @@ -0,0 +1,38 @@ +import asyncio +import logging + +from DMBotNetwork import ClUnit, Server + + +class NetClassPong: + async def net_ping(self, cl_unit: ClUnit) -> None: + await cl_unit.send_log_info(f"Pong, {cl_unit.login}!") + + +async def main(): + await Server.setup_server( + server_name="test_server_name", + host="localhost", + port=5000, + db_path="./test_db_path/", + init_owner_password="owner_password", + base_access={}, + allow_registration=False, + timeout=5.0, + ) + + Server.register_methods_from_class(NetClassPong) + + await Server.start() + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + + logger1 = logging.getLogger("aiosqlite") + logger1.propagate = False + + asyncio.run(main()) diff --git a/setup.py b/setup.py index 64027d0..fb38db9 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,9 @@ setup( name="DMBotNetwork", - version="0.1.5", + version="0.2.0", packages=find_packages(), - install_requires=["aiosqlite", "bcrypt", "msgpack"], + install_requires=["aiosqlite", "aiofiles", "bcrypt", "msgpack"], author="Angels And Demons dev team", author_email="dm.bot.adm@gmail.com", description="Нэткод для проектов DMBot", diff --git a/test.py b/test.py new file mode 100644 index 0000000..e69de29