From 126f8901e2887cef8da63765df0bf3954e6eaa14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 17:51:43 +0100 Subject: [PATCH 1/8] removed websocket server from pype modules --- pype/modules/websocket_server/__init__.py | 10 - .../websocket_server/hosts/__init__.py | 0 .../websocket_server/hosts/aftereffects.py | 64 --- .../websocket_server/hosts/external_app_1.py | 47 -- .../websocket_server/hosts/photoshop.py | 64 --- .../stubs/aftereffects_server_stub.py | 502 ------------------ .../stubs/photoshop_server_stub.py | 311 ----------- .../test_client/wsrpc_client.html | 179 ------- .../test_client/wsrpc_client.py | 34 -- .../websocket_server/websocket_server.py | 265 --------- 10 files changed, 1476 deletions(-) delete mode 100644 pype/modules/websocket_server/__init__.py delete mode 100644 pype/modules/websocket_server/hosts/__init__.py delete mode 100644 pype/modules/websocket_server/hosts/aftereffects.py delete mode 100644 pype/modules/websocket_server/hosts/external_app_1.py delete mode 100644 pype/modules/websocket_server/hosts/photoshop.py delete mode 100644 pype/modules/websocket_server/stubs/aftereffects_server_stub.py delete mode 100644 pype/modules/websocket_server/stubs/photoshop_server_stub.py delete mode 100644 pype/modules/websocket_server/test_client/wsrpc_client.html delete mode 100644 pype/modules/websocket_server/test_client/wsrpc_client.py delete mode 100644 pype/modules/websocket_server/websocket_server.py diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py deleted file mode 100644 index 0f6888585f..0000000000 --- a/pype/modules/websocket_server/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .websocket_server import ( - WebsocketModule, - WebSocketServer -) - - -__all__ = ( - "WebsocketModule", - "WebSocketServer" -) diff --git a/pype/modules/websocket_server/hosts/__init__.py b/pype/modules/websocket_server/hosts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pype/modules/websocket_server/hosts/aftereffects.py b/pype/modules/websocket_server/hosts/aftereffects.py deleted file mode 100644 index 14d2c04338..0000000000 --- a/pype/modules/websocket_server/hosts/aftereffects.py +++ /dev/null @@ -1,64 +0,0 @@ -from pype.api import Logger -from wsrpc_aiohttp import WebSocketRoute -import functools - -import avalon.aftereffects as aftereffects - -log = Logger().get_logger("WebsocketServer") - - -class AfterEffects(WebSocketRoute): - """ - One route, mimicking external application (like Harmony, etc). - All functions could be called from client. - 'do_notify' function calls function on the client - mimicking - notification after long running job on the server or similar - """ - instance = None - - def init(self, **kwargs): - # Python __init__ must be return "self". - # This method might return anything. - log.debug("someone called AfterEffects route") - self.instance = self - return kwargs - - # server functions - async def ping(self): - log.debug("someone called AfterEffects route ping") - - # This method calls function on the client side - # client functions - - async def read(self): - log.debug("aftereffects.read client calls server server calls " - "aftereffects client") - return await self.socket.call('aftereffects.read') - - # panel routes for tools - async def creator_route(self): - self._tool_route("creator") - - async def workfiles_route(self): - self._tool_route("workfiles") - - async def loader_route(self): - self._tool_route("loader") - - async def publish_route(self): - self._tool_route("publish") - - async def sceneinventory_route(self): - self._tool_route("sceneinventory") - - async def projectmanager_route(self): - self._tool_route("projectmanager") - - def _tool_route(self, tool_name): - """The address accessed when clicking on the buttons.""" - partial_method = functools.partial(aftereffects.show, tool_name) - - aftereffects.execute_in_main_thread(partial_method) - - # Required return statement. - return "nothing" diff --git a/pype/modules/websocket_server/hosts/external_app_1.py b/pype/modules/websocket_server/hosts/external_app_1.py deleted file mode 100644 index 9352787175..0000000000 --- a/pype/modules/websocket_server/hosts/external_app_1.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio - -from pype.api import Logger -from wsrpc_aiohttp import WebSocketRoute - -log = Logger().get_logger("WebsocketServer") - - -class ExternalApp1(WebSocketRoute): - """ - One route, mimicking external application (like Harmony, etc). - All functions could be called from client. - 'do_notify' function calls function on the client - mimicking - notification after long running job on the server or similar - """ - - def init(self, **kwargs): - # Python __init__ must be return "self". - # This method might return anything. - log.debug("someone called ExternalApp1 route") - return kwargs - - async def server_function_one(self): - log.info('In function one') - - async def server_function_two(self): - log.info('In function two') - return 'function two' - - async def server_function_three(self): - log.info('In function three') - asyncio.ensure_future(self.do_notify()) - return '{"message":"function tree"}' - - async def server_function_four(self, *args, **kwargs): - log.info('In function four args {} kwargs {}'.format(args, kwargs)) - ret = dict(**kwargs) - ret["message"] = "function four received arguments" - return str(ret) - - # This method calls function on the client side - async def do_notify(self): - import time - time.sleep(5) - log.info('Calling function on server after delay') - awesome = 'Somebody server_function_three method!' - await self.socket.call('notify', result=awesome) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py deleted file mode 100644 index cdfb9413a0..0000000000 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ /dev/null @@ -1,64 +0,0 @@ -from pype.api import Logger -from wsrpc_aiohttp import WebSocketRoute -import functools - -import avalon.photoshop as photoshop - -log = Logger().get_logger("WebsocketServer") - - -class Photoshop(WebSocketRoute): - """ - One route, mimicking external application (like Harmony, etc). - All functions could be called from client. - 'do_notify' function calls function on the client - mimicking - notification after long running job on the server or similar - """ - instance = None - - def init(self, **kwargs): - # Python __init__ must be return "self". - # This method might return anything. - log.debug("someone called Photoshop route") - self.instance = self - return kwargs - - # server functions - async def ping(self): - log.debug("someone called Photoshop route ping") - - # This method calls function on the client side - # client functions - - async def read(self): - log.debug("photoshop.read client calls server server calls " - "Photo client") - return await self.socket.call('Photoshop.read') - - # panel routes for tools - async def creator_route(self): - self._tool_route("creator") - - async def workfiles_route(self): - self._tool_route("workfiles") - - async def loader_route(self): - self._tool_route("loader") - - async def publish_route(self): - self._tool_route("publish") - - async def sceneinventory_route(self): - self._tool_route("sceneinventory") - - async def projectmanager_route(self): - self._tool_route("projectmanager") - - def _tool_route(self, tool_name): - """The address accessed when clicking on the buttons.""" - partial_method = functools.partial(photoshop.show, tool_name) - - photoshop.execute_in_main_thread(partial_method) - - # Required return statement. - return "nothing" diff --git a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py deleted file mode 100644 index 9449d0b378..0000000000 --- a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py +++ /dev/null @@ -1,502 +0,0 @@ -from pype.modules.websocket_server import WebSocketServer -""" - Stub handling connection from server to client. - Used anywhere solution is calling client methods. -""" -import json -import attr - -import logging -log = logging.getLogger(__name__) - - -@attr.s -class AEItem(object): - """ - Object denoting Item in AE. Each item is created in AE by any Loader, - but contains same fields, which are being used in later processing. - """ - # metadata - id = attr.ib() # id created by AE, could be used for querying - name = attr.ib() # name of item - item_type = attr.ib(default=None) # item type (footage, folder, comp) - # all imported elements, single for - # regular image, array for Backgrounds - members = attr.ib(factory=list) - workAreaStart = attr.ib(default=None) - workAreaDuration = attr.ib(default=None) - frameRate = attr.ib(default=None) - file_name = attr.ib(default=None) - - -class AfterEffectsServerStub(): - """ - Stub for calling function on client (Photoshop js) side. - Expects that client is already connected (started when avalon menu - is opened). - 'self.websocketserver.call' is used as async wrapper - """ - - def __init__(self): - self.websocketserver = WebSocketServer.get_instance() - self.client = self.websocketserver.get_client() - - def open(self, path): - """ - Open file located at 'path' (local). - Args: - path(string): file path locally - Returns: None - """ - self.websocketserver.call(self.client.call - ('AfterEffects.open', path=path) - ) - - def get_metadata(self): - """ - Get complete stored JSON with metadata from AE.Metadata.Label - field. - - It contains containers loaded by any Loader OR instances creted - by Creator. - - Returns: - (dict) - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_metadata') - ) - try: - metadata = json.loads(res) - except json.decoder.JSONDecodeError: - raise ValueError("Unparsable metadata {}".format(res)) - return metadata or [] - - def read(self, item, layers_meta=None): - """ - Parses item metadata from Label field of active document. - Used as filter to pick metadata for specific 'item' only. - - Args: - item (AEItem): pulled info from AE - layers_meta (dict): full list from Headline - (load and inject for better performance in loops) - Returns: - (dict): - """ - if layers_meta is None: - layers_meta = self.get_metadata() - - for item_meta in layers_meta: - if 'container' in item_meta.get('id') and \ - str(item.id) == str(item_meta.get('members')[0]): - return item_meta - - log.debug("Couldn't find layer metadata") - - def imprint(self, item, data, all_items=None, items_meta=None): - """ - Save item metadata to Label field of metadata of active document - Args: - item (AEItem): - data(string): json representation for single layer - all_items (list of item): for performance, could be - injected for usage in loop, if not, single call will be - triggered - items_meta(string): json representation from Headline - (for performance - provide only if imprint is in - loop - value should be same) - Returns: None - """ - if not items_meta: - items_meta = self.get_metadata() - - result_meta = [] - # fix existing - is_new = True - - for item_meta in items_meta: - if item_meta.get('members') \ - and str(item.id) == str(item_meta.get('members')[0]): - is_new = False - if data: - item_meta.update(data) - result_meta.append(item_meta) - else: - result_meta.append(item_meta) - - if is_new: - result_meta.append(data) - - # Ensure only valid ids are stored. - if not all_items: - # loaders create FootageItem now - all_items = self.get_items(comps=True, - folders=True, - footages=True) - item_ids = [int(item.id) for item in all_items] - cleaned_data = [] - for meta in result_meta: - # for creation of instance OR loaded container - if 'instance' in meta.get('id') or \ - int(meta.get('members')[0]) in item_ids: - cleaned_data.append(meta) - - payload = json.dumps(cleaned_data, indent=4) - - self.websocketserver.call(self.client.call - ('AfterEffects.imprint', payload=payload)) - - def get_active_document_full_name(self): - """ - Returns just a name of active document via ws call - Returns(string): file name - """ - res = self.websocketserver.call(self.client.call( - 'AfterEffects.get_active_document_full_name')) - - return res - - def get_active_document_name(self): - """ - Returns just a name of active document via ws call - Returns(string): file name - """ - res = self.websocketserver.call(self.client.call( - 'AfterEffects.get_active_document_name')) - - return res - - def get_items(self, comps, folders=False, footages=False): - """ - Get all items from Project panel according to arguments. - There are multiple different types: - CompItem (could have multiple layers - source for Creator, - will be rendered) - FolderItem (collection type, currently used for Background - loading) - FootageItem (imported file - created by Loader) - Args: - comps (bool): return CompItems - folders (bool): return FolderItem - footages (bool: return FootageItem - - Returns: - (list) of namedtuples - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_items', - comps=comps, - folders=folders, - footages=footages) - ) - return self._to_records(res) - - def get_selected_items(self, comps, folders=False, footages=False): - """ - Same as get_items but using selected items only - Args: - comps (bool): return CompItems - folders (bool): return FolderItem - footages (bool: return FootageItem - - Returns: - (list) of namedtuples - - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_selected_items', - comps=comps, - folders=folders, - footages=footages) - ) - return self._to_records(res) - - def import_file(self, path, item_name, import_options=None): - """ - Imports file as a FootageItem. Used in Loader - Args: - path (string): absolute path for asset file - item_name (string): label for created FootageItem - import_options (dict): different files (img vs psd) need different - config - - """ - res = self.websocketserver.call(self.client.call( - 'AfterEffects.import_file', - path=path, - item_name=item_name, - import_options=import_options) - ) - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Couldn't import {} file".format(path)) - - def replace_item(self, item, path, item_name): - """ Replace FootageItem with new file - - Args: - item (dict): - path (string):absolute path - item_name (string): label on item in Project list - - """ - self.websocketserver.call(self.client.call - ('AfterEffects.replace_item', - item_id=item.id, - path=path, item_name=item_name)) - - def rename_item(self, item, item_name): - """ Replace item with item_name - - Args: - item (dict): - item_name (string): label on item in Project list - - """ - self.websocketserver.call(self.client.call - ('AfterEffects.rename_item', - item_id=item.id, - item_name=item_name)) - - def delete_item(self, item_id): - """ Deletes *Item in a file - Args: - item_id (int): - - """ - self.websocketserver.call(self.client.call - ('AfterEffects.delete_item', - item_id=item_id - )) - - def is_saved(self): - # TODO - return True - - def set_label_color(self, item_id, color_idx): - """ - Used for highlight additional information in Project panel. - Green color is loaded asset, blue is created asset - Args: - item_id (int): - color_idx (int): 0-16 Label colors from AE Project view - """ - self.websocketserver.call(self.client.call - ('AfterEffects.set_label_color', - item_id=item_id, - color_idx=color_idx - )) - - def get_work_area(self, item_id): - """ Get work are information for render purposes - Args: - item_id (int): - - Returns: - (namedtuple) - - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_work_area', - item_id=item_id - )) - - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Couldn't get work area") - - def set_work_area(self, item, start, duration, frame_rate): - """ - Set work area to predefined values (from Ftrack). - Work area directs what gets rendered. - Beware of rounding, AE expects seconds, not frames directly. - - Args: - item (dict): - start (float): workAreaStart in seconds - duration (float): in seconds - frame_rate (float): frames in seconds - """ - self.websocketserver.call(self.client.call - ('AfterEffects.set_work_area', - item_id=item.id, - start=start, - duration=duration, - frame_rate=frame_rate - )) - - def save(self): - """ - Saves active document - Returns: None - """ - self.websocketserver.call(self.client.call - ('AfterEffects.save')) - - def saveAs(self, project_path, as_copy): - """ - Saves active project to aep (copy) or png or jpg - Args: - project_path(string): full local path - as_copy: - Returns: None - """ - self.websocketserver.call(self.client.call - ('AfterEffects.saveAs', - image_path=project_path, - as_copy=as_copy)) - - def get_render_info(self): - """ Get render queue info for render purposes - - Returns: - (namedtuple): with 'file_name' field - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_render_info')) - - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Render queue needs to have file extension in 'Output to'") - - def get_audio_url(self, item_id): - """ Get audio layer absolute url for comp - - Args: - item_id (int): composition id - Returns: - (str): absolute path url - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.get_audio_url', - item_id=item_id)) - - return res - - def close(self): - self.client.close() - - def import_background(self, comp_id, comp_name, files): - """ - Imports backgrounds images to existing or new composition. - - If comp_id is not provided, new composition is created, basic - values (width, heights, frameRatio) takes from first imported - image. - - All images from background json are imported as a FootageItem and - separate layer is created for each of them under composition. - - Order of imported 'files' is important. - - Args: - comp_id (int): id of existing composition (null if new) - comp_name (str): used when new composition - files (list): list of absolute paths to import and - add as layers - - Returns: - (AEItem): object with id of created folder, all imported images - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.import_background', - comp_id=comp_id, - comp_name=comp_name, - files=files)) - - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Import background failed.") - - def reload_background(self, comp_id, comp_name, files): - """ - Reloads backgrounds images to existing composition. - - It actually deletes complete folder with imported images and - created composition for safety. - - Args: - comp_id (int): id of existing composition to be overwritten - comp_name (str): new name of composition (could be same as old - if version up only) - files (list): list of absolute paths to import and - add as layers - Returns: - (AEItem): object with id of created folder, all imported images - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.reload_background', - comp_id=comp_id, - comp_name=comp_name, - files=files)) - - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Reload of background failed.") - - def add_item_as_layer(self, comp_id, item_id): - """ - Adds already imported FootageItem ('item_id') as a new - layer to composition ('comp_id'). - - Args: - comp_id (int): id of target composition - item_id (int): FootageItem.id - comp already found previously - """ - res = self.websocketserver.call(self.client.call - ('AfterEffects.add_item_as_layer', - comp_id=comp_id, - item_id=item_id)) - - records = self._to_records(res) - if records: - return records.pop() - - log.debug("Adding new layer failed.") - - def _to_records(self, res): - """ - Converts string json representation into list of AEItem - dot notation access to work. - Returns: - res(string): - json representation - """ - if not res: - return [] - - try: - layers_data = json.loads(res) - except json.decoder.JSONDecodeError: - raise ValueError("Received broken JSON {}".format(res)) - if not layers_data: - return [] - - ret = [] - # convert to AEItem to use dot donation - if isinstance(layers_data, dict): - layers_data = [layers_data] - for d in layers_data: - # currently implemented and expected fields - item = AEItem(d.get('id'), - d.get('name'), - d.get('type'), - d.get('members'), - d.get('workAreaStart'), - d.get('workAreaDuration'), - d.get('frameRate'), - d.get('file_name')) - - ret.append(item) - return ret diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py deleted file mode 100644 index d223153797..0000000000 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ /dev/null @@ -1,311 +0,0 @@ -from pype.modules.websocket_server import WebSocketServer -""" - Stub handling connection from server to client. - Used anywhere solution is calling client methods. -""" -import json -from collections import namedtuple - - -class PhotoshopServerStub(): - """ - Stub for calling function on client (Photoshop js) side. - Expects that client is already connected (started when avalon menu - is opened). - 'self.websocketserver.call' is used as async wrapper - """ - - def __init__(self): - self.websocketserver = WebSocketServer.get_instance() - self.client = self.websocketserver.get_client() - - def open(self, path): - """ - Open file located at 'path' (local). - Args: - path(string): file path locally - Returns: None - """ - self.websocketserver.call(self.client.call - ('Photoshop.open', path=path) - ) - - def read(self, layer, layers_meta=None): - """ - Parses layer metadata from Headline field of active document - Args: - layer: - Format of tuple: { 'id':'123', - 'name': 'My Layer 1', - 'type': 'GUIDE'|'FG'|'BG'|'OBJ' - 'visible': 'true'|'false' - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_layers')) - - return self._to_records(res) - - def get_layers_in_layers(self, layers): - """ - Return all layers that belong to layers (might be groups). - Args: - layers : - Returns: - """ - all_layers = self.get_layers() - ret = [] - parent_ids = set([lay.id for lay in layers]) - - for layer in all_layers: - parents = set(layer.parents) - if len(parent_ids & parents) > 0: - ret.append(layer) - if layer.id in parent_ids: - ret.append(layer) - - return ret - - def create_group(self, name): - """ - Create new group (eg. LayerSet) - Returns: - """ - ret = self.websocketserver.call(self.client.call - ('Photoshop.create_group', - name=name)) - # create group on PS is asynchronous, returns only id - layer = {"id": ret, "name": name, "group": True} - return namedtuple('Layer', layer.keys())(*layer.values()) - - def group_selected_layers(self, name): - """ - Group selected layers into new LayerSet (eg. group) - Returns: (Layer) - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.group_selected_layers', - name=name) - ) - res = self._to_records(res) - - if res: - return res.pop() - raise ValueError("No group record returned") - - def get_selected_layers(self): - """ - Get a list of actually selected layers - Returns: - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_selected_layers')) - return self._to_records(res) - - def select_layers(self, layers): - """ - Selects specified layers in Photoshop by its ids - Args: - layers: - Returns: None - """ - layer_ids = [layer.id for layer in layers] - - self.websocketserver.call(self.client.call - ('Photoshop.get_layers', - layers=layer_ids) - ) - - def get_active_document_full_name(self): - """ - Returns full name with path of active document via ws call - Returns(string): full path with name - """ - res = self.websocketserver.call( - self.client.call('Photoshop.get_active_document_full_name')) - - return res - - def get_active_document_name(self): - """ - Returns just a name of active document via ws call - Returns(string): file name - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_active_document_name')) - - return res - - def is_saved(self): - """ - Returns true if no changes in active document - Returns: - """ - return self.websocketserver.call(self.client.call - ('Photoshop.is_saved')) - - def save(self): - """ - Saves active document - Returns: None - """ - self.websocketserver.call(self.client.call - ('Photoshop.save')) - - def saveAs(self, image_path, ext, as_copy): - """ - Saves active document to psd (copy) or png or jpg - Args: - image_path(string): full local path - ext: - as_copy: - Returns: None - """ - self.websocketserver.call(self.client.call - ('Photoshop.saveAs', - image_path=image_path, - ext=ext, - as_copy=as_copy)) - - def set_visible(self, layer_id, visibility): - """ - Set layer with 'layer_id' to 'visibility' - Args: - layer_id: - visibility: - Returns: None - """ - self.websocketserver.call(self.client.call - ('Photoshop.set_visible', - layer_id=layer_id, - visibility=visibility)) - - def get_layers_metadata(self): - """ - Reads layers metadata from Headline from active document in PS. - (Headline accessible by File > File Info) - Returns(string): - json documents - """ - layers_data = {} - res = self.websocketserver.call(self.client.call('Photoshop.read')) - try: - layers_data = json.loads(res) - except json.decoder.JSONDecodeError: - pass - return layers_data - - def import_smart_object(self, path, layer_name): - """ - Import the file at `path` as a smart object to active document. - - Args: - path (str): File path to import. - layer_name (str): Unique layer name to differentiate how many times - same smart object was loaded - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.import_smart_object', - path=path, name=layer_name)) - - return self._to_records(res).pop() - - def replace_smart_object(self, layer, path, layer_name): - """ - Replace the smart object `layer` with file at `path` - layer_name (str): Unique layer name to differentiate how many times - same smart object was loaded - - Args: - layer (namedTuple): Layer("id":XX, "name":"YY"..). - path (str): File to import. - """ - self.websocketserver.call(self.client.call - ('Photoshop.replace_smart_object', - layer_id=layer.id, - path=path, name=layer_name)) - - def delete_layer(self, layer_id): - """ - Deletes specific layer by it's id. - Args: - layer_id (int): id of layer to delete - """ - self.websocketserver.call(self.client.call - ('Photoshop.delete_layer', - layer_id=layer_id)) - - def close(self): - self.client.close() - - def _to_records(self, res): - """ - Converts string json representation into list of named tuples for - dot notation access to work. - Returns: - res(string): - json representation - """ - try: - layers_data = json.loads(res) - except json.decoder.JSONDecodeError: - raise ValueError("Received broken JSON {}".format(res)) - ret = [] - # convert to namedtuple to use dot donation - if isinstance(layers_data, dict): # TODO refactore - layers_data = [layers_data] - for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) - return ret diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.html b/pype/modules/websocket_server/test_client/wsrpc_client.html deleted file mode 100644 index 9c3f469aca..0000000000 --- a/pype/modules/websocket_server/test_client/wsrpc_client.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - Title - - - - - - - - - - - - - -
-
Test of wsrpc javascript client
- -
- -
-
-
-
-

No return value

-
-
-
    -
  • Calls server_function_one
  • -
  • Function only logs on server
  • -
  • No return value
  • -
  •  
  • -
  •  
  • -
  •  
  • -
- -
-
-
-
-

Return value

-
-
-
    -
  • Calls server_function_two
  • -
  • Function logs on server
  • -
  • Returns simple text value
  • -
  •  
  • -
  •  
  • -
  •  
  • -
- -
-
-
-
-

Notify

-
-
-
    -
  • Calls server_function_three
  • -
  • Function logs on server
  • -
  • Returns json payload
  • -
  • Server then calls function ON the client after delay
  • -
  •  
  • -
- -
-
-
-
-

Send value

-
-
-
    -
  • Calls server_function_four
  • -
  • Function logs on server
  • -
  • Returns modified sent values
  • -
  •  
  • -
  •  
  • -
  •  
  • -
- -
-
-
-
- - - \ No newline at end of file diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.py b/pype/modules/websocket_server/test_client/wsrpc_client.py deleted file mode 100644 index ef861513ae..0000000000 --- a/pype/modules/websocket_server/test_client/wsrpc_client.py +++ /dev/null @@ -1,34 +0,0 @@ -import asyncio - -from wsrpc_aiohttp import WSRPCClient - -""" - Simple testing Python client for wsrpc_aiohttp - Calls sequentially multiple methods on server -""" - -loop = asyncio.get_event_loop() - - -async def main(): - print("main") - client = WSRPCClient("ws://127.0.0.1:8099/ws/", - loop=asyncio.get_event_loop()) - - client.add_route('notify', notify) - await client.connect() - print("connected") - print(await client.proxy.ExternalApp1.server_function_one()) - print(await client.proxy.ExternalApp1.server_function_two()) - print(await client.proxy.ExternalApp1.server_function_three()) - print(await client.proxy.ExternalApp1.server_function_four(foo="one")) - await client.close() - - -def notify(socket, *args, **kwargs): - print("called from server") - - -if __name__ == "__main__": - # loop.run_until_complete(main()) - asyncio.run(main()) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py deleted file mode 100644 index 7a6710349b..0000000000 --- a/pype/modules/websocket_server/websocket_server.py +++ /dev/null @@ -1,265 +0,0 @@ -import os -import sys -import pyclbr -import importlib -import urllib -import threading - -import six -from pype.lib import PypeLogger -from .. import PypeModule, ITrayService - -if six.PY2: - web = asyncio = STATIC_DIR = WebSocketAsync = None -else: - from aiohttp import web - import asyncio - from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync - -log = PypeLogger().get_logger("WebsocketServer") - - -class WebsocketModule(PypeModule, ITrayService): - name = "Websocket server" - label = "Websocket server" - - def initialize(self, module_settings): - if asyncio is None: - raise AssertionError( - "WebSocketServer module requires Python 3.5 or higher." - ) - - self.enabled = True - self.websocket_server = None - - def connect_with_modules(self, *_a, **kw): - return - - def tray_init(self): - self.websocket_server = WebSocketServer() - self.websocket_server.on_stop_callbacks.append( - self.set_service_failed_icon - ) - - def tray_start(self): - if self.websocket_server: - self.websocket_server.module_start() - - def tray_exit(self): - if self.websocket_server: - self.websocket_server.module_stop() - - -class WebSocketServer(): - """ - Basic POC implementation of asychronic websocket RPC server. - Uses class in external_app_1.py to mimic implementation for single - external application. - 'test_client' folder contains two test implementations of client - """ - _instance = None - - def __init__(self): - WebSocketServer._instance = self - - self.client = None - self.handlers = {} - self.on_stop_callbacks = [] - - port = None - websocket_url = os.getenv("WEBSOCKET_URL") - if websocket_url: - parsed = urllib.parse.urlparse(websocket_url) - port = parsed.port - if not port: - port = 8098 # fallback - - self.app = web.Application() - - self.app.router.add_route("*", "/ws/", WebSocketAsync) - self.app.router.add_static("/js", STATIC_DIR) - self.app.router.add_static("/", ".") - - # add route with multiple methods for single "external app" - directories_with_routes = ['hosts'] - self.add_routes_for_directories(directories_with_routes) - - self.websocket_thread = WebsocketServerThread(self, port) - - def module_start(self): - if self.websocket_thread: - self.websocket_thread.start() - - def module_stop(self): - if self.websocket_thread: - self.websocket_thread.stop() - - def add_routes_for_directories(self, directories_with_routes): - """ Loops through selected directories to find all modules and - in them all classes implementing 'WebSocketRoute' that could be - used as route. - All methods in these classes are registered automatically. - """ - for dir_name in directories_with_routes: - dir_name = os.path.join(os.path.dirname(__file__), dir_name) - for file_name in os.listdir(dir_name): - if '.py' in file_name and '__' not in file_name: - self.add_routes_for_module(file_name, dir_name) - - def add_routes_for_module(self, file_name, dir_name): - """ Auto routes for all classes implementing 'WebSocketRoute' - in 'file_name' in 'dir_name' - """ - module_name = file_name.replace('.py', '') - module_info = pyclbr.readmodule(module_name, [dir_name]) - - for class_name, cls_object in module_info.items(): - sys.path.append(dir_name) - if 'WebSocketRoute' in cls_object.super: - log.debug('Adding route for {}'.format(class_name)) - module = importlib.import_module(module_name) - cls = getattr(module, class_name) - WebSocketAsync.add_route(class_name, cls) - sys.path.pop() - - def call(self, func): - log.debug("websocket.call {}".format(func)) - future = asyncio.run_coroutine_threadsafe(func, - self.websocket_thread.loop) - result = future.result() - return result - - def get_client(self): - """ - Return first connected client to WebSocket - TODO implement selection by Route - :return: client - """ - clients = WebSocketAsync.get_clients() - client = None - if len(clients) > 0: - key = list(clients.keys())[0] - client = clients.get(key) - - return client - - @staticmethod - def get_instance(): - if WebSocketServer._instance is None: - WebSocketServer() - return WebSocketServer._instance - - def stop_websocket_server(self): - self.stop() - - @property - def is_running(self): - return self.websocket_thread.is_running - - def stop(self): - if not self.is_running: - return - try: - log.debug("Stopping websocket server") - self.websocket_thread.is_running = False - self.websocket_thread.stop() - except Exception: - log.warning( - "Error has happened during Killing websocket server", - exc_info=True - ) - - def thread_stopped(self): - for callback in self.on_stop_callbacks: - callback() - - -class WebsocketServerThread(threading.Thread): - """ Listener for websocket rpc requests. - - It would be probably better to "attach" this to main thread (as for - example Harmony needs to run something on main thread), but currently - it creates separate thread and separate asyncio event loop - """ - def __init__(self, module, port): - if asyncio is None: - raise AssertionError( - "WebSocketServer module requires Python 3.5 or higher." - ) - - super(WebsocketServerThread, self).__init__() - - self.is_running = False - self.port = port - self.module = module - self.loop = None - self.runner = None - self.site = None - self.tasks = [] - - def run(self): - self.is_running = True - - try: - log.info("Starting websocket server") - self.loop = asyncio.new_event_loop() # create new loop for thread - asyncio.set_event_loop(self.loop) - - self.loop.run_until_complete(self.start_server()) - - log.debug( - "Running Websocket server on URL:" - " \"ws://localhost:{}\"".format(self.port) - ) - - asyncio.ensure_future(self.check_shutdown(), loop=self.loop) - self.loop.run_forever() - except Exception: - log.warning( - "Websocket Server service has failed", exc_info=True - ) - finally: - self.loop.close() # optional - - self.is_running = False - self.module.thread_stopped() - log.info("Websocket server stopped") - - async def start_server(self): - """ Starts runner and TCPsite """ - self.runner = web.AppRunner(self.module.app) - await self.runner.setup() - self.site = web.TCPSite(self.runner, 'localhost', self.port) - await self.site.start() - - def stop(self): - """Sets is_running flag to false, 'check_shutdown' shuts server down""" - self.is_running = False - - async def check_shutdown(self): - """ Future that is running and checks if server should be running - periodically. - """ - while self.is_running: - while self.tasks: - task = self.tasks.pop(0) - log.debug("waiting for task {}".format(task)) - await task - log.debug("returned value {}".format(task.result)) - - await asyncio.sleep(0.5) - - log.debug("Starting shutdown") - await self.site.stop() - log.debug("Site stopped") - await self.runner.cleanup() - log.debug("Runner stopped") - tasks = [task for task in asyncio.all_tasks() if - task is not asyncio.current_task()] - list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks - results = await asyncio.gather(*tasks, return_exceptions=True) - log.debug(f'Finished awaiting cancelled tasks, results: {results}...') - await self.loop.shutdown_asyncgens() - # to really make sure everything else has time to stop - await asyncio.sleep(0.07) - self.loop.stop() From f4efe9537c11de10d58c0e0122a652910fb10738 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 17:52:05 +0100 Subject: [PATCH 2/8] added webserver module that uses similar logic to previous websocket server --- pype/modules/webserver/__init__.py | 6 + pype/modules/webserver/server.py | 142 +++++++++++++++++++++ pype/modules/webserver/webserver_module.py | 44 +++++++ 3 files changed, 192 insertions(+) create mode 100644 pype/modules/webserver/__init__.py create mode 100644 pype/modules/webserver/server.py create mode 100644 pype/modules/webserver/webserver_module.py diff --git a/pype/modules/webserver/__init__.py b/pype/modules/webserver/__init__.py new file mode 100644 index 0000000000..ace27d94ab --- /dev/null +++ b/pype/modules/webserver/__init__.py @@ -0,0 +1,6 @@ +from .webserver_module import WebServerModule + + +__all__ = ( + "WebServerModule", +) diff --git a/pype/modules/webserver/server.py b/pype/modules/webserver/server.py new file mode 100644 index 0000000000..41f8f86a1b --- /dev/null +++ b/pype/modules/webserver/server.py @@ -0,0 +1,142 @@ +import threading +import asyncio + +from aiohttp import web + +from pype.lib import PypeLogger + +log = PypeLogger.get_logger("WebServer") + + +class WebServerManager: + """Manger that care about web server thread.""" + def __init__(self, module): + self.module = module + + self.client = None + self.handlers = {} + self.on_stop_callbacks = [] + + self.app = web.Application() + + # add route with multiple methods for single "external app" + + self.webserver_thread = WebServerThread(self, self.module.port) + + def add_route(self, *args, **kwargs): + self.app.router.add_route(*args, **kwargs) + + def add_static(self, *args, **kwargs): + self.app.router.add_static(*args, **kwargs) + + def start_server(self): + if self.webserver_thread and not self.webserver_thread.is_alive(): + self.webserver_thread.start() + + def stop_server(self): + if not self.is_running: + return + try: + log.debug("Stopping Web server") + self.webserver_thread.is_running = False + self.webserver_thread.stop() + + except Exception: + log.warning( + "Error has happened during Killing Web server", + exc_info=True + ) + + @property + def is_running(self): + if not self.webserver_thread: + return False + return self.webserver_thread.is_running + + def thread_stopped(self): + for callback in self.on_stop_callbacks: + callback() + + +class WebServerThread(threading.Thread): + """ Listener for requests in thread.""" + def __init__(self, manager, port): + super(WebServerThread, self).__init__() + + self.is_running = False + self.port = port + self.manager = manager + self.loop = None + self.runner = None + self.site = None + self.tasks = [] + + def run(self): + self.is_running = True + + try: + log.info("Starting WebServer server") + self.loop = asyncio.new_event_loop() # create new loop for thread + asyncio.set_event_loop(self.loop) + + self.loop.run_until_complete(self.start_server()) + + log.debug( + "Running Web server on URL: \"localhost:{}\"".format(self.port) + ) + + asyncio.ensure_future(self.check_shutdown(), loop=self.loop) + self.loop.run_forever() + + except Exception: + log.warning( + "Web Server service has failed", exc_info=True + ) + finally: + self.loop.close() # optional + + self.is_running = False + self.manager.thread_stopped() + log.info("Web server stopped") + + async def start_server(self): + """ Starts runner and TCPsite """ + self.runner = web.AppRunner(self.manager.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, 'localhost', self.port) + await self.site.start() + + def stop(self): + """Sets is_running flag to false, 'check_shutdown' shuts server down""" + self.is_running = False + + async def check_shutdown(self): + """ Future that is running and checks if server should be running + periodically. + """ + while self.is_running: + while self.tasks: + task = self.tasks.pop(0) + log.debug("waiting for task {}".format(task)) + await task + log.debug("returned value {}".format(task.result)) + + await asyncio.sleep(0.5) + + log.debug("Starting shutdown") + await self.site.stop() + log.debug("Site stopped") + await self.runner.cleanup() + log.debug("Runner stopped") + tasks = [ + task + for task in asyncio.all_tasks() + if task is not asyncio.current_task() + ] + list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks + results = await asyncio.gather(*tasks, return_exceptions=True) + log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + await self.loop.shutdown_asyncgens() + # to really make sure everything else has time to stop + await asyncio.sleep(0.07) + self.loop.stop() diff --git a/pype/modules/webserver/webserver_module.py b/pype/modules/webserver/webserver_module.py new file mode 100644 index 0000000000..359cb3ae7b --- /dev/null +++ b/pype/modules/webserver/webserver_module.py @@ -0,0 +1,44 @@ +from .. import PypeModule, ITrayService + + +class WebServerModule(PypeModule, ITrayService): + name = "Websocket server" + label = "WebServer" + + def initialize(self, module_settings): + self.enabled = True + self.server_manager = None + + # TODO find free port + self.port = 8098 + + def connect_with_modules(self, *_a, **_kw): + return + + def tray_init(self): + self.create_server_manager() + + def tray_start(self): + self.start_server() + + def tray_exit(self): + self.stop_server() + + def start_server(self): + if self.server_manager: + self.server_manager.start_server() + + def stop_server(self): + if self.server_manager: + self.server_manager.stop_server() + + def create_server_manager(self): + if self.server_manager: + return + + from .server import WebServerManager + + self.server_manager = WebServerManager(self) + self.server_manager.on_stop_callbacks.append( + self.set_service_failed_icon + ) From 297978c081fd69619ac147a03e585e5d02407a2e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 17:52:25 +0100 Subject: [PATCH 3/8] modified imports in pype's modules --- pype/modules/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index e0481d0c92..787b9128e8 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -40,7 +40,7 @@ from .log_viewer import LogViewModule from .muster import MusterModule from .deadline import DeadlineModule from .standalonepublish_action import StandAlonePublishAction -from .websocket_server import WebsocketModule +from .webserver import WebServerModule from .sync_server import SyncServer @@ -82,6 +82,6 @@ __all__ = ( "DeadlineModule", "StandAlonePublishAction", - "WebsocketModule", + "WebServerModule", "SyncServer" ) From 2887c645424dc73cd4a03733d0e69dec46ac6eef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 5 Mar 2021 19:32:19 +0100 Subject: [PATCH 4/8] changed name of module --- pype/modules/webserver/webserver_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/webserver/webserver_module.py b/pype/modules/webserver/webserver_module.py index 359cb3ae7b..86afffd958 100644 --- a/pype/modules/webserver/webserver_module.py +++ b/pype/modules/webserver/webserver_module.py @@ -2,7 +2,7 @@ from .. import PypeModule, ITrayService class WebServerModule(PypeModule, ITrayService): - name = "Websocket server" + name = "webserver" label = "WebServer" def initialize(self, module_settings): From 24c0d0754c9eca54637b6ebfbdf3da920312a474 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 10:03:07 +0100 Subject: [PATCH 5/8] fix application manager refresh --- pype/lib/applications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index f3a650be4f..d20b01c3d2 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -101,6 +101,9 @@ class ApplicationManager: def refresh(self): """Refresh applications from settings.""" + self.applications.clear() + self.tools.clear() + settings = get_system_settings() hosts_definitions = settings["applications"] From 1243b27af26d28ec4aa88bb2f9f85ef6aec8a92f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 10:03:25 +0100 Subject: [PATCH 6/8] refresh application manager on actions discover --- pype/tools/launcher/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 58f0076871..631f6ddc98 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -144,6 +144,7 @@ class ActionModel(QtGui.QStandardItemModel): if not project_doc: return actions + self.application_manager.refresh() for app_def in project_doc["config"]["apps"]: app_name = app_def["name"] app = self.application_manager.applications.get(app_name) From a1bfefad9cede964d22419a46402a27f3205eea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Mar 2021 18:48:16 +0100 Subject: [PATCH 7/8] handler of static files was moved to webserver module --- pype/modules/rest_api/rest_api.py | 3 --- pype/modules/webserver/webserver_module.py | 11 +++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py index a30402b5fe..b37f5e13eb 100644 --- a/pype/modules/rest_api/rest_api.py +++ b/pype/modules/rest_api/rest_api.py @@ -107,7 +107,6 @@ class RestApiModule(PypeModule, ITrayService): self.rest_api_url = None self.rest_api_thread = None - self.resources_url = None def register_callback( self, path, callback, url_prefix="", methods=[], strict_match=False @@ -189,11 +188,9 @@ class RestApiModule(PypeModule, ITrayService): self.rest_api_url = "http://localhost:{}".format(port) self.rest_api_thread = RestApiThread(self, port) self.register_statics("/res", resources.RESOURCES_DIR) - self.resources_url = "{}/res".format(self.rest_api_url) # Set rest api environments os.environ["PYPE_REST_API_URL"] = self.rest_api_url - os.environ["PYPE_STATICS_SERVER"] = self.resources_url def tray_start(self): RestApiFactory.prepare_registered() diff --git a/pype/modules/webserver/webserver_module.py b/pype/modules/webserver/webserver_module.py index 86afffd958..0cdf478d8c 100644 --- a/pype/modules/webserver/webserver_module.py +++ b/pype/modules/webserver/webserver_module.py @@ -1,3 +1,5 @@ +import os +from pype import resources from .. import PypeModule, ITrayService @@ -17,6 +19,7 @@ class WebServerModule(PypeModule, ITrayService): def tray_init(self): self.create_server_manager() + self._add_resources_statics() def tray_start(self): self.start_server() @@ -24,6 +27,14 @@ class WebServerModule(PypeModule, ITrayService): def tray_exit(self): self.stop_server() + def _add_resources_statics(self): + static_prefix = "/res" + self.server_manager.add_static(static_prefix, resources.RESOURCES_DIR) + + os.environ["PYPE_STATICS_SERVER"] = "http://localhost:{}{}".format( + self.port, static_prefix + ) + def start_server(self): if self.server_manager: self.server_manager.start_server() From b524b0b0c436c71552287d8a839eec7a985ae1fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 11 Mar 2021 10:46:20 +0100 Subject: [PATCH 8/8] resolve conflict --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index eae14f2960..b6b56c4b3b 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit eae14f2960c4ccf2f0211e0726e88563129c0296 +Subproject commit b6b56c4b3be612138fb2df6f11c8e27f11c62ffd