From d8b33674bf2d827f120e64d8273ec2569233b62d Mon Sep 17 00:00:00 2001 From: Maxim Podosochnyy <podosochnyy@perx.ru> Date: Tue, 5 Mar 2024 13:10:54 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/extension_service/servicer.py | 82 +++++++++++++- perxis/extensions/actions.py | 68 ++++++++++++ perxis/extensions/bootstrap.py | 4 +- perxis/extensions/extension_service.py | 54 +++++++++- perxis/extensions/extension_setup.py | 142 +++++++++++++++++++++++++ setup.py | 2 +- 6 files changed, 343 insertions(+), 9 deletions(-) create mode 100644 perxis/extensions/actions.py diff --git a/examples/extension_service/servicer.py b/examples/extension_service/servicer.py index 12a8455..ab47966 100644 --- a/examples/extension_service/servicer.py +++ b/examples/extension_service/servicer.py @@ -1,8 +1,13 @@ +import random + from constants import extension -from constants import collections +from constants import collections as extension_collections +from perxis.extensions.actions import make_action_dict +from perxis.extensions import extension_service_pb2 from perxis.extensions.extension_service import ExtensionService +from perxis.extensions import extension_pb2 from perxis.collections import helpers as collections_helpers from perxis.roles import roles_pb2 from perxis.common import common_pb2 @@ -11,7 +16,30 @@ from perxis.clients import clients_pb2 class Servicer(ExtensionService): extension_id = extension.ID - collections = collections_helpers.make_collection_instances("./schemes", collections.schemes_mapping) + collections = collections_helpers.make_collection_instances("./schemes", extension_collections.schemes_mapping) + actions = [ + make_action_dict( + extension_id=extension.ID, + action_id="demo-action-no-callback", + name="Действие с несуществующим каллбэком", + kind=extension_pb2.Action.Kind.ITEMS, + classes=[extension_collections.TEST_COLLECTION_ID] + ), + make_action_dict( + extension_id=extension.ID, + action_id="demo-action-items", + name="Действие над элементами", + kind=extension_pb2.Action.Kind.ITEMS, + classes=[extension_collections.TEST_COLLECTION_ID] + ), + make_action_dict( + extension_id=extension.ID, + action_id="demo-action-item", + name="Действие над одним элементом", + kind=extension_pb2.Action.Kind.ITEM, + classes=[extension_collections.TEST_COLLECTION_ID] + ) + ] roles = [ roles_pb2.Role( id="demo-role", @@ -37,3 +65,53 @@ class Servicer(ExtensionService): } ) ] + + async def demo_action_items( + self, + request: extension_pb2.ActionRequest, + context + ) -> extension_service_pb2.ActionResponse: + self.logger.info( + "Действие над несколькими элементами %s %s %s %s" % ( + request.space_id, request.env_id, request.item_ids, request.collection_id + ) + ) + + with_error = bool(random.randint(0, 1)) + + if with_error: + state = extension_service_pb2.ActionResponse.State.ERROR + else: + state = extension_service_pb2.ActionResponse.State.DONE + + return extension_service_pb2.ActionResponse( + state=state, + title="Действие над одним элементом", + error=", ".join(["Ошибка"] if with_error else []), + msg=f"{'Ошибка' if with_error else 'ОК'} ({request.item_ids}, {request.collection_id})" + ) + + async def demo_action_item( + self, + request: extension_pb2.ActionRequest, + context + ) -> extension_service_pb2.ActionResponse: + self.logger.info( + "Действие над одним элементом %s %s %s %s" % ( + request.space_id, request.env_id, request.item_id, request.collection_id + ) + ) + + with_error = bool(random.randint(0, 1)) + + if with_error: + state = extension_service_pb2.ActionResponse.State.ERROR + else: + state = extension_service_pb2.ActionResponse.State.DONE + + return extension_service_pb2.ActionResponse( + state=state, + title="Действие над одним элементом", + error=", ".join(["Ошибка"] if with_error else []), + msg=f"{'Ошибка' if with_error else 'ОК'} ({request.item_id}, {request.collection_id})" + ) diff --git a/perxis/extensions/actions.py b/perxis/extensions/actions.py new file mode 100644 index 0000000..da0a677 --- /dev/null +++ b/perxis/extensions/actions.py @@ -0,0 +1,68 @@ +import copy + +from typing import Optional + +from google.protobuf.struct_pb2 import Struct +from perxis.items import items_pb2 + + +ACTIONS_COLLECTION_ID = "space_actions" + + +def process_action_id(action_id: str) -> str: + action_id = action_id.lower().replace("-", "_").replace(" ", "_") + + return action_id + + +def make_action_dict( + extension_id: str, + action_id: str, + name: str, + kind: int, + classes: Optional[list[str]] = None, + target: Optional[int] = 0, + description: Optional[str] = "", + icon: Optional[str] = "", + confirm: bool = True, + view: Optional[int] = 0, +) -> dict: + if not classes: + classes = ["*"] + + action_id = process_action_id(action_id) + + return { + "id": action_id, + "action": f"grpc:///{extension_id}/{action_id}", + "name": name, + "target": target, + "classes": classes, + "description": description, + "icon": icon, + "kind": kind, + "confirm": confirm, + "view": view, + } + + +def make_action_struct(data: dict) -> Struct: + struct = Struct() + struct.update(data) + + return struct + + +def make_action_item(space_id: str, env_id: str, data: dict) -> items_pb2.Item: + copied_data = copy.deepcopy(data) + action_id = copied_data.pop("id") + + struct = make_action_struct(data) + + return items_pb2.Item( + id=action_id, + space_id=space_id, + env_id=env_id, + collection_id=ACTIONS_COLLECTION_ID, + data=struct + ) diff --git a/perxis/extensions/bootstrap.py b/perxis/extensions/bootstrap.py index bc465cf..8924b37 100644 --- a/perxis/extensions/bootstrap.py +++ b/perxis/extensions/bootstrap.py @@ -8,6 +8,7 @@ from concurrent import futures from perxis.collections import collections_pb2_grpc from perxis.environments import environments_pb2_grpc from perxis.roles import roles_pb2_grpc +from perxis.items import items_pb2_grpc from perxis.clients import clients_pb2_grpc from perxis.common import operation_service_pb2_grpc from perxis.extensions import manager_service_pb2_grpc, manager_service_pb2, extension_service_pb2_grpc @@ -67,11 +68,12 @@ async def _main( roles_stub = roles_pb2_grpc.RolesStub(content_channel) clients_stub = clients_pb2_grpc.ClientsStub(content_channel) environments_stub = environments_pb2_grpc.EnvironmentsStub(content_channel) + items_stub = items_pb2_grpc.ItemsStub(content_channel) server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10)) servicer = servicer_cls( - collections_stub, environments_stub, roles_stub, clients_stub + collections_stub, environments_stub, roles_stub, clients_stub, items_stub ) extension_service_pb2_grpc.add_ExtensionServiceServicer_to_server(servicer, server) diff --git a/perxis/extensions/extension_service.py b/perxis/extensions/extension_service.py index 6da5bfb..986a2a1 100644 --- a/perxis/extensions/extension_service.py +++ b/perxis/extensions/extension_service.py @@ -11,8 +11,9 @@ from google.protobuf import timestamp_pb2, any_pb2, wrappers_pb2 from perxis.extensions import extension_service_pb2, extension_service_pb2_grpc, extension_pb2 from perxis.roles import roles_pb2_grpc, roles_pb2 +from perxis.items import items_pb2_grpc from perxis.clients import clients_pb2_grpc, clients_pb2 -from perxis.common import common_pb2, operation_pb2, operation_service_pb2_grpc, operation_service_pb2, error_pb2 +from perxis.common import operation_pb2, operation_service_pb2_grpc, operation_service_pb2, error_pb2 from perxis.collections import collections_pb2_grpc, collections_pb2 from perxis.environments import environments_pb2_grpc from perxis.extensions.extension_setup import ExtensionSetup @@ -81,6 +82,7 @@ class ExtensionService( collections: list[collections_pb2.Collection] = [] roles: list[roles_pb2.Role] = [] clients: list[clients_pb2.Client] = [] + actions: list[dict] = [] __operations: dict[str, OperationMeta] = {} @@ -89,16 +91,18 @@ class ExtensionService( environments_service: environments_pb2_grpc.EnvironmentsStub, roles_service: roles_pb2_grpc.RolesStub, clients_service: clients_pb2_grpc.ClientsStub, + items_service: items_pb2_grpc.ItemsStub, ): self.logger = logging.getLogger(__name__) self.collections_service = collections_service self.environments_service = environments_service self.roles_service = roles_service self.clients_service = clients_service + self.items_service = items_service self.extension_setup = ExtensionSetup( self.collections_service, self.environments_service, - self.roles_service, self.clients_service + self.roles_service, self.clients_service, self.items_service, ) for collection in self.collections or []: @@ -110,6 +114,9 @@ class ExtensionService( for client in self.clients or []: self.extension_setup.add_client(client) + for action in self.actions or []: + self.extension_setup.add_action(action) + @aiocron.crontab('0 * * * *', start=True) async def remove_old_operations(): self.remove_old_operations() @@ -266,11 +273,48 @@ class ExtensionService( return self.get_operation_meta(operation_id).to_operation() + async def _dispatch_action( + self, request: extension_pb2.ActionRequest, context + ) -> extension_service_pb2.ActionResponse: + action_id = request.action.split("/")[-1] + + if not hasattr(self, action_id): + response = extension_service_pb2.ActionResponse( + state=extension_service_pb2.ActionResponse.State.ERROR, + title="Невозможно выполнить действие", + error=f"В расширении отсутсвует функция {action_id}" + ) + else: + func = getattr(self, action_id) + + response = await func(request, context) + + return response + async def Action(self, request: extension_pb2.ActionRequest, context): - context.set_code(grpc.StatusCode.UNKNOWN) - context.set_details("Unknown action") + operation_description = "Действие %s для окружения %s пространства %s" % ( + request.action, + request.env_id, + request.space_id, + ) + + self.logger.info(operation_description) + + response = await self._dispatch_action(request, context) + + if response.state == extension_service_pb2.ActionResponse.State.ERROR: + error_description = "Ошибка действия %s в расширении %s: %s" % ( + request.action, + self.extension_id, + response.error + ) + + #context.set_code(grpc.StatusCode.UNKNOWN) + #context.set_details(error_description) + + self.logger.error(error_description) - return None + return response async def Get(self, request: operation_service_pb2.GetOperationRequest, context): operations_meta = self.get_operation_meta(request.operation_id) diff --git a/perxis/extensions/extension_setup.py b/perxis/extensions/extension_setup.py index ce2c83a..9efc474 100644 --- a/perxis/extensions/extension_setup.py +++ b/perxis/extensions/extension_setup.py @@ -11,9 +11,11 @@ from deepdiff import DeepDiff from google.protobuf.json_format import MessageToDict from perxis.collections import collections_pb2_grpc, collections_pb2 from perxis.roles import roles_pb2_grpc, roles_pb2 +from perxis.items import items_pb2_grpc, items_pb2 from perxis.common import common_pb2 from perxis.clients import clients_pb2_grpc, clients_pb2 from perxis.environments import environments_pb2_grpc, environments_pb2 +from perxis.extensions.actions import make_action_item, make_action_struct, ACTIONS_COLLECTION_ID logger = logging.getLogger(__name__) @@ -26,19 +28,28 @@ class ExtensionSetup: environments_service: environments_pb2_grpc.EnvironmentsStub, roles_service: roles_pb2_grpc.RolesStub, clients_service: clients_pb2_grpc.ClientsStub, + items_service: items_pb2_grpc.ItemsStub, ): self.collections = [] self.clients = [] self.roles = [] + self.actions = [] self.roles_service = roles_service self.clients_service = clients_service self.collections_service = collections_service self.environments_service = environments_service + self.items_service = items_service self.__max_attempts_count = 5 self.__sleep_time = 1 + def add_action(self, action: dict): + self.actions.append(action) + + def set_actions(self, actions: list[dict]): + self.actions = actions + def add_collection(self, collection: collections_pb2.Collection): self.collections.append(collection) @@ -437,12 +448,140 @@ class ExtensionSetup: return error_message + # Работа с действиями + async def __check_actions(self, space_id: str, env_id: str) -> list[str]: + errors_list = [] + + ids = [data["id"] for data in self.actions if data.get("id")] + + items = [] + + try: + result = await self.items_service.Find( + items_pb2.FindRequest( + space_id=space_id, + env_id=env_id, + collection_id=ACTIONS_COLLECTION_ID, + filter=items_pb2.Filter(q=[f"id in {ids}"]), + options=items_pb2.FindOptions( + options=common_pb2.FindOptions( + page_num=0, page_size=len(ids) + ) + ), + ) + ) + + items = result.items + except grpc.RpcError as e: + errors_list.append(f"Не удалось получить данные о действиях, ошибка {e.details()}") + finally: + found_ids = [item.id for item in items] + + for action_id in found_ids: + if action_id not in ids: + errors_list.append(f"Действие {action_id} не найдено") + + return errors_list + + async def __update_actions(self, space_id: str, env_id: str) -> list[str]: + errors_list = [] + not_found = False + + for action in self.actions: + action_item = make_action_item(space_id, env_id, action) + + try: + r = await self.items_service.Update( + items_pb2.UpdateRequest( + item=items_pb2.Item( + id=action_item.id, + space_id=space_id, + env_id=env_id, + collection_id=ACTIONS_COLLECTION_ID, + data=action_item.data, + ) + ) + ) + except grpc.RpcError as e: + if "not found" not in e.details(): + errors_list.append(f"Не удалось обновить действие {action_item.id}, {e.details()}") + continue + + not_found = True + + if not_found: + try: + await self.items_service.Create( + items_pb2.CreateRequest( + item=action_item + ) + ) + except grpc.RpcError as e: + errors_list.append(f"Не удалось создать действие {action_item.id}, {e.details()}") + + try: + await self.items_service.Publish( + items_pb2.PublishRequest( + item=action_item + ) + ) + except grpc.RpcError as e: + errors_list.append(f"Не удалось опубликовать действие {action_item.id}, {e.details()}") + + return errors_list + + async def __remove_actions(self, space_id: str, env_id: str) -> list[str]: + errors_list = [] + + for action in self.actions: + action_item = make_action_item(space_id, env_id, action) + + try: + await self.items_service.Unpublish( + items_pb2.UnpublishRequest( + item=items_pb2.Item( + id=action_item.id, + space_id=action_item.space_id, + env_id=action_item.env_id, + collection_id=action_item.collection_id, + ) + ) + ) + except grpc.RpcError as e: + if "not found" not in e.details(): + errors_list.append(f"Не удалось снять с публикации действие {action.get('id', 'n/a')}, {e.details()}") + + try: + await self.items_service.Delete( + items_pb2.DeleteRequest( + space_id=space_id, + env_id=env_id, + item=items_pb2.Item( + id=action_item.id, + space_id=action_item.space_id, + env_id=action_item.env_id, + collection_id=action_item.collection_id, + ), + options=items_pb2.DeleteOptions( + update_attrs=True, + erase=True + ) + ) + ) + except grpc.RpcError as e: + # Отсутствие действия это не ошибка + if "not found" not in e.details(): + errors_list.append(f"Не удалось удалить действие {action.get('id', 'n/a')}, {e.details()}") + + return errors_list + async def install(self, space_id: str, env_id: str, use_force: bool) -> list[str]: errors = [] errors += await self.__update_collections(space_id, env_id) errors += await self.__update_roles(space_id, env_id) errors += await self.__update_clients(space_id) + errors += await self.__update_actions(space_id, env_id) return errors @@ -452,6 +591,7 @@ class ExtensionSetup: errors += await self.__update_collections(space_id, env_id) errors += await self.__update_roles(space_id, env_id) errors += await self.__update_clients(space_id) + errors += await self.__update_actions(space_id, env_id) return errors @@ -459,6 +599,7 @@ class ExtensionSetup: errors = await self.__check_collections(space_id, env_id) errors += await self.__check_roles(space_id) errors += await self.__check_clients(space_id) + errors += await self.__check_actions(space_id, env_id) return errors @@ -469,5 +610,6 @@ class ExtensionSetup: errors += await self.__remove_collections(space_id, env_id) errors += await self.__remove_clients(space_id) errors += await self.__remove_roles(space_id) + errors += await self.__remove_actions(space_id, env_id) return errors diff --git a/setup.py b/setup.py index 9c3f8fd..5f82c5f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def load_requirements(): setup( name='perxis', - version='1.3.0', + version='1.4.0', description='Perxis python client', long_description=long_description, long_description_content_type='text/markdown', -- GitLab