perxis.extensions.extension_setup

Модуль содержит класс ExtensionSetup. Он используется для управления всеми данными расширения

   1"""
   2Модуль содержит класс ExtensionSetup. Он используется для управления всеми данными расширения
   3"""
   4
   5
   6import logging
   7
   8import grpc
   9import json
  10import time
  11import copy
  12import typing
  13
  14
  15from deepdiff import DeepDiff
  16from google.protobuf.json_format import MessageToDict
  17from perxis.collections import collections_pb2_grpc, collections_pb2
  18from perxis.roles import roles_pb2_grpc, roles_pb2
  19from perxis.items import items_pb2_grpc, items_pb2
  20from perxis.common import common_pb2
  21from perxis.clients import clients_pb2_grpc, clients_pb2
  22from perxis.environments import environments_pb2_grpc, environments_pb2
  23from perxis.extensions.actions import make_action_item, ACTIONS_COLLECTION_ID
  24from perxis.extensions.item_models import AbstractItem, SyncPolicyItem
  25from perxis.provider import PerxisItemsWrapper
  26
  27
  28logger = logging.getLogger(__name__)
  29
  30
  31class ExtensionSetup:
  32    """
  33        Attributes:
  34            collections_service (collections_pb2_grpc.CollectionsStub): ссылка на сервис коллекций
  35            environments_service (environments_pb2_grpc.EnvironmentsStub): ссылка на сервис окружений
  36            roles_service (roles_pb2_grpc.RolesStub): ссылка на сервис ролей
  37            clients_service (clients_pb2_grpc.ClientsStub): ссылка на сервис клиентов
  38            items_service (items_pb2_grpc.ItemsStub): ссылка на сервис item'ов
  39            collections (list[collections_pb2.Collection]): список коллекций расширения
  40            clients (list[clients_pb2.Client]): список клиентов расширения
  41            roles (list[roles_pb2.Role]): список ролей расширения
  42            items (list[AbstractItem]): список item'ов расширения
  43            __max_attempts_count (int): макс. кол-во попыток записи
  44            __sleep_time (int): время ожидания перед попыткой
  45
  46    """
  47
  48    def __init__(
  49            self,
  50            collections_service: collections_pb2_grpc.CollectionsStub,
  51            environments_service: environments_pb2_grpc.EnvironmentsStub,
  52            roles_service: roles_pb2_grpc.RolesStub,
  53            clients_service: clients_pb2_grpc.ClientsStub,
  54            items_service: items_pb2_grpc.ItemsStub,
  55    ):
  56        self.collections = []
  57        self.clients = []
  58        self.roles = []
  59        self.actions = []
  60        self.items = []
  61
  62        self.roles_service = roles_service
  63        self.clients_service = clients_service
  64        self.collections_service = collections_service
  65        self.environments_service = environments_service
  66        self.items_service = items_service
  67
  68        self.__max_attempts_count = 5
  69        self.__sleep_time = 1
  70
  71    def add_action(self, action: dict):
  72        self.actions.append(action)
  73
  74    def set_actions(self, actions: list[dict]):
  75        self.actions = actions
  76
  77    def add_collection(self, collection: collections_pb2.Collection):
  78        self.collections.append(collection)
  79
  80    def set_collections(self, collections: list[collections_pb2.Collection]):
  81        self.collections = collections
  82
  83    def add_role(self, role: roles_pb2.Role):
  84        self.roles.append(role)
  85
  86    def set_roles(self, roles: list[roles_pb2.Role]):
  87        self.roles = roles
  88
  89    def add_client(self, client: clients_pb2.Client):
  90        self.clients.append(client)
  91
  92    def set_clients(self, clients: list[clients_pb2.Client]):
  93        self.clients = clients
  94
  95    def set_items(self, items: list[AbstractItem]):
  96        self.items = items
  97
  98    # Работа с ролями
  99    async def __remove_roles(self, space_id: str) -> list[str]:
 100        """
 101            Метод для удаления ролей из пространства. Возвращает массив с ошибками
 102
 103            Arguments:
 104                  space_id (str): идентификатор пространства
 105
 106            Returns:
 107                  list[str]
 108        """
 109
 110        errors_list: list[str] = []
 111
 112        for role in self.roles:
 113            try:
 114                await self.roles_service.Delete(
 115                    roles_pb2.DeleteRequest(
 116                        space_id=space_id, role_id=role.id
 117                    )
 118                )
 119            except grpc.RpcError as e:
 120                # Если роли не существует считать это ошибкой не надо
 121                if "not found" not in e.details():
 122                    errors_list.append(f"Не удалось удалить роль {role.id}, {e.details()}")
 123
 124        return errors_list
 125
 126    async def __check_roles(self, space_id: str) -> list[str]:
 127        """
 128            Метод для проверки ролей из пространства. Возвращает массив с ошибками
 129
 130            Arguments:
 131                  space_id (str): идентификатор пространства
 132
 133            Returns:
 134                  list[str]
 135        """
 136
 137        errors_list = []
 138
 139        for role in self.roles:
 140            try:
 141                await self.roles_service.Get(
 142                    roles_pb2.GetRequest(space_id=space_id, role_id=role.id)
 143                )
 144            except grpc.RpcError as e:
 145                errors_list.append(f"Не удалось получить роль {role.id}, ошибка {e.details()}")
 146
 147        return errors_list
 148
 149    async def __update_roles(self, space_id: str, env_id: str) -> list[str]:
 150        """
 151            Метод для установки ролей в пространство. Возвращает массив с ошибками. Права доступа
 152            которые были установлены в perxis вручную или другим расширением не затираются.
 153
 154            Arguments:
 155                  space_id (str): идентификатор пространства
 156                  env_id (str): идентификатор окружения
 157
 158                Returns:
 159                      list[str]
 160            """
 161
 162        errors_list = []
 163
 164        for local_role in self.roles:
 165            try:
 166                get_response = await self.roles_service.Get(
 167                    roles_pb2.GetRequest(
 168                        space_id=space_id,
 169                        role_id=local_role.id
 170                    )
 171                )
 172
 173                role = get_response.role
 174            except grpc.RpcError as e:
 175                if "not found" not in e.details():
 176                    errors_list.append(f"Не удалось получить роль {local_role.id}, {e.details()}")
 177                    continue
 178
 179                role = None
 180
 181            cloned_role = copy.deepcopy(local_role)
 182            cloned_role.space_id = space_id
 183
 184            if role:
 185                if not role.environments:
 186                    role.environments[:] = [env_id]
 187                elif env_id not in role.environments:
 188                    role.environments.append(env_id)
 189
 190                cloned_role.environments[:] = role.environments
 191
 192                # Произвести мерж правил доступа
 193                for exist_rule in role.rules:
 194                    was_found = False
 195
 196                    for local_rule in cloned_role.rules:
 197                        # Если запись с правами доступа на коллекцию найдена - правила нужно совместить
 198                        if local_rule.collection_id == exist_rule.collection_id:
 199                            was_found = True
 200
 201                            local_rule.actions[:] = list(set(list(local_rule.actions) + list(exist_rule.actions)))
 202
 203                            break
 204
 205                    # Если правило для коллекций не было найдено - его нужно добавить
 206                    if not was_found:
 207                        cloned_role.rules.append(common_pb2.Rule(
 208                            collection_id=exist_rule.collection_id,
 209                            actions=list(exist_rule.actions)
 210                        ))
 211
 212                try:
 213                    await self.roles_service.Update(
 214                        roles_pb2.UpdateRequest(
 215                            role=cloned_role
 216                        )
 217                    )
 218                except grpc.RpcError as e:
 219                    errors_list.append(f"Не удалось обновить роль {local_role.id}, {e.details()}")
 220            else:
 221                if env_id not in cloned_role.environments:
 222                    cloned_role.environments.append(env_id)
 223
 224                try:
 225                    cloned_role = copy.deepcopy(local_role)
 226                    cloned_role.space_id = space_id
 227
 228                    response = await self.roles_service.Create(
 229                        roles_pb2.CreateRequest(
 230                            role=cloned_role
 231                        ),
 232                    )
 233                except grpc.RpcError as e:
 234                    # На этапе install считается что ролей __нет__. При install с указанием force роли предварительно
 235                    # удаляются
 236                    errors_list.append(f"Не удалось создать роль {local_role.id}, {e.details()}")
 237
 238        return errors_list
 239
 240    # Работа с клиентами
 241    async def __check_clients(self, space_id: str) -> list[str]:
 242        """
 243            Метод для проверки клиентов в пространстве. Возвращает массив с ошибками
 244
 245            Arguments:
 246                  space_id (str): идентификатор пространства
 247
 248            Returns:
 249                  list[str]
 250        """
 251
 252        errors_list = []
 253
 254        for client in self.clients:
 255            try:
 256                await self.clients_service.Get(
 257                    clients_pb2.GetRequest(space_id=space_id, id=client.id)
 258                )
 259            except grpc.RpcError as e:
 260                errors_list.append(f"Не удалось получить клиент {client.id}, ошибка {e.details()}")
 261
 262        return errors_list
 263
 264    async def __remove_clients(self, space_id: str) -> list[str]:
 265        """
 266            Метод для удаления клиентов из пространства. Возвращает массив с ошибками
 267
 268            Arguments:
 269                  space_id (str): идентификатор пространства
 270
 271            Returns:
 272                  list[str]
 273        """
 274
 275        errors_list: list[str] = []
 276
 277        for client in self.clients:
 278            try:
 279                await self.clients_service.Delete(
 280                    clients_pb2.DeleteRequest(
 281                        space_id=space_id, id=client.id
 282                    )
 283                )
 284            except grpc.RpcError as e:
 285                # Отсутствие клиента ошибкой не считается
 286                if "not found" not in e.details():
 287                    errors_list.append(f"Не удалось удалить клиент {client.id}, {e.details()}")
 288
 289        return errors_list
 290
 291    async def __update_clients(self, space_id: str) -> list[str]:
 292        """
 293            Метод для создания / обновления клиентов в пространстве. Возвращает массив с ошибками.
 294            Свойства oauth и api_key не затираются в случае если клиент уже был создан
 295
 296            Arguments:
 297                  space_id (str): идентификатор пространства
 298
 299            Returns:
 300                  list[str]
 301        """
 302
 303        errors_list = []
 304
 305        for local_client in self.clients:
 306            try:
 307                # Перед обновлением клиента предварительно нужно получить текущую запись чтобы скопировать оттуда
 308                # токены. Иначе после обновления расширения перестанут работать все приложения которые использовали
 309                # токен клиента
 310                get_response = await self.clients_service.Get(
 311                    clients_pb2.GetRequest(
 312                        space_id=space_id,
 313                        id=local_client.id
 314                    )
 315                )
 316
 317                client = get_response.client
 318            except grpc.RpcError as e:
 319                if "not found" not in e.details():
 320                    errors_list.append(f"Не удалось получить клиент {local_client.id}, {e.details()}")
 321                    continue
 322
 323                client = None
 324
 325            if client:
 326                try:
 327                    # Нужно чтобы у клиента каждый раз не слетали данные токенов при переустановке
 328                    # свойства oauth, api_key и tls должны браться из __созданного__ клиента
 329                    new_client = clients_pb2.Client(
 330                        id=local_client.id,
 331                        space_id=space_id,
 332                        name=local_client.name,
 333                        description=local_client.description,
 334                        disabled=client.disabled,
 335                        role_id=local_client.role_id,
 336                        oauth=client.oauth or local_client.oauth,
 337                        api_key=client.api_key or local_client.api_key,
 338                        tls=client.tls
 339                    )
 340
 341                    await self.clients_service.Update(
 342                        clients_pb2.UpdateRequest(
 343                            client=new_client
 344                        )
 345                    )
 346                except grpc.RpcError as e:
 347                    errors_list.append(f"Не удалось обновить клиент {local_client.id}, {e.details()}")
 348            else:
 349                try:
 350                    cloned_client = copy.deepcopy(local_client)
 351                    cloned_client.space_id = space_id
 352
 353                    await self.clients_service.Create(
 354                        clients_pb2.CreateRequest(
 355                            client=cloned_client
 356                        ),
 357                    )
 358                except grpc.RpcError as e:
 359                    errors_list.append(f"Не удалось создать клиент {local_client.id}, {e.details()}")
 360
 361        return errors_list
 362
 363    # Работа с коллекциями
 364    async def __remove_collections(self, space_id: str, env_id: str) -> list[str]:
 365        """
 366            Метод для удаления коллекций из определённого окружения пространства. Возвращает массив с ошибками
 367
 368            Arguments:
 369                  space_id (str): идентификатор пространства
 370                  env_id (str): идентификатор окружения
 371
 372            Returns:
 373                  list[str]
 374        """
 375
 376        errors_list: list[str] = []
 377
 378        for collection in self.collections:
 379            try:
 380                await self.collections_service.Delete(
 381                    collections_pb2.DeleteRequest(
 382                        space_id=space_id, env_id=env_id, collection_id=collection.id
 383                    )
 384                )
 385            except grpc.RpcError as e:
 386                # Отсутствие коллекции это не ошибка
 387                if "not found" not in e.details():
 388                    errors_list.append(f"Не удалось удалить коллекцию {collection.id}, {e.details()}")
 389
 390        return errors_list
 391
 392    async def __check_collections(self, space_id: str, env_id: str) -> list[str]:
 393        """
 394            Метод для проверки коллекций из определённого окружения пространства. Возвращает массив с ошибками
 395
 396            Arguments:
 397                  space_id (str): идентификатор пространства
 398                  env_id (str): идентификатор окружения
 399
 400            Returns:
 401                  list[str]
 402        """
 403
 404        errors_list = []
 405
 406        for collection in self.collections:
 407            try:
 408                await self.collections_service.Get(
 409                    collections_pb2.GetRequest(space_id=space_id, env_id=env_id, collection_id=collection.id)
 410                )
 411            except grpc.RpcError as e:
 412                errors_list.append(f"Не удалось получить коллекцию {collection.id}, ошибка {e.details()}")
 413
 414        return errors_list
 415
 416    async def __update_collections(self, space_id: str, env_id: str) -> list[str]:
 417        """
 418            Метод __обновления__ коллекций. Миграция окружения требуется
 419            только в случае если одна или несколько схем коллекций изменялись. Алгоритм работы:
 420            1. Получить фактически существующую коллекцию из БД
 421            2. Обновить её в perxis
 422            3. Сравнить схему коллекции в расширении и в perxis
 423            4. Если схема изменена - обновить схему в perxis
 424            5. Если обновлялась хотя бы одна схема в perxis - запустить миграцию окружения
 425
 426            Arguments:
 427                  space_id (str): идентификатор пространства
 428                  env_id (str): идентификатор окружения
 429
 430            Returns:
 431                  list[str]
 432        """
 433
 434        need_to_migrate_environment = False
 435        errors_list = []
 436
 437        for local_collection in self.collections:
 438            try:
 439                # Необходимо получить текущую версию коллекции для того чтобы сравнить схемы
 440                get_response = await self.collections_service.Get(
 441                    collections_pb2.GetRequest(space_id=space_id, env_id=env_id, collection_id=local_collection.id)
 442                )
 443
 444                collection = get_response.collection
 445            except grpc.RpcError as e:
 446                collection = None
 447
 448            cloned_collection = copy.deepcopy(local_collection)
 449            cloned_collection.space_id = space_id
 450            cloned_collection.env_id = env_id
 451
 452            # Коллекция может быть не найдена в случае если она добавлена в новой версии
 453            if not collection:
 454                try:
 455                    create_response = await self.collections_service.Create(
 456                        collections_pb2.CreateRequest(collection=cloned_collection)
 457                    )
 458
 459                    collection = create_response.created
 460                except grpc.RpcError as e:
 461                    errors_list.append(f"Не удалось создать коллекцию {local_collection.id}, {e.details()}")
 462
 463                    continue
 464            else:
 465                # если view-коллекция то не устанавливаем схему
 466                if MessageToDict(collection).get("view"):
 467                    continue
 468                try:
 469                    cloned_collection = copy.deepcopy(local_collection)
 470                    cloned_collection.space_id = space_id
 471                    cloned_collection.env_id = env_id
 472
 473                    await self.collections_service.Update(
 474                        collections_pb2.UpdateRequest(collection=cloned_collection)
 475                    )
 476                except grpc.RpcError as e:
 477                    errors_list.append(f"Не удалось обновить коллекцию {local_collection.id}, {e.details()}")
 478
 479                    continue
 480
 481            diff = DeepDiff(
 482                json.loads(collection.schema or "{}"),
 483                json.loads(local_collection.schema or "{}"),
 484                ignore_numeric_type_changes=True,
 485                exclude_paths=["root['loaded']"]
 486            )
 487            if diff:
 488                need_to_migrate_environment = True
 489
 490                set_schema_error_message = await self.__set_collection_schema(
 491                    space_id, env_id, local_collection.id, local_collection.schema
 492                )
 493                if set_schema_error_message:
 494                    errors_list.append(set_schema_error_message)
 495
 496        if need_to_migrate_environment:
 497            migrate_environment_error_message = await self.__migrate_environment(space_id, env_id)
 498            if migrate_environment_error_message:
 499                errors_list.append(migrate_environment_error_message)
 500
 501        return errors_list
 502
 503    async def __migrate_environment(self, space_id: str, env_id: str) -> typing.Optional[str]:
 504        """
 505            Метод для миграции окружения.
 506            Так как perxis может не сразу выставить коллекции / окружению статус ready операцию необходимо выполнять
 507            с попытками
 508            Arguments:
 509                  space_id (str): идентификатор пространства
 510                  env_id (str): идентификатор окружения
 511
 512            Returns:
 513                  str | None
 514        """
 515
 516        attempt = 0
 517        is_ok = False
 518        error_message = None
 519
 520        while attempt <= self.__max_attempts_count and not is_ok:
 521            time.sleep(self.__sleep_time)
 522
 523            try:
 524                await self.environments_service.Migrate(environments_pb2.MigrateRequest(
 525                    space_id=space_id,
 526                    env_id=env_id,
 527                    options=environments_pb2.MigrateOptions(
 528                        wait=True
 529                    )
 530                ))
 531
 532                is_ok = True
 533            except grpc.RpcError as e:
 534                # Если не удалось мигрировать окружение по любой причине кроме подготовки - это ошибка
 535                if "is preparing" not in e.details():
 536                    error_message = e.details()
 537
 538                    # Для принудительного выхода из цикла
 539                    attempt = self.__max_attempts_count
 540
 541            attempt += 1
 542
 543        return error_message
 544
 545    async def __set_collection_schema(self, space_id: str, env_id: str, collection_id: str, schema: str) -> typing.Optional[str]:
 546        """
 547            Метод для установки схемы коллекции в определённом окружении пространства
 548            Так как perxis может не сразу выставить коллекции / окружению статус ready операцию необходимо выполнять
 549            с попытками
 550            Arguments:
 551                  space_id (str): идентификатор пространства
 552                  env_id (str): идентификатор окружения
 553                  collection_id (str): идентификатор коллекции
 554                  schema (str): схема коллекции в формате json
 555
 556            Returns:
 557                  str | None
 558        """
 559
 560        attempt = 0
 561        is_ok = False
 562        error_message = None
 563
 564        while attempt <= self.__max_attempts_count and not is_ok:
 565            time.sleep(self.__sleep_time)
 566
 567            try:
 568                await self.collections_service.SetSchema(
 569                    collections_pb2.SetSchemaRequest(
 570                        space_id=space_id,
 571                        env_id=env_id,
 572                        collection_id=collection_id,
 573                        schema=schema
 574                    )
 575                )
 576
 577                is_ok = True
 578            except grpc.RpcError as e:
 579                # Если не удалось установить схему по любой причине кроме подготовки - это ошибка
 580                if "is preparing" not in e.details():
 581                    error_message = e.details()
 582
 583                    # Для принудительного выхода из цикла
 584                    attempt = self.__max_attempts_count
 585
 586            attempt += 1
 587
 588        return error_message
 589
 590    # Работа с действиями
 591    async def __check_actions(self, space_id: str, env_id: str) -> list[str]:
 592        """
 593            Метод для проверки действий из определённого окружения пространства. Возвращает массив с ошибками
 594
 595            Arguments:
 596                  space_id (str): идентификатор пространства
 597                  env_id (str): идентификатор окружения
 598
 599            Returns:
 600                  list[str]
 601        """
 602
 603        errors_list = []
 604
 605        ids = [data["id"] for data in self.actions if data.get("id")]
 606
 607        items = []
 608
 609        try:
 610            result = await self.items_service.Find(
 611                items_pb2.FindRequest(
 612                    space_id=space_id,
 613                    env_id=env_id,
 614                    collection_id=ACTIONS_COLLECTION_ID,
 615                    filter=items_pb2.Filter(q=[f"id in {ids}"]),
 616                    options=items_pb2.FindOptions(
 617                        options=common_pb2.FindOptions(
 618                            page_num=0, page_size=len(ids)
 619                        )
 620                    ),
 621                )
 622            )
 623
 624            items = result.items
 625        except grpc.RpcError as e:
 626            errors_list.append(f"Не удалось получить данные о действиях, ошибка {e.details()}")
 627        finally:
 628            found_ids = [item.id for item in items]
 629
 630            for action_id in found_ids:
 631                if action_id not in ids:
 632                    errors_list.append(f"Действие {action_id} не найдено")
 633
 634        return errors_list
 635
 636    async def __update_actions(self, space_id: str, env_id: str) -> list[str]:
 637        """
 638            Метод для создания / обновления действий из определённого окружения пространства. Возвращает массив с ошибками
 639
 640            Arguments:
 641                  space_id (str): идентификатор пространства
 642                  env_id (str): идентификатор окружения
 643
 644            Returns:
 645                  list[str]
 646        """
 647
 648        errors_list = []
 649        not_found = False
 650
 651        for action in self.actions:
 652            action_item = make_action_item(space_id, env_id, action)
 653
 654            try:
 655                await self.items_service.Update(
 656                    items_pb2.UpdateRequest(
 657                        item=items_pb2.Item(
 658                            id=action_item.id,
 659                            space_id=space_id,
 660                            env_id=env_id,
 661                            collection_id=ACTIONS_COLLECTION_ID,
 662                            data=action_item.data,
 663                        )
 664                    )
 665                )
 666            except grpc.RpcError as e:
 667                if "not found" not in e.details():
 668                    errors_list.append(f"Не удалось обновить действие {action_item.id}, {e.details()}")
 669                    continue
 670
 671                not_found = True
 672            except Exception as e:
 673                errors_list.append(f"Не удалось обновить действие {action_item.id}, {e}")
 674
 675                continue
 676
 677            if not_found:
 678                try:
 679                    await self.items_service.Create(
 680                        items_pb2.CreateRequest(
 681                            item=action_item
 682                        )
 683                    )
 684                except grpc.RpcError as e:
 685                    errors_list.append(f"Не удалось создать действие {action_item.id}, {e.details()}")
 686                except Exception as e:
 687                    errors_list.append(f"Не удалось создать действие {action_item.id}, {e}")
 688
 689                    continue
 690
 691            try:
 692                await self.items_service.Publish(
 693                    items_pb2.PublishRequest(
 694                        item=action_item
 695                    )
 696                )
 697            except grpc.RpcError as e:
 698                # Если action уже опубликован
 699                if "item cannot be published in this state" not in e.details():
 700                    errors_list.append(f"Не удалось опубликовать действие {action_item.id}, {e.details()}")
 701            except Exception as e:
 702                errors_list.append(f"Не удалось опубликовать действие {action_item.id}, {e}")
 703
 704        return errors_list
 705
 706    async def __remove_actions(self, space_id: str, env_id: str) -> list[str]:
 707        """
 708            Метод для удаления действий из определённого окружения пространства. Возвращает массив с ошибками
 709
 710            Arguments:
 711                  space_id (str): идентификатор пространства
 712                  env_id (str): идентификатор окружения
 713
 714            Returns:
 715                  list[str]
 716        """
 717
 718        errors_list = []
 719        for action in self.actions:
 720            action_item = make_action_item(space_id, env_id, action)
 721
 722            try:
 723                await self.items_service.Delete(
 724                    items_pb2.DeleteRequest(
 725                        item=items_pb2.Item(
 726                            id=action_item.id,
 727                            space_id=space_id,
 728                            env_id=env_id,
 729                            collection_id=action_item.collection_id,
 730                        ),
 731                        options=items_pb2.DeleteOptions(
 732                            update_attrs=True,
 733                            erase=True
 734                        )
 735                    )
 736                )
 737            except grpc.RpcError as e:
 738                # Отсутствие действия это не ошибка
 739                if "not found" not in e.details():
 740                    errors_list.append(f"Не удалось удалить действие {action.get('id', 'n/a')}, {e.details()}")
 741            except Exception as e:
 742                errors_list.append(f"Не удалось удалить действие - {e}")
 743
 744        return errors_list
 745
 746    async def __check_items(self, space_id: str, env_id: str) -> list[str]:
 747        """
 748            Метод для item'ов действий из определённого окружения пространства. Возвращает массив с ошибками
 749
 750            Arguments:
 751                  space_id (str): идентификатор пространства
 752                  env_id (str): идентификатор окружения
 753
 754            Returns:
 755                  list[str]
 756        """
 757
 758        errors_list: list[str] = []
 759        wrapper = PerxisItemsWrapper(
 760            self.items_service,
 761        )
 762
 763        for item in self.items:
 764            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 765
 766            if not all_rules_satisfied:
 767                continue
 768
 769            try:
 770                message = await wrapper.find(
 771                    collection_id=item.collection_id,
 772                    env_id=env_id,
 773                    space_id=space_id,
 774                    limit=1,
 775                    offset=0,
 776                    fields=["id"],
 777                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 778                )
 779
 780                if not message.total:
 781                    errors_list.append(f"Item {item.identifier} не найден")
 782            except Exception as e:
 783                errors_list.append(
 784                    f"Не удалось проверить item {item.identifier} "
 785                    f"коллекции {item.collection_id} - {e}"
 786                )
 787
 788        return errors_list
 789
 790    async def __update_items(self, space_id: str, env_id: str) -> list[str]:
 791        """
 792            Метод для создания / обновления item'ов из определённого окружения пространства. Возвращает массив с ошибками
 793
 794            Arguments:
 795                  space_id (str): идентификатор пространства
 796                  env_id (str): идентификатор окружения
 797
 798            Returns:
 799                  list[str]
 800        """
 801
 802        errors_list: list[str] = []
 803        wrapper = PerxisItemsWrapper(
 804            self.items_service,
 805        )
 806
 807        for item in self.items:
 808            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 809
 810            if not all_rules_satisfied:
 811                continue
 812
 813            try:
 814                message = await wrapper.find(
 815                    collection_id=item.collection_id,
 816                    env_id=env_id,
 817                    space_id=space_id,
 818                    limit=1,
 819                    offset=0,
 820                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 821                )
 822
 823                if message.items:
 824                    item_in_perxis = message.items[0]
 825                else:
 826                    item_in_perxis = None
 827            except Exception as e:
 828                if hasattr(e, "details") and "not found" in e.details():
 829                    is_error = False
 830                else:
 831                    is_error = True
 832
 833                if is_error:
 834                    errors_list.append(
 835                        f"Не удалось получить item {item.identifier} "
 836                        f"коллекции {item.collection_id} - {e}"
 837                    )
 838
 839                continue
 840
 841            try:
 842                if item_in_perxis:
 843                    # Если установлен запрет на изменение item'ов
 844                    if not item.with_update:
 845                        continue
 846
 847                    # Для того чтобы не затереть изменения в perxis
 848                    # Нужно смержить данные. Логика работы:
 849                    # 1. Данные которые указаны в `data` в расширении - в приоритете, они замещают то что в perxis
 850                    # 2. Данные которые есть в perxis но нет в расширении - дополняются
 851                    await wrapper.update(
 852                        collection_id=item.collection_id,
 853                        item_id=item_in_perxis.id,
 854                        space_id=space_id,
 855                        env_id=env_id,
 856                        data=item.merge_data(MessageToDict(item_in_perxis)["data"]),
 857                    )
 858                else:
 859                    message = await wrapper.create(
 860                        collection_id=item.collection_id,
 861                        space_id=space_id,
 862                        env_id=env_id,
 863                        data=item.struct,
 864                    )
 865
 866                    item_in_perxis = message.created
 867
 868                await wrapper.publish(
 869                    item_id=item_in_perxis.id,
 870                    collection_id=item.collection_id,
 871                    space_id=space_id,
 872                    env_id=env_id
 873                )
 874            except Exception as e:
 875                errors_list.append(
 876                    f"Не удалось записать item {item.identifier} "
 877                    f"коллекции {item.collection_id} - {e}"
 878                )
 879
 880                continue
 881
 882        return errors_list
 883
 884    async def __remove_items(self, space_id: str, env_id: str) -> list[str]:
 885        """
 886            Метод для удаления item'ов из определённого окружения пространства. Возвращает массив с ошибками
 887
 888            Arguments:
 889                  space_id (str): идентификатор пространства
 890                  env_id (str): идентификатор окружения
 891
 892            Returns:
 893                  list[str]
 894        """
 895
 896        errors_list: list[str] = []
 897        wrapper = PerxisItemsWrapper(
 898            self.items_service,
 899        )
 900
 901        for item in self.items:
 902            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 903
 904            if not all_rules_satisfied:
 905                continue
 906
 907            # Если установлен запрет на удаление item'ов
 908            if not item.with_delete:
 909                continue
 910
 911            try:
 912                message = await wrapper.find(
 913                    collection_id=item.collection_id,
 914                    env_id=env_id,
 915                    space_id=space_id,
 916                    limit=1,
 917                    offset=0,
 918                    fields=["id"],
 919                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 920                )
 921
 922                if message.items:
 923                    await wrapper.delete(
 924                        item_id=message.items[0].id,
 925                        collection_id=message.items[0].collection_id,
 926                        space_id=space_id,
 927                        env_id=env_id,
 928                    )
 929            except Exception as e:
 930                if hasattr(e, "details") and "not found" in e.details():
 931                    is_error = False
 932                else:
 933                    is_error = True
 934
 935                if is_error:
 936                    errors_list.append(
 937                        f"Не удалось удалить item {item.identifier} "
 938                        f"коллекции {item.collection_id} - {e}"
 939                    )
 940
 941        return errors_list
 942
 943    async def __update_view_role(self, space_id: str, env_id: str, mode: str = "add") -> list[str]:
 944        """
 945            Метод для создания / обновления роли view из определённого окружения пространства. Возвращает массив с ошибками
 946
 947            Arguments:
 948                  space_id (str): идентификатор пространства
 949                  env_id (str): идентификатор окружения
 950
 951            Returns:
 952                  list[str]
 953        """
 954
 955        errors = []
 956
 957        # Нужны только относящиеся к синхронизации элементы
 958        items_for_view_role = [
 959            item
 960            for item
 961            in self.items
 962            if (isinstance(item, SyncPolicyItem) or item.collection_id == "hoop_item_sync_policies")
 963            and item.data["export_view"]
 964            and await item.all_rules_is_satisfied(space_id=space_id, env_id=env_id)
 965        ]
 966
 967        if not items_for_view_role:
 968            return errors
 969
 970        try:
 971            message = await self.roles_service.Get(
 972                roles_pb2.GetRequest(
 973                    space_id=space_id,
 974                    role_id="view"
 975                )
 976            )
 977
 978            role = message.role
 979        except grpc.RpcError as e:
 980            if "not found" not in e.details():
 981                errors.append(f"Не удалось получить роль view, {e.details()}")
 982
 983            role = None
 984
 985        if not role:
 986            try:
 987                message = await self.roles_service.Create(
 988                    roles_pb2.CreateRequest(
 989                        role=roles_pb2.Role(
 990                            id="view",
 991                            space_id=space_id,
 992                            description="Роль для view коллекций",
 993                            rules=[],
 994                            environments=["*"],
 995                            allow_management=False,
 996                        )
 997                    ),
 998                )
 999
1000                role = message.created
1001            except grpc.RpcError as e:
1002                errors.append(f"Не удалось создать роль view, {e.details()}")
1003
1004                return errors
1005
1006        # Произвести мерж правил доступа
1007        for item in items_for_view_role:
1008            actions_from_item = []
1009
1010            if not item.data["deny_read"]:
1011                actions_from_item.append(common_pb2.READ)
1012
1013            if not item.data["deny_create"]:
1014                actions_from_item.append(common_pb2.CREATE)
1015
1016            if not item.data["deny_publish"]:
1017                actions_from_item.append(common_pb2.UPDATE)
1018
1019            if not item.data["deny_delete"]:
1020                actions_from_item.append(common_pb2.DELETE)
1021
1022            rule_was_found = False
1023            for rule in role.rules:
1024                if rule.collection_id != item.data["collection"]:
1025                    continue
1026
1027                rule_was_found = True
1028
1029                if mode == "add":
1030                    modified_actions = list(set(list(rule.actions) + actions_from_item))
1031                else:
1032                    modified_actions = [
1033                        action
1034                        for action
1035                        in rule.actions
1036                        if action not in actions_from_item
1037                    ]
1038
1039                rule.actions[:] = modified_actions
1040
1041            # Если правила для коллекции нет и при этом доступы нужно __добавлять__
1042            if not rule_was_found and mode == "add":
1043                role.rules.append(
1044                    common_pb2.Rule(
1045                        collection_id=item.data["collection"],
1046                        actions=actions_from_item,
1047                    )
1048                )
1049
1050        try:
1051            await self.roles_service.Update(
1052                roles_pb2.UpdateRequest(
1053                    role=role
1054                )
1055            )
1056        except grpc.RpcError as e:
1057            errors.append(f"Не удалось обновить роль view, {e.details()}")
1058
1059        return errors
1060
1061
1062    async def install(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1063        """
1064            Метод установки расширения.
1065            TODO разобраться нужны ли отдельные методы `install` и `update`
1066            TODO разобраться с аргументом `use_force`
1067
1068            Arguments:
1069                  space_id (str): идентификатор пространства
1070                  env_id (str): идентификатор окружения
1071                  use_force (bool): флаг не используется
1072
1073            Returns:
1074                  list[str]
1075        """
1076
1077        errors = []
1078
1079        try:
1080            errors += await self.__update_collections(space_id, env_id)
1081            errors += await self.__update_roles(space_id, env_id)
1082            errors += await self.__update_clients(space_id)
1083            errors += await self.__update_actions(space_id, env_id)
1084            errors += await self.__update_items(space_id, env_id)
1085            errors += await self.__update_view_role(space_id, env_id)
1086        except Exception as e:
1087            logger.exception(e)
1088            errors.append(f"Во время установки было необработанное исключение - {e}")
1089
1090        return errors
1091
1092    async def update(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1093        """
1094            Метод установки расширения.
1095            TODO разобраться нужны ли отдельные методы `install` и `update`
1096            TODO разобраться с аргументом `use_force`
1097
1098            Arguments:
1099                  space_id (str): идентификатор пространства
1100                  env_id (str): идентификатор окружения
1101                  use_force (bool): флаг не используется
1102
1103            Returns:
1104                  list[str]
1105        """
1106
1107        errors = []
1108
1109        try:
1110            errors += await self.__update_collections(space_id, env_id)
1111            errors += await self.__update_roles(space_id, env_id)
1112            errors += await self.__update_clients(space_id)
1113            errors += await self.__update_actions(space_id, env_id)
1114            errors += await self.__update_items(space_id, env_id)
1115            errors += await self.__update_view_role(space_id, env_id)
1116        except Exception as e:
1117            logger.exception(e)
1118            errors.append(f"Во время обновления было необработанное исключение - {e}")
1119
1120        return errors
1121
1122    async def check(self, space_id: str, env_id: str) -> list[str]:
1123        """
1124            Метод проверки расширения.
1125
1126            Arguments:
1127                  space_id (str): идентификатор пространства
1128                  env_id (str): идентификатор окружения
1129
1130            Returns:
1131                  list[str]
1132        """
1133
1134        errors = []
1135
1136        try:
1137            errors += await self.__check_collections(space_id, env_id)
1138            errors += await self.__check_roles(space_id)
1139            errors += await self.__check_clients(space_id)
1140            errors += await self.__check_actions(space_id, env_id)
1141            errors += await self.__check_items(space_id, env_id)
1142        except Exception as e:
1143            logger.exception(e)
1144            errors.append(f"Во время проверки было необработанное исключение - {e}")
1145
1146        return errors
1147
1148    async def uninstall(self, space_id: str, env_id: str, use_remove: bool) -> list[str]:
1149        """
1150            Метод удаления расширения.
1151
1152            Arguments:
1153                  space_id (str): идентификатор пространства
1154                  env_id (str): идентификатор окружения
1155                  use_remove (str): удалять данные расширения
1156
1157            Returns:
1158                  list[str]
1159        """
1160
1161        errors = []
1162        if use_remove:
1163            try:
1164                errors += await self.__remove_collections(space_id, env_id)
1165                errors += await self.__remove_clients(space_id)
1166                errors += await self.__remove_roles(space_id)
1167                errors += await self.__remove_actions(space_id, env_id)
1168                errors += await self.__remove_items(space_id, env_id)
1169                errors += await self.__update_view_role(space_id, env_id, "remove")
1170            except Exception as e:
1171                logger.exception(e)
1172                errors.append(f"Во время удаления данных было необработанное исключение - {e}")
1173
1174        return errors
logger = <Logger perxis.extensions.extension_setup (WARNING)>
class ExtensionSetup:
  32class ExtensionSetup:
  33    """
  34        Attributes:
  35            collections_service (collections_pb2_grpc.CollectionsStub): ссылка на сервис коллекций
  36            environments_service (environments_pb2_grpc.EnvironmentsStub): ссылка на сервис окружений
  37            roles_service (roles_pb2_grpc.RolesStub): ссылка на сервис ролей
  38            clients_service (clients_pb2_grpc.ClientsStub): ссылка на сервис клиентов
  39            items_service (items_pb2_grpc.ItemsStub): ссылка на сервис item'ов
  40            collections (list[collections_pb2.Collection]): список коллекций расширения
  41            clients (list[clients_pb2.Client]): список клиентов расширения
  42            roles (list[roles_pb2.Role]): список ролей расширения
  43            items (list[AbstractItem]): список item'ов расширения
  44            __max_attempts_count (int): макс. кол-во попыток записи
  45            __sleep_time (int): время ожидания перед попыткой
  46
  47    """
  48
  49    def __init__(
  50            self,
  51            collections_service: collections_pb2_grpc.CollectionsStub,
  52            environments_service: environments_pb2_grpc.EnvironmentsStub,
  53            roles_service: roles_pb2_grpc.RolesStub,
  54            clients_service: clients_pb2_grpc.ClientsStub,
  55            items_service: items_pb2_grpc.ItemsStub,
  56    ):
  57        self.collections = []
  58        self.clients = []
  59        self.roles = []
  60        self.actions = []
  61        self.items = []
  62
  63        self.roles_service = roles_service
  64        self.clients_service = clients_service
  65        self.collections_service = collections_service
  66        self.environments_service = environments_service
  67        self.items_service = items_service
  68
  69        self.__max_attempts_count = 5
  70        self.__sleep_time = 1
  71
  72    def add_action(self, action: dict):
  73        self.actions.append(action)
  74
  75    def set_actions(self, actions: list[dict]):
  76        self.actions = actions
  77
  78    def add_collection(self, collection: collections_pb2.Collection):
  79        self.collections.append(collection)
  80
  81    def set_collections(self, collections: list[collections_pb2.Collection]):
  82        self.collections = collections
  83
  84    def add_role(self, role: roles_pb2.Role):
  85        self.roles.append(role)
  86
  87    def set_roles(self, roles: list[roles_pb2.Role]):
  88        self.roles = roles
  89
  90    def add_client(self, client: clients_pb2.Client):
  91        self.clients.append(client)
  92
  93    def set_clients(self, clients: list[clients_pb2.Client]):
  94        self.clients = clients
  95
  96    def set_items(self, items: list[AbstractItem]):
  97        self.items = items
  98
  99    # Работа с ролями
 100    async def __remove_roles(self, space_id: str) -> list[str]:
 101        """
 102            Метод для удаления ролей из пространства. Возвращает массив с ошибками
 103
 104            Arguments:
 105                  space_id (str): идентификатор пространства
 106
 107            Returns:
 108                  list[str]
 109        """
 110
 111        errors_list: list[str] = []
 112
 113        for role in self.roles:
 114            try:
 115                await self.roles_service.Delete(
 116                    roles_pb2.DeleteRequest(
 117                        space_id=space_id, role_id=role.id
 118                    )
 119                )
 120            except grpc.RpcError as e:
 121                # Если роли не существует считать это ошибкой не надо
 122                if "not found" not in e.details():
 123                    errors_list.append(f"Не удалось удалить роль {role.id}, {e.details()}")
 124
 125        return errors_list
 126
 127    async def __check_roles(self, space_id: str) -> list[str]:
 128        """
 129            Метод для проверки ролей из пространства. Возвращает массив с ошибками
 130
 131            Arguments:
 132                  space_id (str): идентификатор пространства
 133
 134            Returns:
 135                  list[str]
 136        """
 137
 138        errors_list = []
 139
 140        for role in self.roles:
 141            try:
 142                await self.roles_service.Get(
 143                    roles_pb2.GetRequest(space_id=space_id, role_id=role.id)
 144                )
 145            except grpc.RpcError as e:
 146                errors_list.append(f"Не удалось получить роль {role.id}, ошибка {e.details()}")
 147
 148        return errors_list
 149
 150    async def __update_roles(self, space_id: str, env_id: str) -> list[str]:
 151        """
 152            Метод для установки ролей в пространство. Возвращает массив с ошибками. Права доступа
 153            которые были установлены в perxis вручную или другим расширением не затираются.
 154
 155            Arguments:
 156                  space_id (str): идентификатор пространства
 157                  env_id (str): идентификатор окружения
 158
 159                Returns:
 160                      list[str]
 161            """
 162
 163        errors_list = []
 164
 165        for local_role in self.roles:
 166            try:
 167                get_response = await self.roles_service.Get(
 168                    roles_pb2.GetRequest(
 169                        space_id=space_id,
 170                        role_id=local_role.id
 171                    )
 172                )
 173
 174                role = get_response.role
 175            except grpc.RpcError as e:
 176                if "not found" not in e.details():
 177                    errors_list.append(f"Не удалось получить роль {local_role.id}, {e.details()}")
 178                    continue
 179
 180                role = None
 181
 182            cloned_role = copy.deepcopy(local_role)
 183            cloned_role.space_id = space_id
 184
 185            if role:
 186                if not role.environments:
 187                    role.environments[:] = [env_id]
 188                elif env_id not in role.environments:
 189                    role.environments.append(env_id)
 190
 191                cloned_role.environments[:] = role.environments
 192
 193                # Произвести мерж правил доступа
 194                for exist_rule in role.rules:
 195                    was_found = False
 196
 197                    for local_rule in cloned_role.rules:
 198                        # Если запись с правами доступа на коллекцию найдена - правила нужно совместить
 199                        if local_rule.collection_id == exist_rule.collection_id:
 200                            was_found = True
 201
 202                            local_rule.actions[:] = list(set(list(local_rule.actions) + list(exist_rule.actions)))
 203
 204                            break
 205
 206                    # Если правило для коллекций не было найдено - его нужно добавить
 207                    if not was_found:
 208                        cloned_role.rules.append(common_pb2.Rule(
 209                            collection_id=exist_rule.collection_id,
 210                            actions=list(exist_rule.actions)
 211                        ))
 212
 213                try:
 214                    await self.roles_service.Update(
 215                        roles_pb2.UpdateRequest(
 216                            role=cloned_role
 217                        )
 218                    )
 219                except grpc.RpcError as e:
 220                    errors_list.append(f"Не удалось обновить роль {local_role.id}, {e.details()}")
 221            else:
 222                if env_id not in cloned_role.environments:
 223                    cloned_role.environments.append(env_id)
 224
 225                try:
 226                    cloned_role = copy.deepcopy(local_role)
 227                    cloned_role.space_id = space_id
 228
 229                    response = await self.roles_service.Create(
 230                        roles_pb2.CreateRequest(
 231                            role=cloned_role
 232                        ),
 233                    )
 234                except grpc.RpcError as e:
 235                    # На этапе install считается что ролей __нет__. При install с указанием force роли предварительно
 236                    # удаляются
 237                    errors_list.append(f"Не удалось создать роль {local_role.id}, {e.details()}")
 238
 239        return errors_list
 240
 241    # Работа с клиентами
 242    async def __check_clients(self, space_id: str) -> list[str]:
 243        """
 244            Метод для проверки клиентов в пространстве. Возвращает массив с ошибками
 245
 246            Arguments:
 247                  space_id (str): идентификатор пространства
 248
 249            Returns:
 250                  list[str]
 251        """
 252
 253        errors_list = []
 254
 255        for client in self.clients:
 256            try:
 257                await self.clients_service.Get(
 258                    clients_pb2.GetRequest(space_id=space_id, id=client.id)
 259                )
 260            except grpc.RpcError as e:
 261                errors_list.append(f"Не удалось получить клиент {client.id}, ошибка {e.details()}")
 262
 263        return errors_list
 264
 265    async def __remove_clients(self, space_id: str) -> list[str]:
 266        """
 267            Метод для удаления клиентов из пространства. Возвращает массив с ошибками
 268
 269            Arguments:
 270                  space_id (str): идентификатор пространства
 271
 272            Returns:
 273                  list[str]
 274        """
 275
 276        errors_list: list[str] = []
 277
 278        for client in self.clients:
 279            try:
 280                await self.clients_service.Delete(
 281                    clients_pb2.DeleteRequest(
 282                        space_id=space_id, id=client.id
 283                    )
 284                )
 285            except grpc.RpcError as e:
 286                # Отсутствие клиента ошибкой не считается
 287                if "not found" not in e.details():
 288                    errors_list.append(f"Не удалось удалить клиент {client.id}, {e.details()}")
 289
 290        return errors_list
 291
 292    async def __update_clients(self, space_id: str) -> list[str]:
 293        """
 294            Метод для создания / обновления клиентов в пространстве. Возвращает массив с ошибками.
 295            Свойства oauth и api_key не затираются в случае если клиент уже был создан
 296
 297            Arguments:
 298                  space_id (str): идентификатор пространства
 299
 300            Returns:
 301                  list[str]
 302        """
 303
 304        errors_list = []
 305
 306        for local_client in self.clients:
 307            try:
 308                # Перед обновлением клиента предварительно нужно получить текущую запись чтобы скопировать оттуда
 309                # токены. Иначе после обновления расширения перестанут работать все приложения которые использовали
 310                # токен клиента
 311                get_response = await self.clients_service.Get(
 312                    clients_pb2.GetRequest(
 313                        space_id=space_id,
 314                        id=local_client.id
 315                    )
 316                )
 317
 318                client = get_response.client
 319            except grpc.RpcError as e:
 320                if "not found" not in e.details():
 321                    errors_list.append(f"Не удалось получить клиент {local_client.id}, {e.details()}")
 322                    continue
 323
 324                client = None
 325
 326            if client:
 327                try:
 328                    # Нужно чтобы у клиента каждый раз не слетали данные токенов при переустановке
 329                    # свойства oauth, api_key и tls должны браться из __созданного__ клиента
 330                    new_client = clients_pb2.Client(
 331                        id=local_client.id,
 332                        space_id=space_id,
 333                        name=local_client.name,
 334                        description=local_client.description,
 335                        disabled=client.disabled,
 336                        role_id=local_client.role_id,
 337                        oauth=client.oauth or local_client.oauth,
 338                        api_key=client.api_key or local_client.api_key,
 339                        tls=client.tls
 340                    )
 341
 342                    await self.clients_service.Update(
 343                        clients_pb2.UpdateRequest(
 344                            client=new_client
 345                        )
 346                    )
 347                except grpc.RpcError as e:
 348                    errors_list.append(f"Не удалось обновить клиент {local_client.id}, {e.details()}")
 349            else:
 350                try:
 351                    cloned_client = copy.deepcopy(local_client)
 352                    cloned_client.space_id = space_id
 353
 354                    await self.clients_service.Create(
 355                        clients_pb2.CreateRequest(
 356                            client=cloned_client
 357                        ),
 358                    )
 359                except grpc.RpcError as e:
 360                    errors_list.append(f"Не удалось создать клиент {local_client.id}, {e.details()}")
 361
 362        return errors_list
 363
 364    # Работа с коллекциями
 365    async def __remove_collections(self, space_id: str, env_id: str) -> list[str]:
 366        """
 367            Метод для удаления коллекций из определённого окружения пространства. Возвращает массив с ошибками
 368
 369            Arguments:
 370                  space_id (str): идентификатор пространства
 371                  env_id (str): идентификатор окружения
 372
 373            Returns:
 374                  list[str]
 375        """
 376
 377        errors_list: list[str] = []
 378
 379        for collection in self.collections:
 380            try:
 381                await self.collections_service.Delete(
 382                    collections_pb2.DeleteRequest(
 383                        space_id=space_id, env_id=env_id, collection_id=collection.id
 384                    )
 385                )
 386            except grpc.RpcError as e:
 387                # Отсутствие коллекции это не ошибка
 388                if "not found" not in e.details():
 389                    errors_list.append(f"Не удалось удалить коллекцию {collection.id}, {e.details()}")
 390
 391        return errors_list
 392
 393    async def __check_collections(self, space_id: str, env_id: str) -> list[str]:
 394        """
 395            Метод для проверки коллекций из определённого окружения пространства. Возвращает массив с ошибками
 396
 397            Arguments:
 398                  space_id (str): идентификатор пространства
 399                  env_id (str): идентификатор окружения
 400
 401            Returns:
 402                  list[str]
 403        """
 404
 405        errors_list = []
 406
 407        for collection in self.collections:
 408            try:
 409                await self.collections_service.Get(
 410                    collections_pb2.GetRequest(space_id=space_id, env_id=env_id, collection_id=collection.id)
 411                )
 412            except grpc.RpcError as e:
 413                errors_list.append(f"Не удалось получить коллекцию {collection.id}, ошибка {e.details()}")
 414
 415        return errors_list
 416
 417    async def __update_collections(self, space_id: str, env_id: str) -> list[str]:
 418        """
 419            Метод __обновления__ коллекций. Миграция окружения требуется
 420            только в случае если одна или несколько схем коллекций изменялись. Алгоритм работы:
 421            1. Получить фактически существующую коллекцию из БД
 422            2. Обновить её в perxis
 423            3. Сравнить схему коллекции в расширении и в perxis
 424            4. Если схема изменена - обновить схему в perxis
 425            5. Если обновлялась хотя бы одна схема в perxis - запустить миграцию окружения
 426
 427            Arguments:
 428                  space_id (str): идентификатор пространства
 429                  env_id (str): идентификатор окружения
 430
 431            Returns:
 432                  list[str]
 433        """
 434
 435        need_to_migrate_environment = False
 436        errors_list = []
 437
 438        for local_collection in self.collections:
 439            try:
 440                # Необходимо получить текущую версию коллекции для того чтобы сравнить схемы
 441                get_response = await self.collections_service.Get(
 442                    collections_pb2.GetRequest(space_id=space_id, env_id=env_id, collection_id=local_collection.id)
 443                )
 444
 445                collection = get_response.collection
 446            except grpc.RpcError as e:
 447                collection = None
 448
 449            cloned_collection = copy.deepcopy(local_collection)
 450            cloned_collection.space_id = space_id
 451            cloned_collection.env_id = env_id
 452
 453            # Коллекция может быть не найдена в случае если она добавлена в новой версии
 454            if not collection:
 455                try:
 456                    create_response = await self.collections_service.Create(
 457                        collections_pb2.CreateRequest(collection=cloned_collection)
 458                    )
 459
 460                    collection = create_response.created
 461                except grpc.RpcError as e:
 462                    errors_list.append(f"Не удалось создать коллекцию {local_collection.id}, {e.details()}")
 463
 464                    continue
 465            else:
 466                # если view-коллекция то не устанавливаем схему
 467                if MessageToDict(collection).get("view"):
 468                    continue
 469                try:
 470                    cloned_collection = copy.deepcopy(local_collection)
 471                    cloned_collection.space_id = space_id
 472                    cloned_collection.env_id = env_id
 473
 474                    await self.collections_service.Update(
 475                        collections_pb2.UpdateRequest(collection=cloned_collection)
 476                    )
 477                except grpc.RpcError as e:
 478                    errors_list.append(f"Не удалось обновить коллекцию {local_collection.id}, {e.details()}")
 479
 480                    continue
 481
 482            diff = DeepDiff(
 483                json.loads(collection.schema or "{}"),
 484                json.loads(local_collection.schema or "{}"),
 485                ignore_numeric_type_changes=True,
 486                exclude_paths=["root['loaded']"]
 487            )
 488            if diff:
 489                need_to_migrate_environment = True
 490
 491                set_schema_error_message = await self.__set_collection_schema(
 492                    space_id, env_id, local_collection.id, local_collection.schema
 493                )
 494                if set_schema_error_message:
 495                    errors_list.append(set_schema_error_message)
 496
 497        if need_to_migrate_environment:
 498            migrate_environment_error_message = await self.__migrate_environment(space_id, env_id)
 499            if migrate_environment_error_message:
 500                errors_list.append(migrate_environment_error_message)
 501
 502        return errors_list
 503
 504    async def __migrate_environment(self, space_id: str, env_id: str) -> typing.Optional[str]:
 505        """
 506            Метод для миграции окружения.
 507            Так как perxis может не сразу выставить коллекции / окружению статус ready операцию необходимо выполнять
 508            с попытками
 509            Arguments:
 510                  space_id (str): идентификатор пространства
 511                  env_id (str): идентификатор окружения
 512
 513            Returns:
 514                  str | None
 515        """
 516
 517        attempt = 0
 518        is_ok = False
 519        error_message = None
 520
 521        while attempt <= self.__max_attempts_count and not is_ok:
 522            time.sleep(self.__sleep_time)
 523
 524            try:
 525                await self.environments_service.Migrate(environments_pb2.MigrateRequest(
 526                    space_id=space_id,
 527                    env_id=env_id,
 528                    options=environments_pb2.MigrateOptions(
 529                        wait=True
 530                    )
 531                ))
 532
 533                is_ok = True
 534            except grpc.RpcError as e:
 535                # Если не удалось мигрировать окружение по любой причине кроме подготовки - это ошибка
 536                if "is preparing" not in e.details():
 537                    error_message = e.details()
 538
 539                    # Для принудительного выхода из цикла
 540                    attempt = self.__max_attempts_count
 541
 542            attempt += 1
 543
 544        return error_message
 545
 546    async def __set_collection_schema(self, space_id: str, env_id: str, collection_id: str, schema: str) -> typing.Optional[str]:
 547        """
 548            Метод для установки схемы коллекции в определённом окружении пространства
 549            Так как perxis может не сразу выставить коллекции / окружению статус ready операцию необходимо выполнять
 550            с попытками
 551            Arguments:
 552                  space_id (str): идентификатор пространства
 553                  env_id (str): идентификатор окружения
 554                  collection_id (str): идентификатор коллекции
 555                  schema (str): схема коллекции в формате json
 556
 557            Returns:
 558                  str | None
 559        """
 560
 561        attempt = 0
 562        is_ok = False
 563        error_message = None
 564
 565        while attempt <= self.__max_attempts_count and not is_ok:
 566            time.sleep(self.__sleep_time)
 567
 568            try:
 569                await self.collections_service.SetSchema(
 570                    collections_pb2.SetSchemaRequest(
 571                        space_id=space_id,
 572                        env_id=env_id,
 573                        collection_id=collection_id,
 574                        schema=schema
 575                    )
 576                )
 577
 578                is_ok = True
 579            except grpc.RpcError as e:
 580                # Если не удалось установить схему по любой причине кроме подготовки - это ошибка
 581                if "is preparing" not in e.details():
 582                    error_message = e.details()
 583
 584                    # Для принудительного выхода из цикла
 585                    attempt = self.__max_attempts_count
 586
 587            attempt += 1
 588
 589        return error_message
 590
 591    # Работа с действиями
 592    async def __check_actions(self, space_id: str, env_id: str) -> list[str]:
 593        """
 594            Метод для проверки действий из определённого окружения пространства. Возвращает массив с ошибками
 595
 596            Arguments:
 597                  space_id (str): идентификатор пространства
 598                  env_id (str): идентификатор окружения
 599
 600            Returns:
 601                  list[str]
 602        """
 603
 604        errors_list = []
 605
 606        ids = [data["id"] for data in self.actions if data.get("id")]
 607
 608        items = []
 609
 610        try:
 611            result = await self.items_service.Find(
 612                items_pb2.FindRequest(
 613                    space_id=space_id,
 614                    env_id=env_id,
 615                    collection_id=ACTIONS_COLLECTION_ID,
 616                    filter=items_pb2.Filter(q=[f"id in {ids}"]),
 617                    options=items_pb2.FindOptions(
 618                        options=common_pb2.FindOptions(
 619                            page_num=0, page_size=len(ids)
 620                        )
 621                    ),
 622                )
 623            )
 624
 625            items = result.items
 626        except grpc.RpcError as e:
 627            errors_list.append(f"Не удалось получить данные о действиях, ошибка {e.details()}")
 628        finally:
 629            found_ids = [item.id for item in items]
 630
 631            for action_id in found_ids:
 632                if action_id not in ids:
 633                    errors_list.append(f"Действие {action_id} не найдено")
 634
 635        return errors_list
 636
 637    async def __update_actions(self, space_id: str, env_id: str) -> list[str]:
 638        """
 639            Метод для создания / обновления действий из определённого окружения пространства. Возвращает массив с ошибками
 640
 641            Arguments:
 642                  space_id (str): идентификатор пространства
 643                  env_id (str): идентификатор окружения
 644
 645            Returns:
 646                  list[str]
 647        """
 648
 649        errors_list = []
 650        not_found = False
 651
 652        for action in self.actions:
 653            action_item = make_action_item(space_id, env_id, action)
 654
 655            try:
 656                await self.items_service.Update(
 657                    items_pb2.UpdateRequest(
 658                        item=items_pb2.Item(
 659                            id=action_item.id,
 660                            space_id=space_id,
 661                            env_id=env_id,
 662                            collection_id=ACTIONS_COLLECTION_ID,
 663                            data=action_item.data,
 664                        )
 665                    )
 666                )
 667            except grpc.RpcError as e:
 668                if "not found" not in e.details():
 669                    errors_list.append(f"Не удалось обновить действие {action_item.id}, {e.details()}")
 670                    continue
 671
 672                not_found = True
 673            except Exception as e:
 674                errors_list.append(f"Не удалось обновить действие {action_item.id}, {e}")
 675
 676                continue
 677
 678            if not_found:
 679                try:
 680                    await self.items_service.Create(
 681                        items_pb2.CreateRequest(
 682                            item=action_item
 683                        )
 684                    )
 685                except grpc.RpcError as e:
 686                    errors_list.append(f"Не удалось создать действие {action_item.id}, {e.details()}")
 687                except Exception as e:
 688                    errors_list.append(f"Не удалось создать действие {action_item.id}, {e}")
 689
 690                    continue
 691
 692            try:
 693                await self.items_service.Publish(
 694                    items_pb2.PublishRequest(
 695                        item=action_item
 696                    )
 697                )
 698            except grpc.RpcError as e:
 699                # Если action уже опубликован
 700                if "item cannot be published in this state" not in e.details():
 701                    errors_list.append(f"Не удалось опубликовать действие {action_item.id}, {e.details()}")
 702            except Exception as e:
 703                errors_list.append(f"Не удалось опубликовать действие {action_item.id}, {e}")
 704
 705        return errors_list
 706
 707    async def __remove_actions(self, space_id: str, env_id: str) -> list[str]:
 708        """
 709            Метод для удаления действий из определённого окружения пространства. Возвращает массив с ошибками
 710
 711            Arguments:
 712                  space_id (str): идентификатор пространства
 713                  env_id (str): идентификатор окружения
 714
 715            Returns:
 716                  list[str]
 717        """
 718
 719        errors_list = []
 720        for action in self.actions:
 721            action_item = make_action_item(space_id, env_id, action)
 722
 723            try:
 724                await self.items_service.Delete(
 725                    items_pb2.DeleteRequest(
 726                        item=items_pb2.Item(
 727                            id=action_item.id,
 728                            space_id=space_id,
 729                            env_id=env_id,
 730                            collection_id=action_item.collection_id,
 731                        ),
 732                        options=items_pb2.DeleteOptions(
 733                            update_attrs=True,
 734                            erase=True
 735                        )
 736                    )
 737                )
 738            except grpc.RpcError as e:
 739                # Отсутствие действия это не ошибка
 740                if "not found" not in e.details():
 741                    errors_list.append(f"Не удалось удалить действие {action.get('id', 'n/a')}, {e.details()}")
 742            except Exception as e:
 743                errors_list.append(f"Не удалось удалить действие - {e}")
 744
 745        return errors_list
 746
 747    async def __check_items(self, space_id: str, env_id: str) -> list[str]:
 748        """
 749            Метод для item'ов действий из определённого окружения пространства. Возвращает массив с ошибками
 750
 751            Arguments:
 752                  space_id (str): идентификатор пространства
 753                  env_id (str): идентификатор окружения
 754
 755            Returns:
 756                  list[str]
 757        """
 758
 759        errors_list: list[str] = []
 760        wrapper = PerxisItemsWrapper(
 761            self.items_service,
 762        )
 763
 764        for item in self.items:
 765            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 766
 767            if not all_rules_satisfied:
 768                continue
 769
 770            try:
 771                message = await wrapper.find(
 772                    collection_id=item.collection_id,
 773                    env_id=env_id,
 774                    space_id=space_id,
 775                    limit=1,
 776                    offset=0,
 777                    fields=["id"],
 778                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 779                )
 780
 781                if not message.total:
 782                    errors_list.append(f"Item {item.identifier} не найден")
 783            except Exception as e:
 784                errors_list.append(
 785                    f"Не удалось проверить item {item.identifier} "
 786                    f"коллекции {item.collection_id} - {e}"
 787                )
 788
 789        return errors_list
 790
 791    async def __update_items(self, space_id: str, env_id: str) -> list[str]:
 792        """
 793            Метод для создания / обновления item'ов из определённого окружения пространства. Возвращает массив с ошибками
 794
 795            Arguments:
 796                  space_id (str): идентификатор пространства
 797                  env_id (str): идентификатор окружения
 798
 799            Returns:
 800                  list[str]
 801        """
 802
 803        errors_list: list[str] = []
 804        wrapper = PerxisItemsWrapper(
 805            self.items_service,
 806        )
 807
 808        for item in self.items:
 809            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 810
 811            if not all_rules_satisfied:
 812                continue
 813
 814            try:
 815                message = await wrapper.find(
 816                    collection_id=item.collection_id,
 817                    env_id=env_id,
 818                    space_id=space_id,
 819                    limit=1,
 820                    offset=0,
 821                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 822                )
 823
 824                if message.items:
 825                    item_in_perxis = message.items[0]
 826                else:
 827                    item_in_perxis = None
 828            except Exception as e:
 829                if hasattr(e, "details") and "not found" in e.details():
 830                    is_error = False
 831                else:
 832                    is_error = True
 833
 834                if is_error:
 835                    errors_list.append(
 836                        f"Не удалось получить item {item.identifier} "
 837                        f"коллекции {item.collection_id} - {e}"
 838                    )
 839
 840                continue
 841
 842            try:
 843                if item_in_perxis:
 844                    # Если установлен запрет на изменение item'ов
 845                    if not item.with_update:
 846                        continue
 847
 848                    # Для того чтобы не затереть изменения в perxis
 849                    # Нужно смержить данные. Логика работы:
 850                    # 1. Данные которые указаны в `data` в расширении - в приоритете, они замещают то что в perxis
 851                    # 2. Данные которые есть в perxis но нет в расширении - дополняются
 852                    await wrapper.update(
 853                        collection_id=item.collection_id,
 854                        item_id=item_in_perxis.id,
 855                        space_id=space_id,
 856                        env_id=env_id,
 857                        data=item.merge_data(MessageToDict(item_in_perxis)["data"]),
 858                    )
 859                else:
 860                    message = await wrapper.create(
 861                        collection_id=item.collection_id,
 862                        space_id=space_id,
 863                        env_id=env_id,
 864                        data=item.struct,
 865                    )
 866
 867                    item_in_perxis = message.created
 868
 869                await wrapper.publish(
 870                    item_id=item_in_perxis.id,
 871                    collection_id=item.collection_id,
 872                    space_id=space_id,
 873                    env_id=env_id
 874                )
 875            except Exception as e:
 876                errors_list.append(
 877                    f"Не удалось записать item {item.identifier} "
 878                    f"коллекции {item.collection_id} - {e}"
 879                )
 880
 881                continue
 882
 883        return errors_list
 884
 885    async def __remove_items(self, space_id: str, env_id: str) -> list[str]:
 886        """
 887            Метод для удаления item'ов из определённого окружения пространства. Возвращает массив с ошибками
 888
 889            Arguments:
 890                  space_id (str): идентификатор пространства
 891                  env_id (str): идентификатор окружения
 892
 893            Returns:
 894                  list[str]
 895        """
 896
 897        errors_list: list[str] = []
 898        wrapper = PerxisItemsWrapper(
 899            self.items_service,
 900        )
 901
 902        for item in self.items:
 903            all_rules_satisfied = await item.all_rules_is_satisfied(space_id, env_id)
 904
 905            if not all_rules_satisfied:
 906                continue
 907
 908            # Если установлен запрет на удаление item'ов
 909            if not item.with_delete:
 910                continue
 911
 912            try:
 913                message = await wrapper.find(
 914                    collection_id=item.collection_id,
 915                    env_id=env_id,
 916                    space_id=space_id,
 917                    limit=1,
 918                    offset=0,
 919                    fields=["id"],
 920                    filters=[f"{item.identifier_field} == '{item.identifier}'"]
 921                )
 922
 923                if message.items:
 924                    await wrapper.delete(
 925                        item_id=message.items[0].id,
 926                        collection_id=message.items[0].collection_id,
 927                        space_id=space_id,
 928                        env_id=env_id,
 929                    )
 930            except Exception as e:
 931                if hasattr(e, "details") and "not found" in e.details():
 932                    is_error = False
 933                else:
 934                    is_error = True
 935
 936                if is_error:
 937                    errors_list.append(
 938                        f"Не удалось удалить item {item.identifier} "
 939                        f"коллекции {item.collection_id} - {e}"
 940                    )
 941
 942        return errors_list
 943
 944    async def __update_view_role(self, space_id: str, env_id: str, mode: str = "add") -> list[str]:
 945        """
 946            Метод для создания / обновления роли view из определённого окружения пространства. Возвращает массив с ошибками
 947
 948            Arguments:
 949                  space_id (str): идентификатор пространства
 950                  env_id (str): идентификатор окружения
 951
 952            Returns:
 953                  list[str]
 954        """
 955
 956        errors = []
 957
 958        # Нужны только относящиеся к синхронизации элементы
 959        items_for_view_role = [
 960            item
 961            for item
 962            in self.items
 963            if (isinstance(item, SyncPolicyItem) or item.collection_id == "hoop_item_sync_policies")
 964            and item.data["export_view"]
 965            and await item.all_rules_is_satisfied(space_id=space_id, env_id=env_id)
 966        ]
 967
 968        if not items_for_view_role:
 969            return errors
 970
 971        try:
 972            message = await self.roles_service.Get(
 973                roles_pb2.GetRequest(
 974                    space_id=space_id,
 975                    role_id="view"
 976                )
 977            )
 978
 979            role = message.role
 980        except grpc.RpcError as e:
 981            if "not found" not in e.details():
 982                errors.append(f"Не удалось получить роль view, {e.details()}")
 983
 984            role = None
 985
 986        if not role:
 987            try:
 988                message = await self.roles_service.Create(
 989                    roles_pb2.CreateRequest(
 990                        role=roles_pb2.Role(
 991                            id="view",
 992                            space_id=space_id,
 993                            description="Роль для view коллекций",
 994                            rules=[],
 995                            environments=["*"],
 996                            allow_management=False,
 997                        )
 998                    ),
 999                )
1000
1001                role = message.created
1002            except grpc.RpcError as e:
1003                errors.append(f"Не удалось создать роль view, {e.details()}")
1004
1005                return errors
1006
1007        # Произвести мерж правил доступа
1008        for item in items_for_view_role:
1009            actions_from_item = []
1010
1011            if not item.data["deny_read"]:
1012                actions_from_item.append(common_pb2.READ)
1013
1014            if not item.data["deny_create"]:
1015                actions_from_item.append(common_pb2.CREATE)
1016
1017            if not item.data["deny_publish"]:
1018                actions_from_item.append(common_pb2.UPDATE)
1019
1020            if not item.data["deny_delete"]:
1021                actions_from_item.append(common_pb2.DELETE)
1022
1023            rule_was_found = False
1024            for rule in role.rules:
1025                if rule.collection_id != item.data["collection"]:
1026                    continue
1027
1028                rule_was_found = True
1029
1030                if mode == "add":
1031                    modified_actions = list(set(list(rule.actions) + actions_from_item))
1032                else:
1033                    modified_actions = [
1034                        action
1035                        for action
1036                        in rule.actions
1037                        if action not in actions_from_item
1038                    ]
1039
1040                rule.actions[:] = modified_actions
1041
1042            # Если правила для коллекции нет и при этом доступы нужно __добавлять__
1043            if not rule_was_found and mode == "add":
1044                role.rules.append(
1045                    common_pb2.Rule(
1046                        collection_id=item.data["collection"],
1047                        actions=actions_from_item,
1048                    )
1049                )
1050
1051        try:
1052            await self.roles_service.Update(
1053                roles_pb2.UpdateRequest(
1054                    role=role
1055                )
1056            )
1057        except grpc.RpcError as e:
1058            errors.append(f"Не удалось обновить роль view, {e.details()}")
1059
1060        return errors
1061
1062
1063    async def install(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1064        """
1065            Метод установки расширения.
1066            TODO разобраться нужны ли отдельные методы `install` и `update`
1067            TODO разобраться с аргументом `use_force`
1068
1069            Arguments:
1070                  space_id (str): идентификатор пространства
1071                  env_id (str): идентификатор окружения
1072                  use_force (bool): флаг не используется
1073
1074            Returns:
1075                  list[str]
1076        """
1077
1078        errors = []
1079
1080        try:
1081            errors += await self.__update_collections(space_id, env_id)
1082            errors += await self.__update_roles(space_id, env_id)
1083            errors += await self.__update_clients(space_id)
1084            errors += await self.__update_actions(space_id, env_id)
1085            errors += await self.__update_items(space_id, env_id)
1086            errors += await self.__update_view_role(space_id, env_id)
1087        except Exception as e:
1088            logger.exception(e)
1089            errors.append(f"Во время установки было необработанное исключение - {e}")
1090
1091        return errors
1092
1093    async def update(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1094        """
1095            Метод установки расширения.
1096            TODO разобраться нужны ли отдельные методы `install` и `update`
1097            TODO разобраться с аргументом `use_force`
1098
1099            Arguments:
1100                  space_id (str): идентификатор пространства
1101                  env_id (str): идентификатор окружения
1102                  use_force (bool): флаг не используется
1103
1104            Returns:
1105                  list[str]
1106        """
1107
1108        errors = []
1109
1110        try:
1111            errors += await self.__update_collections(space_id, env_id)
1112            errors += await self.__update_roles(space_id, env_id)
1113            errors += await self.__update_clients(space_id)
1114            errors += await self.__update_actions(space_id, env_id)
1115            errors += await self.__update_items(space_id, env_id)
1116            errors += await self.__update_view_role(space_id, env_id)
1117        except Exception as e:
1118            logger.exception(e)
1119            errors.append(f"Во время обновления было необработанное исключение - {e}")
1120
1121        return errors
1122
1123    async def check(self, space_id: str, env_id: str) -> list[str]:
1124        """
1125            Метод проверки расширения.
1126
1127            Arguments:
1128                  space_id (str): идентификатор пространства
1129                  env_id (str): идентификатор окружения
1130
1131            Returns:
1132                  list[str]
1133        """
1134
1135        errors = []
1136
1137        try:
1138            errors += await self.__check_collections(space_id, env_id)
1139            errors += await self.__check_roles(space_id)
1140            errors += await self.__check_clients(space_id)
1141            errors += await self.__check_actions(space_id, env_id)
1142            errors += await self.__check_items(space_id, env_id)
1143        except Exception as e:
1144            logger.exception(e)
1145            errors.append(f"Во время проверки было необработанное исключение - {e}")
1146
1147        return errors
1148
1149    async def uninstall(self, space_id: str, env_id: str, use_remove: bool) -> list[str]:
1150        """
1151            Метод удаления расширения.
1152
1153            Arguments:
1154                  space_id (str): идентификатор пространства
1155                  env_id (str): идентификатор окружения
1156                  use_remove (str): удалять данные расширения
1157
1158            Returns:
1159                  list[str]
1160        """
1161
1162        errors = []
1163        if use_remove:
1164            try:
1165                errors += await self.__remove_collections(space_id, env_id)
1166                errors += await self.__remove_clients(space_id)
1167                errors += await self.__remove_roles(space_id)
1168                errors += await self.__remove_actions(space_id, env_id)
1169                errors += await self.__remove_items(space_id, env_id)
1170                errors += await self.__update_view_role(space_id, env_id, "remove")
1171            except Exception as e:
1172                logger.exception(e)
1173                errors.append(f"Во время удаления данных было необработанное исключение - {e}")
1174
1175        return errors
Attributes:
  • collections_service (collections_pb2_grpc.CollectionsStub): ссылка на сервис коллекций
  • environments_service (environments_pb2_grpc.EnvironmentsStub): ссылка на сервис окружений
  • roles_service (roles_pb2_grpc.RolesStub): ссылка на сервис ролей
  • clients_service (clients_pb2_grpc.ClientsStub): ссылка на сервис клиентов
  • items_service (items_pb2_grpc.ItemsStub): ссылка на сервис item'ов
  • collections (list[collections_pb2.Collection]): список коллекций расширения
  • clients (list[clients_pb2.Client]): список клиентов расширения
  • roles (list[roles_pb2.Role]): список ролей расширения
  • items (list[AbstractItem]): список item'ов расширения
  • __max_attempts_count (int): макс. кол-во попыток записи
  • __sleep_time (int): время ожидания перед попыткой
49    def __init__(
50            self,
51            collections_service: collections_pb2_grpc.CollectionsStub,
52            environments_service: environments_pb2_grpc.EnvironmentsStub,
53            roles_service: roles_pb2_grpc.RolesStub,
54            clients_service: clients_pb2_grpc.ClientsStub,
55            items_service: items_pb2_grpc.ItemsStub,
56    ):
57        self.collections = []
58        self.clients = []
59        self.roles = []
60        self.actions = []
61        self.items = []
62
63        self.roles_service = roles_service
64        self.clients_service = clients_service
65        self.collections_service = collections_service
66        self.environments_service = environments_service
67        self.items_service = items_service
68
69        self.__max_attempts_count = 5
70        self.__sleep_time = 1
collections
clients
roles
actions
items
roles_service
clients_service
collections_service
environments_service
items_service
def add_action(self, action: dict):
72    def add_action(self, action: dict):
73        self.actions.append(action)
def set_actions(self, actions: list[dict]):
75    def set_actions(self, actions: list[dict]):
76        self.actions = actions
def add_collection(self, collection: collections.collections_pb2.Collection):
78    def add_collection(self, collection: collections_pb2.Collection):
79        self.collections.append(collection)
def set_collections(self, collections: list[collections.collections_pb2.Collection]):
81    def set_collections(self, collections: list[collections_pb2.Collection]):
82        self.collections = collections
def add_role(self, role: roles.roles_pb2.Role):
84    def add_role(self, role: roles_pb2.Role):
85        self.roles.append(role)
def set_roles(self, roles: list[roles.roles_pb2.Role]):
87    def set_roles(self, roles: list[roles_pb2.Role]):
88        self.roles = roles
def add_client(self, client: clients.clients_pb2.Client):
90    def add_client(self, client: clients_pb2.Client):
91        self.clients.append(client)
def set_clients(self, clients: list[clients.clients_pb2.Client]):
93    def set_clients(self, clients: list[clients_pb2.Client]):
94        self.clients = clients
def set_items(self, items: list[perxis.extensions.item_models.AbstractItem]):
96    def set_items(self, items: list[AbstractItem]):
97        self.items = items
async def install(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1063    async def install(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1064        """
1065            Метод установки расширения.
1066            TODO разобраться нужны ли отдельные методы `install` и `update`
1067            TODO разобраться с аргументом `use_force`
1068
1069            Arguments:
1070                  space_id (str): идентификатор пространства
1071                  env_id (str): идентификатор окружения
1072                  use_force (bool): флаг не используется
1073
1074            Returns:
1075                  list[str]
1076        """
1077
1078        errors = []
1079
1080        try:
1081            errors += await self.__update_collections(space_id, env_id)
1082            errors += await self.__update_roles(space_id, env_id)
1083            errors += await self.__update_clients(space_id)
1084            errors += await self.__update_actions(space_id, env_id)
1085            errors += await self.__update_items(space_id, env_id)
1086            errors += await self.__update_view_role(space_id, env_id)
1087        except Exception as e:
1088            logger.exception(e)
1089            errors.append(f"Во время установки было необработанное исключение - {e}")
1090
1091        return errors

Метод установки расширения. TODO разобраться нужны ли отдельные методы install и update TODO разобраться с аргументом use_force

Arguments:
  • space_id (str): идентификатор пространства
  • env_id (str): идентификатор окружения
  • use_force (bool): флаг не используется
Returns:

list[str]

async def update(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1093    async def update(self, space_id: str, env_id: str, use_force: bool) -> list[str]:
1094        """
1095            Метод установки расширения.
1096            TODO разобраться нужны ли отдельные методы `install` и `update`
1097            TODO разобраться с аргументом `use_force`
1098
1099            Arguments:
1100                  space_id (str): идентификатор пространства
1101                  env_id (str): идентификатор окружения
1102                  use_force (bool): флаг не используется
1103
1104            Returns:
1105                  list[str]
1106        """
1107
1108        errors = []
1109
1110        try:
1111            errors += await self.__update_collections(space_id, env_id)
1112            errors += await self.__update_roles(space_id, env_id)
1113            errors += await self.__update_clients(space_id)
1114            errors += await self.__update_actions(space_id, env_id)
1115            errors += await self.__update_items(space_id, env_id)
1116            errors += await self.__update_view_role(space_id, env_id)
1117        except Exception as e:
1118            logger.exception(e)
1119            errors.append(f"Во время обновления было необработанное исключение - {e}")
1120
1121        return errors

Метод установки расширения. TODO разобраться нужны ли отдельные методы install и update TODO разобраться с аргументом use_force

Arguments:
  • space_id (str): идентификатор пространства
  • env_id (str): идентификатор окружения
  • use_force (bool): флаг не используется
Returns:

list[str]

async def check(self, space_id: str, env_id: str) -> list[str]:
1123    async def check(self, space_id: str, env_id: str) -> list[str]:
1124        """
1125            Метод проверки расширения.
1126
1127            Arguments:
1128                  space_id (str): идентификатор пространства
1129                  env_id (str): идентификатор окружения
1130
1131            Returns:
1132                  list[str]
1133        """
1134
1135        errors = []
1136
1137        try:
1138            errors += await self.__check_collections(space_id, env_id)
1139            errors += await self.__check_roles(space_id)
1140            errors += await self.__check_clients(space_id)
1141            errors += await self.__check_actions(space_id, env_id)
1142            errors += await self.__check_items(space_id, env_id)
1143        except Exception as e:
1144            logger.exception(e)
1145            errors.append(f"Во время проверки было необработанное исключение - {e}")
1146
1147        return errors

Метод проверки расширения.

Arguments:
  • space_id (str): идентификатор пространства
  • env_id (str): идентификатор окружения
Returns:

list[str]

async def uninstall(self, space_id: str, env_id: str, use_remove: bool) -> list[str]:
1149    async def uninstall(self, space_id: str, env_id: str, use_remove: bool) -> list[str]:
1150        """
1151            Метод удаления расширения.
1152
1153            Arguments:
1154                  space_id (str): идентификатор пространства
1155                  env_id (str): идентификатор окружения
1156                  use_remove (str): удалять данные расширения
1157
1158            Returns:
1159                  list[str]
1160        """
1161
1162        errors = []
1163        if use_remove:
1164            try:
1165                errors += await self.__remove_collections(space_id, env_id)
1166                errors += await self.__remove_clients(space_id)
1167                errors += await self.__remove_roles(space_id)
1168                errors += await self.__remove_actions(space_id, env_id)
1169                errors += await self.__remove_items(space_id, env_id)
1170                errors += await self.__update_view_role(space_id, env_id, "remove")
1171            except Exception as e:
1172                logger.exception(e)
1173                errors.append(f"Во время удаления данных было необработанное исключение - {e}")
1174
1175        return errors

Метод удаления расширения.

Arguments:
  • space_id (str): идентификатор пространства
  • env_id (str): идентификатор окружения
  • use_remove (str): удалять данные расширения
Returns:

list[str]