From 4a97794e2cb6e5364f8dcb8018f305e233468188 Mon Sep 17 00:00:00 2001 From: KyMAN <0kyman0@gmail.com> Date: Thu, 28 May 2026 07:24:34 +0200 Subject: [PATCH] #wip: WebSockets building. --- JSON/AnP.settings.json | 13 +- Public/ecma/Drivers/WebSocketsDriver.ecma.js | 76 +++++++---- Python/Abstracts/WebSocketsServerAbstract.py | 26 ++++ Python/Application/AnP.py | 5 + Python/Application/Event.py | 5 +- Python/Drivers/OllamaDriver.py | 2 +- Python/Drivers/WebSocketServerDriver.py | 71 +++++++--- Python/Interfaces/Application/AnPInterface.py | 2 + .../WebSocketsServerManagerInterface.py | 43 ++++++ Python/Managers/ModelsManager.py | 7 +- Python/Managers/WebSocketsServerManager.py | 128 ++++++++++++++++++ Python/Models/RequestModel.py | 1 + Python/Utils/Common.py | 55 +++++++- Python/run.py | 4 +- 14 files changed, 380 insertions(+), 58 deletions(-) create mode 100644 Python/Abstracts/WebSocketsServerAbstract.py create mode 100644 Python/Interfaces/Managers/WebSocketsServerManagerInterface.py create mode 100644 Python/Managers/WebSocketsServerManager.py diff --git a/JSON/AnP.settings.json b/JSON/AnP.settings.json index ebe075d..121e39c 100644 --- a/JSON/AnP.settings.json +++ b/JSON/AnP.settings.json @@ -63,10 +63,15 @@ "http_server_port" : 18000, "AnP_HTTPServer_end" : null, - "AnP_WebSocketServer_start" : null, - "web_socket_server_host" : "", - "web_socket_server_port" : 18765, - "AnP_WebSocketServer_end" : null, + "AnP_WebSocketsServerManager_start" : null, + "default_web_sockets_server" : { + "anp" : { + "type" : "WebSocketServerDriver", + "host" : "localhost", + "port" : 18765 + } + }, + "AnP_WebSocketsServerManager_end" : null, "AnP_TitlesManager_start" : null, "default_titles_files" : [ diff --git a/Public/ecma/Drivers/WebSocketsDriver.ecma.js b/Public/ecma/Drivers/WebSocketsDriver.ecma.js index db03388..cf84002 100644 --- a/Public/ecma/Drivers/WebSocketsDriver.ecma.js +++ b/Public/ecma/Drivers/WebSocketsDriver.ecma.js @@ -1,6 +1,7 @@ "use strict"; import {Common} from "../Utils/Common.ecma.js"; +import {Event} from "../Application/Event.ecma.js"; /** * @class WebSocketsDriver @@ -12,6 +13,12 @@ import {Common} from "../Utils/Common.ecma.js"; */ export const WebSocketsDriver = (function(){ + /** + * @callback continue_callback + * @param {boolean} ok + * @return {void} + */ + /** * @constructs WebSocketsDriver * @param {!(Object.|Array.)} inputs @@ -23,19 +30,39 @@ export const WebSocketsDriver = (function(){ /** @type {WebSocketsDriver} */ const self = this; - let server = null, + /** @type {WebSocket|null} */ + let client = null, + /** @type {string|null} */ url = null, + /** @type {boolean} */ started = false; + /** @type {Event} */ this.on_open = new Event(); + /** @type {Event} */ this.on_message = new Event(); + /** @type {Event} */ this.on_error = new Event(); + /** @type {Event} */ this.on_close = new Event(); - const constructor = () => {}; + /** + * @returns {void} + * @access private + */ + const constructor = () => { + url = Common.get_string(inputs.url, "ws://localhost:8080"); + }; + + /** + * @param {?continue_callback} callback + * @returns {boolean} + * @access public + */ this.start = (callback = null) => { + /** @type {continue_callback} */ const end = ok => Common.execute(callback, ok); if(started){ @@ -44,27 +71,24 @@ export const WebSocketsDriver = (function(){ }; started = true; - server = new WebSocket(`ws://${host}:${port}`); + client = new WebSocket(url); - server.onopen = on_open; - - server.onmessage = event => { - self.on_message.trigger(event); - }; - - server.onerror = event => { - self.on_error.trigger(event); - }; - - server.onclose = event => { - self.on_close.trigger(event); - }; + client.onopen = on_open; + client.onmessage = on_message + client.onerror = on_error; + client.onclose = on_close; return true; }; + /** + * @param {?continue_callback} callback + * @returns {boolean} + * @access public + */ this.close = (callback = null) => { + /** @type {continue_callback} */ const end = ok => Common.execute(callback, ok); if(!started){ @@ -73,34 +97,30 @@ export const WebSocketsDriver = (function(){ }; started = false; - server.close(); - server = null; + client.close(); + client = null; return true; }; - const open = event => { + const on_open = event => { console.log(["client", event]); }; - const new_message = event => { + const on_message = event => { console.log(["message", event]); }; - const error = event => { + const on_error = event => { console.log(["error", event]); }; - const close = event => { + const on_close = event => { console.log(["close", event]); }; - this.send = (ids, message) => { - Common.get_array(ids).forEach(id => {}); - }; - - this.send_to_all = message => { - self.send(Object.keys(clients), message); + this.send = message => { + client.send(message); }; constructor(); diff --git a/Python/Abstracts/WebSocketsServerAbstract.py b/Python/Abstracts/WebSocketsServerAbstract.py new file mode 100644 index 0000000..cd0866a --- /dev/null +++ b/Python/Abstracts/WebSocketsServerAbstract.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self, Optional, Sequence +from abc import ABC, abstractmethod +from Application.Event import Event + +class WebSocketsServerAbstract(ABC): + + def __init__(self:Self, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None) -> None: + self.on_new_client:Event = Event() + self.on_message:Event = Event() + self.on_close:Event = Event() + self.on_error:Event = Event() + + @abstractmethod + def start(self:Self) -> None:pass + + @abstractmethod + def close(self:Self) -> None:pass + + @abstractmethod + def close_client(self:Self, id:int) -> None:pass + + @abstractmethod + def send(self:Self, data:Any|None, ids:Optional[int|Sequence[int]] = None) -> None:pass \ No newline at end of file diff --git a/Python/Application/AnP.py b/Python/Application/AnP.py index beeee7b..8a3daa2 100644 --- a/Python/Application/AnP.py +++ b/Python/Application/AnP.py @@ -14,6 +14,7 @@ from Managers.ControllersManager import ControllersManager from Managers.DispatchesManager import DispatchesManager from Managers.IndexesManager import IndexesManager from Managers.RoutesManager import RoutesManager +from Managers.WebSocketsServerManager import WebSocketsServerManager from Drivers.HTTPDriver import HTTPDriver from Utils.Common import Common from Utils.Patterns import RE @@ -45,6 +46,7 @@ class AnP: self.dispatches:DispatchesManager = DispatchesManager(self) self.indexes:IndexesManager = IndexesManager(self) self.routes:RoutesManager = RoutesManager(self) + self.web_sockets_servers:WebSocketsServerManager = WebSocketsServerManager(self) self.http_server:HTTPDriver = HTTPDriver(self) def update(self:Self) -> None: @@ -58,6 +60,7 @@ class AnP: self.dispatches.update() self.indexes.update() self.routes.update() + self.web_sockets_servers.update() self.http_server.update() def reset(self:Self) -> None: @@ -71,10 +74,12 @@ class AnP: self.dispatches.reset() self.indexes.reset() self.routes.reset() + self.web_sockets_servers.reset() self.http_server.reset() def close(self:Self) -> None: self.__working = False + self.web_sockets_servers.close() self.http_server.close() def working(self:Self) -> bool: diff --git a/Python/Application/Event.py b/Python/Application/Event.py index 195be72..56280e5 100644 --- a/Python/Application/Event.py +++ b/Python/Application/Event.py @@ -33,4 +33,7 @@ class Event: def remove(self:Self, i:int) -> None: if i in self.__events: - del self.__events[i] \ No newline at end of file + del self.__events[i] + + def clean(self:Self) -> None: + self.__events = {} \ No newline at end of file diff --git a/Python/Drivers/OllamaDriver.py b/Python/Drivers/OllamaDriver.py index 08cba74..52bb654 100644 --- a/Python/Drivers/OllamaDriver.py +++ b/Python/Drivers/OllamaDriver.py @@ -3,7 +3,7 @@ from typing import Any, Optional, Self, Sequence from Interfaces.Application.AnPInterface import AnPInterface - +from requests import post as Post class OllamaDriver: diff --git a/Python/Drivers/WebSocketServerDriver.py b/Python/Drivers/WebSocketServerDriver.py index bf9c8ea..fa6877b 100644 --- a/Python/Drivers/WebSocketServerDriver.py +++ b/Python/Drivers/WebSocketServerDriver.py @@ -1,28 +1,37 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from threading import Thread from typing import Any, Self, Sequence, Optional +from Abstracts.WebSocketsServerAbstract import WebSocketsServerAbstract +from Abstracts.ModelAbstract import ModelAbstract from websockets.sync.server import serve as server_serve -from websockets import ServerConnection as WebSocketServer, ClientConnection as WebSocketClient -from Application.Event import Event +from websockets import Server as WebSocketServer, ClientConnection as WebSocketClient from Interfaces.Application.AnPInterface import AnPInterface +from Utils.Checks import Check -class WebSocketServerDriver: +class WebSocketServerDriver(WebSocketsServerAbstract, ModelAbstract): def __init__(self:Self, anp:AnPInterface, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None) -> None: self.anp:AnPInterface = anp - self.on_new_client:Event = Event() - self.on_message:Event = Event() - self.on_close:Event = Event() - self.on_error:Event = Event() + super().__init__(inputs) self.__server:WebSocketServer - self.__clients:dict[str, WebSocketClient] = {} + self.__clients:dict[int, WebSocketClient] = {} self.__host:str = anp.settings.get(("web_socket_server_host", "host"), inputs, "") self.__port:int = anp.settings.get(("web_socket_server_port", "port"), inputs, 8765) + self.__client_i:int = 0 + self.__thread:Thread = None - with server_serve(self.__handler, self.__host, self.__port) as self.__server: - self.__server.serve_forever() + anp.settings.get(("web_socket_server_autostart", "autostart"), inputs, True) and self.start() + + def __run(self:Self) -> None: + self.__server = server_serve(self.__handler, self.__host, self.__port) + self.__server.serve_forever() + + def start(self:Self) -> None: + self.__thread = Thread(target = self.__run) + self.__thread.start() def close(self:Self) -> None: @@ -31,14 +40,14 @@ class WebSocketServerDriver: for id in tuple(self.__clients.keys()): self.close_client(id) - self.__server.close() + self.__server.shutdown() - def close_client(self:Self, id:str, show_exception:bool = True) -> None: + def close_client(self:Self, id:int) -> None: if id in self.__clients: try: self.__clients[id].close() except Exception as exception: - show_exception and self.anp.exception(exception, "web_socket_server_client_close_exception", { + self.anp.exception(exception, "web_socket_server_client_close_exception", { "client": id, "port": self.__port, "host": self.__host @@ -47,9 +56,12 @@ class WebSocketServerDriver: def __handler(self:Self, client:WebSocketClient) -> None: - id:str = str(id(client)) + id:int = self.__client_i + + self.__client_i += 1 + self.__clients[id] = client - self.on_new_client.execute(client, id) + self.on_new_client.execute(id) self.anp.print("info", "web_socket_server_client_connected", { "client": id, @@ -64,19 +76,38 @@ class WebSocketServerDriver: message:str = client.recv() if message is None: break - self.on_message.execute(client, message) + self.on_message.execute(id, message) except Exception as exception: self.anp.exception(exception, "web_socket_server_client_exception", { "client": id, "port": self.__port, "host": self.__host }) - self.on_error.execute(client, exception) + self.on_error.execute(id, exception) finally: - self.close_client(id, False) - self.on_close.execute(client) + self.close_client(id) + self.on_close.execute(id) self.anp.print("info", "web_socket_server_client_disconnected", { "client": id, "port": self.__port, "host": self.__host - }) \ No newline at end of file + }) + + def send(self:Self, data:str, ids:int|Sequence[int]) -> bool: + + success:bool = True + id:int + + for id in ids if Check.is_array(ids) else [ids]: + if id in self.__clients: + try: + self.__clients[id].send(data) + except Exception as exception: + self.anp.exception(exception, "web_socket_server_client_send_exception", { + "client": id, + "port": self.__port, + "host": self.__host + }) + success = False + + return success \ No newline at end of file diff --git a/Python/Interfaces/Application/AnPInterface.py b/Python/Interfaces/Application/AnPInterface.py index 09bd8ff..57c84f4 100644 --- a/Python/Interfaces/Application/AnPInterface.py +++ b/Python/Interfaces/Application/AnPInterface.py @@ -12,6 +12,7 @@ from Interfaces.Managers.ControllersManagerInterface import ControllersManagerIn from Interfaces.Managers.DispatchesManagerInterface import DispatchesManagerInterface from Interfaces.Managers.IndexesManagerInterface import IndexesManagerInterface from Interfaces.Managers.RoutesManagerInterface import RoutesManagerInterface +from Interfaces.Managers.WebSocketsServerManagerInterface import WebSocketsServerManagerInterface class AnPInterface(ABC): @@ -25,6 +26,7 @@ class AnPInterface(ABC): self.dispatches:DispatchesManagerInterface = None self.indexes:IndexesManagerInterface = None self.routes:RoutesManagerInterface = None + self.web_sockets_servers:WebSocketsServerManagerInterface = None @abstractmethod def update(self:Self) -> None:pass diff --git a/Python/Interfaces/Managers/WebSocketsServerManagerInterface.py b/Python/Interfaces/Managers/WebSocketsServerManagerInterface.py new file mode 100644 index 0000000..05979aa --- /dev/null +++ b/Python/Interfaces/Managers/WebSocketsServerManagerInterface.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self, Optional, Sequence +from abc import ABC, abstractmethod +from Abstracts.WebSocketsServerAbstract import WebSocketsServerAbstract +from Application.Event import Event + +class WebSocketsServerManagerInterface(ABC): + + def __init__(self:Self, anp:Any, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None) -> None: + self.on_new_client:Event = None + self.on_message:Event = None + self.on_close:Event = None + self.on_error:Event = None + + @abstractmethod + def update(self:Self) -> None:pass + + @abstractmethod + def reset(self:Self) -> None:pass + + @abstractmethod + def close(self:Self) -> None:pass + + @abstractmethod + def add(self:Self, inputs:Any|None, overwrite:bool = False) -> None:pass + + @abstractmethod + def remove(self:Self, names:str|Sequence[str]) -> None:pass + + @abstractmethod + def get(self:Self, name:str) -> WebSocketsServerAbstract|None:pass + + @abstractmethod + def send(self:Self, + name:str, + controller:str, + method:str, + data:Optional[Any] = None, + clients:Optional[int|Sequence[int]] = None, + code:int = 200 + ) -> None:pass \ No newline at end of file diff --git a/Python/Managers/ModelsManager.py b/Python/Managers/ModelsManager.py index 28fa7a0..8157149 100644 --- a/Python/Managers/ModelsManager.py +++ b/Python/Managers/ModelsManager.py @@ -35,7 +35,10 @@ class ModelsManager: key:str for key in Common.get_keys(keys): - if key in self.__models and isinstance(self.__models[key], Type): + if key in self.__models and ( + isinstance(self.__models[key], Type) or + issubclass(self.__models[key], Type) + ): return self.__models[key] return None @@ -47,7 +50,7 @@ class ModelsManager: for key, model in subinputs.items(): if Common.is_mark_key(key) and model is None: continue - if isinstance(model, ModelAbstract) and ( + if issubclass(model, ModelAbstract) and ( overwrite or key not in self.__models ): self.__models[key] = model \ No newline at end of file diff --git a/Python/Managers/WebSocketsServerManager.py b/Python/Managers/WebSocketsServerManager.py new file mode 100644 index 0000000..db49f5c --- /dev/null +++ b/Python/Managers/WebSocketsServerManager.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Self, Optional, Sequence +from Interfaces.Application.AnPInterface import AnPInterface +from Abstracts.WebSocketsServerAbstract import WebSocketsServerAbstract +from Application.Event import Event +from Models.RequestModel import RequestModel +from Utils.Checks import Check +from Utils.Common import Common + +class WebSocketsServerManager: + + def __init__(self:Self, anp:AnPInterface, inputs:Optional[dict[str, Any|None]|Sequence[Any|None]] = None) -> None: + + self.anp:AnPInterface = anp + self.__web_sockets:dict[str, WebSocketsServerAbstract] = {} + self.on_new_client:Event = Event() + self.on_message:Event = Event() + self.on_close:Event = Event() + self.on_error:Event = Event() + + self.on_message.add(self.__receive) + + self.update() + + def update(self:Self) -> None: + + key:str + + for key in ("default_web_sockets_server_files", "web_sockets_server_files", "default_web_sockets_server", "web_sockets_server"): + self.add(self.anp.settings.get(key), True) + + def reset(self:Self) -> None: + + self.__web_sockets = {} + + self.update() + + def close(self:Self) -> None: + for web_socket in self.__web_sockets.values(): + web_socket.close() + self.__web_sockets = {} + + def __set(self:Self, name:str, web_socket:WebSocketsServerAbstract) -> None: + + self.__web_sockets[name] = web_socket + + web_socket.on_new_client.add(lambda id:self.on_new_client(web_socket, id, name)) + web_socket.on_message.add(lambda id, message:self.on_message(web_socket, id, message, name)) + web_socket.on_close.add(lambda id:self.on_close(web_socket, id, name)) + web_socket.on_error.add(lambda id, exception:self.on_error(web_socket, id, exception, name)) + + def add(self:Self, inputs:Any|None, overwrite:bool = False) -> None: + + subinputs:dict[str, Any|None] + + for subinputs in Common.load_json(inputs, True): + + key:str + value:Any|None + + for key, value in subinputs.items(): + if isinstance(value, WebSocketsServerAbstract): + if overwrite or key not in self.__web_sockets: + self.__set(key, value) + elif Check.is_dictionary(value): + if overwrite or key not in self.__web_sockets: + + _type:str|None = Common.get_value("type", value) + + if _type is None: + continue + + Class:type[WebSocketsServerAbstract] = self.anp.models.get(WebSocketsServerAbstract, _type) + + Class and issubclass(Class, WebSocketsServerAbstract) and self.__set(key, Class(self.anp, value)) + + def remove(self:Self, names:str|Sequence[str]) -> None: + for name in names if Check.is_array(names) else [names]: + if name in self.__web_sockets: + try: + self.__web_sockets[name].close() + del self.__web_sockets[name] + except Exception as exception: + self.anp.exception(exception, "web_socket_server_close_exception", {"name": name}) + + def get(self:Self, name:str) -> WebSocketsServerAbstract|None: + return self.__web_sockets.get(name) + + def send(self:Self, + name:str, + controller:str, + method:str, + data:Optional[Any] = None, + clients:Optional[int|Sequence[int]] = None, + code:int = 200 + ) -> None: + if name in self.__web_sockets: + try: + self.__web_sockets[name].send(Common.data_encode({ + "ok" : code >= 200 and code < 300, + "code" : code, + "controller" : controller, + "method" : method, + "data" : data + }), clients) + except Exception as exception: + self.anp.exception(exception, "web_socket_server_send_exception", {"name": name}) + + def __receive(self:Self, web_socket:WebSocketsServerAbstract, client:int, raw_data:str, name:str) -> None: + + data:dict[str, Any|None] = Common.data_decode(raw_data) + + if "controller" in data and "method" in data: + + request:RequestModel = RequestModel() + + request.data = data.get("data", None) + request.variables["web_socket"] = web_socket + request.variables["client_id"] = client + request.variables["web_socket_name"] = name + + self.anp.controllers.execute( + data["controller"], + data["method"], + request + ) \ No newline at end of file diff --git a/Python/Models/RequestModel.py b/Python/Models/RequestModel.py index b479082..d801038 100644 --- a/Python/Models/RequestModel.py +++ b/Python/Models/RequestModel.py @@ -21,6 +21,7 @@ class RequestModel: self.response_code:int = 0 self.response_headers:dict[str, Any|None] = {} self.callback:Callable[[RequestModel, str|bytes|None], None]|None = None + self.data:Any|None = None def get(self:Self, key:str|Sequence[str], default:Optional[Any] = None) -> Any|None: return Common.get_value(key, ( diff --git a/Python/Utils/Common.py b/Python/Utils/Common.py index e8e59b2..e602ce8 100644 --- a/Python/Utils/Common.py +++ b/Python/Utils/Common.py @@ -8,6 +8,8 @@ from json import loads as json_decode from io import FileIO from mimetypes import guess_type as get_mime_by_extension from inspect import FrameInfo, stack as get_stack +from json import dumps as json_encode, loads as json_decode +from base64 import b64encode as base64_encode, b64decode as base64_decode from Utils.Checks import Check from Utils.Patterns import RE @@ -271,4 +273,55 @@ class Common: @staticmethod def is_file(path:str) -> bool: - return is_file(path) \ No newline at end of file + return is_file(path) + + @staticmethod + def json_encode(data:dict[str, Any|None]|Sequence[Any|None]) -> str|None: + try: + return json_encode(data) + except Exception as exception: + pass + return None + + @staticmethod + def json_decode(data:str) -> dict[str, Any|None]|Sequence[Any|None]|None: + try: + return json_decode(data) + except Exception as exception: + pass + return None + + @staticmethod + def base64_encode(data:bytes) -> str|None: + try: + return base64_encode(data).decode("utf-8") + except Exception as exception: + pass + return None + + @staticmethod + def base64_decode(data:str) -> bytes|None: + try: + return base64_decode(data.encode("utf-8")) + except Exception as exception: + pass + return None + + @classmethod + def data_encode(cls:type[Self], data:Any|None) -> str|None: + if Check.is_json_data(data): + data = cls.json_encode(data) + elif not Check.is_string(data): + data = str(data) + return cls.base64_encode(data.encode("utf-8")) + + @classmethod + def data_decode(cls:type[Self], data:str) -> Any|None: + if Check.is_string(data): + data = cls.base64_decode(data) + if Check.is_string(data): + try: + return cls.json_decode(data) + except Exception as exception: + return data + return None \ No newline at end of file diff --git a/Python/run.py b/Python/run.py index c604fe1..8637687 100644 --- a/Python/run.py +++ b/Python/run.py @@ -4,10 +4,12 @@ from typing import Any from Application.AnP import AnP from Controllers.AIController import AIController +from Drivers.WebSocketServerDriver import WebSocketServerDriver inputs:dict[str, dict[str, Any|None]] = { "default_models" : { - "AIController" : AIController + "AIController" : AIController, + "WebSocketServerDriver" : WebSocketServerDriver, } }