diff --git a/examples/extension_service/Dockerfile b/examples/extension_service/Dockerfile index 8e11f941a29274ee56ef20e1ef74ced8f08d60c1..3335c7adf2b4752842ff6b515300a5ffe205d834 100644 --- a/examples/extension_service/Dockerfile +++ b/examples/extension_service/Dockerfile @@ -11,7 +11,7 @@ ARG PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} ENV PIP_EXTRA_INDEX_URL=$PIP_EXTRA_INDEX_URL COPY . /home/${USER}/app -RUN pip install perxis==1.3.0 +RUN pip install perxis==1.10.0 ENV PYTHONPATH="/home/perx/app" ENV PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python diff --git a/examples/extension_service/Dockerfile.local b/examples/extension_service/Dockerfile.local index 1b2d30650f663a69309125051451daf89915b2cc..9118f20159cc6c27e0a0d3f6ee69fc19dea50e78 100644 --- a/examples/extension_service/Dockerfile.local +++ b/examples/extension_service/Dockerfile.local @@ -11,7 +11,7 @@ ARG PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL} ENV PIP_EXTRA_INDEX_URL=$PIP_EXTRA_INDEX_URL COPY . /home/${USER}/app -RUN pip install perxis==1.3.0 +RUN pip install perxis==1.10.0 RUN pip install 'watchdog[watchmedo]' ENV PYTHONPATH="/home/perx/app" diff --git a/examples/extension_service/servicer.py b/examples/extension_service/servicer.py index f83fd8657287cec29cd49de260676c9da1661f78..79aa7e5dee17ad97404b1f96034814b7c221b17c 100644 --- a/examples/extension_service/servicer.py +++ b/examples/extension_service/servicer.py @@ -4,6 +4,8 @@ from constants import extension from constants import collections as extension_collections +from perxis.extensions.utils import datasource_items_from_collections, sync_policies_from_collections +from perxis.extensions.item_models import DataSourceItem, SyncPolicyItem, Item, IfCollectionExists, IfExtensionInstalled from perxis.extensions.actions import make_action_dict from perxis.extensions import extension_service_pb2 from perxis.extensions.extension_service import ExtensionService @@ -76,6 +78,42 @@ class Servicer(ExtensionService): ) ] + # items = (datasource_items_from_collections(extension_collections.schemes_mapping) + # + sync_policies_from_collections(extension_collections.schemes_mapping)) + + items = [ + DataSourceItem( + collection_id="test_collection" + ), + SyncPolicyItem( + collection_id="test_collection", + # Запрет на изменение или удаление записи политики синхронизации + with_update=False, + with_delete=False, + ), + Item( + collection_id="secrets", + data={ + "id": f"{extension_id}__key", + "name": f"Ключ для {extension_id}", + "key": "123" + }, + rules=[IfExtensionInstalled("secrets")] + ), + Item( + collection_id="web_domains", + identifier_field="domain", + data={ + "domain": "example.com", + "primary": True, + "tls_ssl": "letsencrypt", + }, + rules=[ + IfCollectionExists() + ] + ) + ] + async def action_get_current_organization_and_users( self, request: extension_pb2.ActionRequest, diff --git a/perxis/extensions/actions.py b/perxis/extensions/actions.py index e2ae01878e76b8230da91abad8e564a534b60cce..a84976a736d20c585bc3882eb42b07d532b00d46 100644 --- a/perxis/extensions/actions.py +++ b/perxis/extensions/actions.py @@ -1,7 +1,5 @@ import copy -from typing import Optional - from google.protobuf.struct_pb2 import Struct from perxis.items import items_pb2 diff --git a/perxis/extensions/bootstrap.py b/perxis/extensions/bootstrap.py index c3e5c3ece363d0390bacea488906b6b0776285ba..4e3eb6634a70f09ecb24c6d30953f630838ed13e 100644 --- a/perxis/extensions/bootstrap.py +++ b/perxis/extensions/bootstrap.py @@ -100,6 +100,7 @@ async def _main( server = grpc.aio.server(futures.ThreadPoolExecutor(max_workers=10)) servicer = servicer_cls( + ext_manager_service=ext_manager_stub, collections_service=collections_stub, environments_service=environments_stub, roles_service=roles_stub, diff --git a/perxis/extensions/extension_service.py b/perxis/extensions/extension_service.py index 94a479ec9da68fca1f0767df629d8839ed788119..3799b340c03214128f30ce680362764c27c17cde 100644 --- a/perxis/extensions/extension_service.py +++ b/perxis/extensions/extension_service.py @@ -9,7 +9,7 @@ import dataclasses 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.extensions import extension_service_pb2, extension_service_pb2_grpc, extension_pb2, manager_service_pb2_grpc from perxis.collaborators import collaborators_pb2_grpc from perxis.invitations import invitations_pb2_grpc from perxis.locales import locales_pb2_grpc @@ -27,6 +27,7 @@ from perxis.common import operation_pb2, operation_service_pb2_grpc, operation_s from perxis.collections import collections_pb2_grpc, collections_pb2 from perxis.environments import environments_pb2_grpc from perxis.extensions.extension_setup import ExtensionSetup +from perxis.extensions.item_models import AbstractItem def generate_operation_id() -> str: @@ -93,10 +94,12 @@ class ExtensionService( roles: list[roles_pb2.Role] = [] clients: list[clients_pb2.Client] = [] actions: list[dict] = [] + items: list[AbstractItem] = [] __operations: dict[str, OperationMeta] def __init__(self, + ext_manager_service: manager_service_pb2_grpc.ExtensionManagerServiceStub, collections_service: collections_pb2_grpc.CollectionsStub, environments_service: environments_pb2_grpc.EnvironmentsStub, roles_service: roles_pb2_grpc.RolesStub, @@ -115,6 +118,7 @@ class ExtensionService( channel: grpc.Channel, ): self.logger = logging.getLogger(__name__) + self.ext_manager_service = ext_manager_service self.collections_service = collections_service self.environments_service = environments_service self.roles_service = roles_service @@ -151,6 +155,20 @@ class ExtensionService( for action in self.actions or []: self.extension_setup.add_action(action) + services_list = [ + (service_name, getattr(self, service_name)) + for service_name + in self.__dict__ + if service_name.endswith("_service") + ] + services_list.append(("channel", self.channel, )) + + for item in self.items: + for rule in item.rules: + rule.bind_services(services_list) + + self.extension_setup.set_items(self.items) + @aiocron.crontab('0 * * * *', start=True) async def remove_old_operations(): self.remove_old_operations() @@ -251,10 +269,19 @@ class ExtensionService( request.space_id, request.env_id, request.force ) + errors_list += await self.additional_install_operations(operation_id, request, context) + self.result_log("установки", operation_id, request, errors_list) self.mark_operation_as_finished(operation_id, errors_list) + async def additional_install_operations(self, operation_id: str, request: extension_service_pb2.InstallRequest, context) -> list[str]: + """ + Для доп. логики установки + """ + + return [] + async def Install(self, request: extension_service_pb2.InstallRequest, context): operation_id = generate_operation_id() operation_description = "Установка расширения %s для окружения %s пространства %s. %s force" % ( @@ -274,11 +301,19 @@ class ExtensionService( async def _Uninstall(self, operation_id: str, request: extension_service_pb2.UninstallRequest, context): errors_list: list[str] = await self.extension_setup.uninstall(request.space_id, request.env_id, request.remove) + errors_list += await self.additional_uninstall_operations(operation_id, request, context) self.result_log("удаления", operation_id, request, errors_list) self.mark_operation_as_finished(operation_id, errors_list) + async def additional_uninstall_operations(self, operation_id: str, request: extension_service_pb2.UninstallRequest, context) -> list[str]: + """ + Для доп. логики удаления + """ + + return [] + async def Uninstall(self, request: extension_service_pb2.UninstallRequest, context): operation_id = generate_operation_id() operation_description = "Удаление расширения %s для окружения %s пространства %s. %s remove" % ( @@ -303,6 +338,13 @@ class ExtensionService( self.__operations[operation_id].mark_finished(errors_list) + async def additional_check_operations(self, operation_id: str, request: extension_service_pb2.CheckRequest, context) -> list[str]: + """" + Для доп. логики проверки + """ + + return [] + async def Check(self, request: extension_service_pb2.CheckRequest, context): operation_id = generate_operation_id() operation_description = "Проверка расширения %s для окружения %s пространства %s" % ( diff --git a/perxis/extensions/extension_setup.py b/perxis/extensions/extension_setup.py index 4fa44311314ebaa67e594fc68eb4cef1d4a020cf..3bfa04ec060501c855bf05400cf38cc5f2c08489 100644 --- a/perxis/extensions/extension_setup.py +++ b/perxis/extensions/extension_setup.py @@ -16,6 +16,8 @@ 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, ACTIONS_COLLECTION_ID +from perxis.extensions.item_models import AbstractItem, SyncPolicyItem +from perxis.provider import PerxisItemsWrapper logger = logging.getLogger(__name__) @@ -34,6 +36,7 @@ class ExtensionSetup: self.clients = [] self.roles = [] self.actions = [] + self.items = [] self.roles_service = roles_service self.clients_service = clients_service @@ -68,6 +71,9 @@ class ExtensionSetup: def set_clients(self, clients: list[clients_pb2.Client]): self.clients = clients + def set_items(self, items: list[AbstractItem]): + self.items = items + # Работа с ролями async def __remove_roles(self, space_id: str) -> list[str]: errors_list: list[str] = [] @@ -571,40 +577,337 @@ class ExtensionSetup: return errors_list + async def __check_items(self, space_id: str, env_id: str) -> list[str]: + errors_list: list[str] = [] + wrapper = PerxisItemsWrapper( + self.items_service, + ) + + for item in self.items: + all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id) + + if not all_rules_satisfied: + continue + + try: + message = await wrapper.find( + collection_id=item.collection_id, + env_id=env_id, + space_id=space_id, + limit=1, + offset=0, + fields=["id"], + filters=[f"{item.identifier_field} == '{item.identifier}'"] + ) + + if not message.total: + errors_list.append(f"Item {item.identifier} не найден") + except Exception as e: + errors_list.append( + f"Не удалось проверить item {item.identifier} " + f"коллекции {item.collection_id} - {e}" + ) + + return errors_list + + async def __update_items(self, space_id: str, env_id: str) -> list[str]: + errors_list: list[str] = [] + wrapper = PerxisItemsWrapper( + self.items_service, + ) + + for item in self.items: + all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id) + + if not all_rules_satisfied: + continue + + try: + message = await wrapper.find( + collection_id=item.collection_id, + env_id=env_id, + space_id=space_id, + limit=1, + offset=0, + filters=[f"{item.identifier_field} == '{item.identifier}'"] + ) + + if message.items: + item_in_perxis = message.items[0] + else: + item_in_perxis = None + except Exception as e: + if hasattr(e, "details") and "not found" in e.details(): + is_error = False + else: + is_error = True + + if is_error: + errors_list.append( + f"Не удалось получить item {item.identifier} " + f"коллекции {item.collection_id} - {e}" + ) + + continue + + try: + if item_in_perxis: + # Если установлен запрет на изменение item'ов + if not item.with_update: + continue + + # Для того чтобы не затереть изменения в perxis + # Нужно смержить данные. Логика работы: + # 1. Данные которые указаны в `data` в расширении - в приоритете, они замещают то что в perxis + # 2. Данные которые есть в perxis но нет в расширении - дополняются + await wrapper.update( + collection_id=item.collection_id, + item_id=item_in_perxis.id, + space_id=space_id, + env_id=env_id, + data=item.merge_data(MessageToDict(item_in_perxis)["data"]), + ) + else: + message = await wrapper.create( + collection_id=item.collection_id, + space_id=space_id, + env_id=env_id, + data=item.struct, + ) + + item_in_perxis = message.created + + await wrapper.publish( + item_id=item_in_perxis.id, + collection_id=item.collection_id, + space_id=space_id, + env_id=env_id + ) + except Exception as e: + errors_list.append( + f"Не удалось записать item {item.identifier} " + f"коллекции {item.collection_id} - {e}" + ) + + continue + + return errors_list + + async def __remove_items(self, space_id: str, env_id: str) -> list[str]: + errors_list: list[str] = [] + wrapper = PerxisItemsWrapper( + self.items_service, + ) + + for item in self.items: + all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id) + + if not all_rules_satisfied: + continue + + # Если установлен запрет на удаление item'ов + if not item.with_delete: + continue + + try: + message = await wrapper.find( + collection_id=item.collection_id, + env_id=env_id, + space_id=space_id, + limit=1, + offset=0, + fields=["id"], + filters=[f"{item.identifier_field} == '{item.identifier}'"] + ) + + if message.items: + await wrapper.delete( + item_id=message.items[0].id, + collection_id=message.items[0].collection_id, + space_id=space_id, + env_id=env_id, + ) + except Exception as e: + if hasattr(e, "details") and "not found" in e.details(): + is_error = False + else: + is_error = True + + if is_error: + errors_list.append( + f"Не удалось удалить item {item.identifier} " + f"коллекции {item.collection_id} - {e}" + ) + + return errors_list + + async def __update_view_role(self, space_id: str, env_id: str, mode: str = "add") -> list[str]: + errors = [] + + # Нужны только относящиеся к синхронизации элементы + items_for_view_role = [ + item + for item + in self.items + if (isinstance(item, SyncPolicyItem) or item.collection_id == "hoop_item_sync_policies") + and item.data["export_view"] + and await item.all_rules_is_satisfied(space_id=space_id, env_id=env_id) + ] + + if not items_for_view_role: + return errors + + try: + message = await self.roles_service.Get( + roles_pb2.GetRequest( + space_id=space_id, + role_id="view" + ) + ) + + role = message.role + except grpc.RpcError as e: + if "not found" not in e.details(): + errors.append(f"Не удалось получить роль view, {e.details()}") + + role = None + + if not role: + try: + message = await self.roles_service.Create( + roles_pb2.CreateRequest( + role=roles_pb2.Role( + id="view", + space_id=space_id, + description="Роль для view коллекций", + rules=[], + environments=["*"], + allow_management=False, + ) + ), + ) + + role = message.created + except grpc.RpcError as e: + errors.append(f"Не удалось создать роль view, {e.details()}") + + return errors + + # Произвести мерж правил доступа + for item in items_for_view_role: + actions_from_item = [] + + if not item.data["deny_read"]: + actions_from_item.append(common_pb2.READ) + + if not item.data["deny_create"]: + actions_from_item.append(common_pb2.CREATE) + + if not item.data["deny_publish"]: + actions_from_item.append(common_pb2.UPDATE) + + if not item.data["deny_delete"]: + actions_from_item.append(common_pb2.DELETE) + + rule_was_found = False + for rule in role.rules: + if rule.collection_id != item.data["collection"]: + continue + + rule_was_found = True + + if mode == "add": + modified_actions = list(set(list(rule.actions) + actions_from_item)) + else: + modified_actions = [ + action + for action + in rule.actions + if action not in actions_from_item + ] + + rule.actions[:] = modified_actions + + # Если правила для коллекции нет и при этом доступы нужно __добавлять__ + if not rule_was_found and mode == "add": + role.rules.append( + common_pb2.Rule( + collection_id=item.data["collection"], + actions=actions_from_item, + ) + ) + + try: + await self.roles_service.Update( + roles_pb2.UpdateRequest( + role=role + ) + ) + except grpc.RpcError as e: + errors.append(f"Не удалось обновить роль view, {e.details()}") + + return errors + + 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) + try: + 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) + errors += await self.__update_items(space_id, env_id) + errors += await self.__update_view_role(space_id, env_id) + except Exception as e: + logger.exception(e) + errors.append(f"Во время установки было необработанное исключение - {e}") return errors async def update(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) + try: + 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) + errors += await self.__update_items(space_id, env_id) + errors += await self.__update_view_role(space_id, env_id) + except Exception as e: + logger.exception(e) + errors.append(f"Во время обновления было необработанное исключение - {e}") return errors async def check(self, space_id: str, env_id: str) -> list[str]: - 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) + errors = [] + + try: + 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) + errors += await self.__check_items(space_id, env_id) + except Exception as e: + logger.exception(e) + errors.append(f"Во время проверки было необработанное исключение - {e}") return errors async def uninstall(self, space_id: str, env_id: str, use_remove: bool) -> list[str]: errors = [] if use_remove: - 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) + try: + 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) + errors += await self.__remove_items(space_id, env_id) + errors += await self.__update_view_role(space_id, env_id, "remove") + except Exception as e: + logger.exception(e) + errors.append(f"Во время удаления данных было необработанное исключение - {e}") return errors diff --git a/perxis/extensions/item_models.py b/perxis/extensions/item_models.py new file mode 100644 index 0000000000000000000000000000000000000000..078ed22feb143938bbc0ed65dae5ae988af4bb97 --- /dev/null +++ b/perxis/extensions/item_models.py @@ -0,0 +1,148 @@ +import abc + +from google.protobuf.struct_pb2 import Struct + +from perxis.extensions.item_rules import AbstractRule, IfCollectionExists, IfExtensionInstalled + + +class AbstractItem(metaclass=abc.ABCMeta): + """ + Абстрактный класс для item'а. Нужен для определения общих свойств без реализации какого + то конкретного конструктора. + """ + + collection_id: str + data: dict + + rules: list[AbstractRule] + identifier_field: str = "id" + with_update: bool + with_delete: bool + + @property + def identifier(self): + return self.data[self.identifier_field] + + @property + def struct(self) -> Struct: + s = Struct() + s.update(self.data) + + return s + + async def all_rules_is_satisfied(self, space_id: str, env_id: str) -> bool: + return all( + [ + await rule(item=self, space_id=space_id, env_id=env_id) + for rule + in self.rules + ] + ) + + def merge_data(self, data_from_perxis: dict) -> Struct: + """ + Мерж данных из перксиса и расширения. В приоритете данные расширения, если что-то + из них было вручную изменено в perxis - значение будет затёрто + """ + + s = Struct() + s.update({ + **data_from_perxis, + **self.data, + }) + + return s + + +class Item(AbstractItem): + """ + Общий класс для любого Item + """ + + def __init__( + self, + collection_id: str, + data: dict, + with_update: bool = True, + with_delete: bool = True, + identifier_field: str = "id", + rules: list[AbstractRule] | None = None + ): + self.collection_id = collection_id + self.data = data + self.identifier_field = identifier_field + self.rules = rules or [] + self.with_update = with_update + self.with_delete = with_delete + + +class DataSourceItem(AbstractItem): + """ + Класс для системной коллекции web_datasources + """ + + def __init__( + self, + collection_id: str, + rules: list[AbstractRule] | None = None, + query: str = "", + content_type: str = "", + exclude: bool = False, + with_update: bool = True, + with_delete: bool = True, + ): + self.collection_id = "web_datasources" + self.rules = rules or [IfCollectionExists()] + self.data = { + "id": collection_id, + "collection": collection_id, + "query": query, + "content_type": content_type, + "exclude": exclude, + } + + self.with_update = with_update + self.with_delete = with_delete + + +class SyncPolicyItem(AbstractItem): + """ + Класс для коллекции hoop_item_sync_policies расширения perxishoop + """ + + identifier_field = "collection" + + def __init__( + self, + collection_id: str, + name: str = "", + key: str = "id", + export_view: bool = True, + remove_collection: bool = False, + deny_publish: bool = True, + deny_delete: bool = True, + deny_create: bool = True, + deny_read: bool = False, + hidden: bool = False, + rules: list[AbstractRule] | None = None, + with_update: bool = True, + with_delete: bool = True, + ): + self.collection_id = "hoop_item_sync_policies" + self.rules = rules or [IfExtensionInstalled("perxishoop")] + + self.data = { + "name": name or f"Коллекция {collection_id}", + "collection": collection_id, + "key": key, + "export_view": export_view, + "remove_collection": remove_collection, + "deny_publish": deny_publish, + "deny_delete": deny_delete, + "deny_create": deny_create, + "deny_read": deny_read, + "hidden": hidden, + } + + self.with_update = with_update + self.with_delete = with_delete \ No newline at end of file diff --git a/perxis/extensions/item_rules.py b/perxis/extensions/item_rules.py new file mode 100644 index 0000000000000000000000000000000000000000..106a98e28a942b7b4da215ec468227e30d7a8e01 --- /dev/null +++ b/perxis/extensions/item_rules.py @@ -0,0 +1,111 @@ +import abc +import typing +import logging + +from perxis.collections import collections_pb2 +from perxis.extensions import manager_service_pb2 + + +if typing.TYPE_CHECKING: + from perxis.extensions.item_models import AbstractItem + + +class AbstractRule(metaclass=abc.ABCMeta): + """ + Абстрактное правило для обработки создания item'ов в системе. Правила могут + использоваться в кейсах: + 1. В случае если item опционален и невозможность его создания не приведёт к невозможности + использовать само расширение + 2. Если при создании item'а нужно обогатить его данные чем-то внешним, не относящимся к + текущему расширению. Например - при создании item'ов для цепочки сервиса уведомлений. + Каждый последующий item может с помощью правил получать результат создания предыдущего + и на базе этих данных формировать ref + + Так как сервисов в ExtensionService очень много, для того чтобы код их проброса в объект rule + не был излишне длинным - это делается неявным пробросом свойств из ExtensionService. Таким + образом и код короче получается, и в случае если будут добавлены какие то новые сервисы они + автоматически будут проброшены сюда. + """ + + @property + def rule_name(self): + return self.__class__.__name__ + + @property + def logger(self): + return logging.getLogger(self.rule_name) + + def bind_services(self, service_list: list[tuple[str, object]]): + """ + Нужно для автоматического подключения сервисов из ExtensionService. Проблема + в том что их очень много и если руками явно указывать - будет большой boilerplate + """ + + for service_name, service in service_list: + setattr(self, service_name, service) + + @abc.abstractmethod + async def act(self, item: "AbstractItem", space_id: str, env_id: str): + raise NotImplementedError() + + async def __call__(self, item: "AbstractItem", space_id: str, env_id: str) -> bool: + try: + await self.act(item, space_id, env_id) + + return True + except Exception as e: + self.logger.warning( + f"Правило {self.rule_name} не " + f"сработало для {item.collection_id} (item {item.identifier}), " + f"ошибка - {e}" + ) + + return False + + +class IfCollectionExists(AbstractRule): + """ + Правило для опциональных item'ов которые нужно создавать в случае наличия + определённой коллекции в perxis. Может быть полезно если коллекция широко используется + но в данный момент устанавливается только вручную и не привязана ни к одному из расширений + """ + + async def act(self, item: "AbstractItem", space_id: str, env_id: str): + message = await self.collections_service.Get( + collections_pb2.GetRequest( + space_id=space_id, + env_id=env_id, + collection_id=item.collection_id + ) + ) + + if not message.collection: + raise ValueError(f"Коллекция {item.collection_id} не найдена") + + +class IfExtensionInstalled(AbstractRule): + """ + Правило для опциональных item'ов которые нужно создавать только в случае + наличия определённого расширения в perxis + """ + + required_extension: str + + def __init__(self, required_extension: str): + self.required_extension = required_extension + + async def act(self, item: "AbstractItem", space_id: str, env_id: str): + registered_extensions = await self.ext_manager_service.ListRegisteredExtensions( + manager_service_pb2.ListRegisteredExtensionsRequest() + ) + + ext_is_installed = False + for ext in registered_extensions.extensions: + if ext.extension == self.required_extension: + ext_is_installed = True + break + + if not ext_is_installed: + raise ValueError( + f"Расширение {self.required_extension} не установлено в perxis" + ) diff --git a/perxis/extensions/utils.py b/perxis/extensions/utils.py index b5bbc4140df4b248d9dfc627732f45d7b7c7f7fb..d45242aebf6198710f77eeae98e4f40c88c6afca 100644 --- a/perxis/extensions/utils.py +++ b/perxis/extensions/utils.py @@ -1,6 +1,48 @@ from typing import Optional from perxis.extensions import manager_service_pb2 +from perxis.extensions.item_models import DataSourceItem, SyncPolicyItem + + +def datasource_items_from_collections( + collections_map: dict[str, str], + with_update: bool = True, + with_delete: bool = True, +) -> list[DataSourceItem]: + """ + Создание записей источников данных на базе маппинга коллекций + """ + + return [ + DataSourceItem( + collection_id=collection_id, + with_delete=with_delete, + with_update=with_update + ) + for collection_id in collections_map + if collection_id + ] + + +def sync_policies_from_collections( + collections_map: dict[str, str], + with_update: bool = True, + with_delete: bool = True, +) -> list[SyncPolicyItem]: + """ + Создание записей синхронизации коллекций на базе маппинга коллекций + """ + + return [ + SyncPolicyItem( + collection_id=collection_id, + name=collection_name, + with_update=with_update, + with_delete=with_delete, + ) + for collection_id, collection_name in collections_map.items() + if collection_id + ] def get_extension_descriptor( diff --git a/setup.py b/setup.py index 78b8248f27d70760372801d583b30e69737143a4..2fb87abce6dcdc999169ff3de916c98a1157bc9c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def load_requirements(): setup( name='perxis', - version='1.9.0', + version='1.10.0', description='Perxis python client', long_description=long_description, long_description_content_type='text/markdown',