termius/cloud/client/transformers/many.py
# -*- coding: utf-8 -*-
"""Module with many then 1 entry transformers."""
from collections import OrderedDict
from ....core.exceptions import DoesNotExistException, SkipField
from ....core.storage.strategies import SoftDeleteStrategy
from ....core.models.terminal import (
Host, Group,
Tag, SshKey,
Identity, SshConfig,
PFRule, TagHost,
Snippet
)
from .base import Transformer, DeletBadEncrypted
from .single import GetPrimaryKeyTransformerMixin, CryptoBulkEntryTransformer
from .mixins import CryptoChildTransformerCreatorMixin
# pylint: disable=abstract-method
class ManyTransformer(Transformer):
"""Base class for all many transformers."""
def __init__(self, **kwargs):
"""Construct new transformer for entry list."""
super(ManyTransformer, self).__init__(**kwargs)
self.mapping = OrderedDict((
(i.set_name, self.create_child_transformer(i))
for i in self.supported_models
))
# pylint: disable=too-few-public-methods
class SupportedModelsMixin(object):
"""Mixin to keep sync models."""
@property
def supported_models(self):
"""Return model tuple to sync."""
sync_keys = self.account_manager.get_settings()['synchronize_key']
if sync_keys:
return (
SshKey, Snippet,
Identity, SshConfig,
Tag, Group,
Host, PFRule,
TagHost
)
return (
Snippet, SshConfig,
Tag, Group,
Host, PFRule,
TagHost
)
class SoftDeleteMixin(object):
"""Class is a mixin that implements soft_delete."""
def soft_delete(self, model):
"""Soft delete a model instance from the storage.
The main difference between this and soft delete in storage is that
this method adds a remote_id to delete_set in case when the model is
missed in the storage.
"""
if model.id:
return self.storage.delete(model)
return self.get_delete_strategy().delete(model)
def delete_list(self, models):
"""Delete models from the storage and put it to deleted_set."""
for i in models:
self.soft_delete(i)
def get_delete_strategy(self):
"""Create delete strategy."""
return SoftDeleteStrategy(self.storage)
class BulkTransformer(CryptoChildTransformerCreatorMixin,
GetPrimaryKeyTransformerMixin,
SupportedModelsMixin,
SoftDeleteMixin,
ManyTransformer):
"""Transformer for entry list."""
child_transformer_class = CryptoBulkEntryTransformer
def __init__(self, crypto_controller, **kwargs):
"""Construct new transformer for entry list."""
self.crypto_controller = crypto_controller
super(BulkTransformer, self).__init__(**kwargs)
self.deleted_sets_transformer = DeleteSetsTransformer(
storage=self.storage, account_manager=self.account_manager
)
def to_model(self, payload):
"""Convert payload with set list."""
models = {i: [] for i in self.mapping}
models['last_synced'] = payload.pop('now')
bad_encrypted_models = []
for set_name, transformer in self.mapping.items():
if set_name == 'group_set':
payload[set_name] = self.sort_groups(payload[set_name])
saved, to_delete = self.to_model_child_list(
transformer, payload[set_name]
)
bad_encrypted_models.extend(to_delete)
models[set_name] = saved
models['deleted_sets'] = self.deleted_sets_transformer.to_model(
payload.pop('deleted_sets')
)
self.delete_list(bad_encrypted_models)
return models
def to_payload(self, model):
"""Convert model to payload with set list."""
payload = {}
payload['last_synced'] = model.pop('last_synced')
payload['delete_sets'] = self.deleted_sets_transformer.to_payload(None)
for set_name, transformer in self.mapping.items():
internal_model = self.storage.filter(
transformer.model_class, any,
**{
'remote_instance.state.rcontains': ['created', 'updated'],
'remote_instance': None
}
)
payload[set_name] = [
transformer.to_payload(i) for i in internal_model
]
return payload
def sort_groups(self, groups):
"""Sort to prevent missing parent groups."""
def has_parent(group):
return group['parent_group'] is not None
sorted_groups = [i for i in groups if not has_parent(i)]
not_sorted_groups = [i for i in groups if has_parent(i)]
added_group_id = [i['id'] for i in sorted_groups]
while not_sorted_groups:
for i in not_sorted_groups[:]:
if i['parent_group']['id'] in added_group_id:
sorted_groups.append(i)
added_group_id.append(i['id'])
not_sorted_groups.remove(i)
return sorted_groups
def to_model_child_list(self, transformer, payload):
"""Process dictionary list with tranformer.
This returns 2-size tuple where the first item is saved model list and
second one is model list for soft delete.
"""
bad_encrypted_models = []
models = []
for i in payload:
try:
child_model = transformer.to_model(i)
except DeletBadEncrypted as exception:
bad_encrypted_models.append(exception.model)
else:
models.append(child_model)
return models, bad_encrypted_models
class DeleteSetsTransformer(GetPrimaryKeyTransformerMixin,
SupportedModelsMixin,
SoftDeleteMixin,
ManyTransformer):
"""Transformer for deleted_sets field."""
@property
def supported_models(self):
"""Return model tuple to sync."""
return reversed(
super(DeleteSetsTransformer, self).supported_models
)
def create_child_transformer(self, model):
"""Create transformer for sub transformers."""
return self.get_primary_key_transformer(model)
def to_model(self, payload):
"""Handle payload to local models and delete them completely."""
model = self.soft_delete_entries(payload)
self.storage.confirm_delete(payload)
return model
def to_payload(self, model):
"""Retrieve local deleted_set."""
return self.get_delete_strategy().get_delete_sets()
def soft_delete_entries(self, payload):
"""Remove user data and add them to local delete_sets."""
model = {}
for set_name, transformer in self.mapping.items():
deleted_set_with_none = [
self._map_remote_id_to_model(transformer, i)
for i in payload[set_name]
]
deleted_set = [i for i in deleted_set_with_none if i]
model[set_name] = deleted_set
self.delete_list(model[set_name])
return model
# pylint: disable=no-self-use
def _map_remote_id_to_model(self, transformer, remote_id):
try:
return transformer.to_model(remote_id)
except (DoesNotExistException, SkipField):
return None