modular_sdk/services/parent_service.py (390 lines of code) (raw):

from typing import Optional, Iterator, Union, List from pynamodb.expressions.condition import Condition from datetime import datetime from modular_sdk.commons import RESPONSE_BAD_REQUEST_CODE, \ RESPONSE_RESOURCE_NOT_FOUND_CODE from modular_sdk.commons import generate_id from modular_sdk.commons.constants import ALL_PARENT_TYPES, ParentScope, \ COMPOUND_KEYS_SEPARATOR, CLOUD_PROVIDERS, ParentType from modular_sdk.commons.exception import ModularException from modular_sdk.commons.log_helper import get_logger from modular_sdk.commons.time_helper import java_timestamp, utc_datetime from modular_sdk.commons.time_helper import utc_iso from modular_sdk.models.parent import Parent from modular_sdk.models.tenant import Tenant from modular_sdk.modular import Modular from modular_sdk.services.customer_service import CustomerService from modular_sdk.services.tenant_service import TenantService _LOG = get_logger(__name__) class ParentService: def __init__(self, tenant_service: TenantService, customer_service: CustomerService): self.customer_service = customer_service self.tenant_service = tenant_service @staticmethod def get_parent_by_id(parent_id: str) -> Optional[Parent]: return Parent.get_nullable(hash_key=parent_id) @staticmethod def list(): # todo do not use it return list(Parent.scan()) @staticmethod def list_application_parents(application_id, only_active=True): return list(ParentService.i_list_application_parents( application_id=application_id, only_active=only_active )) @staticmethod def i_list_application_parents(application_id: str, type_: Optional[ParentType] = None, scope: Optional[ParentScope] = None, tenant_or_cloud: Optional[str] = None, by_prefix: Optional[bool] = False, only_active=True, limit: Optional[int] = None, last_evaluated_key: Optional[dict] = None, rate_limit: Optional[int] = None ) -> Iterator[Parent]: # can be an empty string is we want to retrieve with literally '' cloud is_tenant_or_cloud = isinstance(tenant_or_cloud, str) if is_tenant_or_cloud and not scope or scope and not type_: raise AssertionError('invalid usage') if type_ and scope and is_tenant_or_cloud: key = COMPOUND_KEYS_SEPARATOR.join((type_, scope, tenant_or_cloud)) if by_prefix: rkc = Parent.type_scope.startswith(key) else: rkc = (Parent.type_scope == key) elif type_ and scope: rkc = Parent.type_scope.startswith(COMPOUND_KEYS_SEPARATOR.join(( type_, scope, '' ))) elif type_: rkc = Parent.type_scope.startswith( f'{type_}{COMPOUND_KEYS_SEPARATOR}') else: rkc = None if only_active: rkc &= (Parent.is_deleted == False) return Parent.application_id_index.query( hash_key=application_id, filter_condition=rkc, limit=limit, last_evaluated_key=last_evaluated_key, rate_limit=rate_limit ) def i_get_parent_by_customer(self, customer_id: str, parent_type: Optional[Union[ParentType, List[ParentType]]] = None, # noqa is_deleted: Optional[bool] = None, meta_conditions: Optional[Condition] = None, limit: Optional[int] = None, last_evaluated_key: Optional[dict] = None ) -> Iterator[Parent]: """ Meta conditions can be used like this: parent = next(parent_service.i_get_parent_by_customer( customer_id='EPAM Systems', parent_type='CUSTODIAN', is_deleted=False, meta_conditions=(Parent.meta['key'] == 'value'), limit=1 ), None) :param customer_id: :param parent_type: :param is_deleted: :param meta_conditions: :param limit: :param last_evaluated_key: :return: """ condition = meta_conditions rkc = None if isinstance(parent_type, list): condition &= (Parent.type.is_in(*parent_type)) elif parent_type: # enum value or str rkc = Parent.type_scope.startswith( self.build_type_scope(parent_type) ) if isinstance(is_deleted, bool): condition &= (Parent.is_deleted == is_deleted) return Parent.customer_id_scope_index.query( hash_key=customer_id, range_key_condition=rkc, filter_condition=condition, limit=limit, last_evaluated_key=last_evaluated_key ) def build(self, application_id: str, customer_id: str, parent_type: ParentType, created_by: str, is_deleted: bool = False, description: Optional[str] = None, meta: Optional[dict] = None, scope: Optional[ParentScope] = None, tenant_name: Optional[str] = None, cloud: Optional[str] = None) -> Parent: """ Make sure to provide valid scope, tenant_name and cloud. Or use specific methods: create_all_scope, create_tenant_scope :param application_id: :param customer_id: :param parent_type: :param is_deleted: :param description: :param meta: :param scope: :param tenant_name: :param cloud: :param created_by: :return: """ # TODO either move validation from here to outside or make the # validation decent (what if application by id does not exist, # what if meta for this parent_type is not valid?, ...) if parent_type not in ALL_PARENT_TYPES: _LOG.warning(f'Invalid parent type specified \'{parent_type}\'. ' f'Available options: {ALL_PARENT_TYPES}') raise ModularException( code=RESPONSE_BAD_REQUEST_CODE, content=f'Invalid parent type specified \'{parent_type}\'. ' f'Available options are: {ALL_PARENT_TYPES}' ) customer = self.customer_service.get(name=customer_id) if not customer: _LOG.error(f'Customer with name \'{customer_id}\' does not exist') raise ModularException( code=RESPONSE_RESOURCE_NOT_FOUND_CODE, content=f'Customer with name \'{customer_id}\' does not exist' ) return self._create( customer_id=customer_id, application_id=application_id, type_=parent_type, description=description, meta=meta, is_deleted=is_deleted, scope=scope, tenant_name=tenant_name, cloud=cloud, created_by=created_by ) @staticmethod def get_dto(parent: Parent) -> dict: dct = parent.get_json() ct = dct.pop('creation_timestamp', None) if ct: dct['created_at'] = utc_iso(datetime.fromtimestamp(ct / 1e3)) dct.pop('type_scope', None) dct['scope'] = parent.scope if parent.cloud: dct['cloud'] = parent.cloud if parent.tenant_name: dct['tenant_name'] = parent.tenant_name return dct @staticmethod def save(parent: Parent): parent.save() def update_meta(self, parent: Parent, updated_by: str): _LOG.debug(f'Going to update parent {parent.parent_id} meta') self.update( parent=parent, attributes=[ Parent.meta ], updated_by=updated_by ) _LOG.debug('Parent meta was updated') @staticmethod def update(parent: Parent, attributes: List, updated_by: str): updatable_attributes = [ Parent.description, Parent.meta, Parent.type, Parent.updated_by, Parent.is_deleted, Parent.type_scope ] actions = [] for attribute in attributes: if attribute not in updatable_attributes: _LOG.warning(f'Attribute {attribute.attr_name} ' f'can\'t be updated.') continue python_attr_name = Parent._dynamo_to_python_attr( attribute.attr_name) update_value = getattr(parent, python_attr_name) actions.append(attribute.set(update_value)) actions.append(Parent.updated_by.set(updated_by)) actions.append( Parent.update_timestamp.set(int(utc_datetime().timestamp() * 1e3))) parent.update(actions=actions) @staticmethod def mark_deleted(parent: Parent): """ Updates the item in DB! No need to save afterwards :param parent: :return: """ _LOG.debug(f'Going to mark the parent {parent.parent_id} as deleted') if parent.is_deleted: _LOG.warning(f'Parent \'{parent.parent_id}\' is already deleted.') return parent.update(actions=[ Parent.is_deleted.set(True), Parent.deletion_timestamp.set(int(utc_datetime().timestamp() * 1e3)) ]) _LOG.debug('Parent was marked as deleted') @staticmethod def force_delete(parent: Parent): _LOG.debug(f'Going to delete parent {parent.parent_id}') parent.delete() _LOG.debug('Parent has been deleted') # new methods @staticmethod def build_type_scope(type_: ParentType, scope: Optional[ParentScope] = None, tenant_name: Optional[str] = None, cloud: Optional[str] = None) -> str: """ All asserts here are against developer errors :param type_: :param scope: :param tenant_name: :param cloud: :return: """ scope = scope or '' tenant_name = tenant_name or '' cloud = (cloud or '').upper() # assert type_ in ALL_PARENT_TYPES, f'Invalid parent type {type_}' if not scope: _LOG.debug('Scope was not provided to build_type_scope. ' 'Keeping tenant and cloud empty') return COMPOUND_KEYS_SEPARATOR.join((type_, scope, '')) if cloud: assert cloud in CLOUD_PROVIDERS, f'Invalid cloud: {cloud}' if scope == ParentScope.ALL: return COMPOUND_KEYS_SEPARATOR.join((type_, scope, cloud)) # scope in (ParentScope.DISABLED, ParentScope.SPECIFIC) return COMPOUND_KEYS_SEPARATOR.join((type_, scope, tenant_name)) def _create(self, customer_id: str, application_id: str, type_: ParentType, created_by: str, description: Optional[str] = None, meta: Optional[dict] = None, is_deleted: bool = False, scope: Optional[ParentScope] = None, tenant_name: Optional[str] = '', cloud: Optional[str] = '') -> Parent: """ Raw create without excessive validations :param customer_id: :param application_id: :param type_: :param description: :param meta: :param is_deleted: :param scope: :param tenant_name: :param cloud: :param created_by: :return: """ return Parent( parent_id=generate_id(), customer_id=customer_id, application_id=application_id, type=type_.value if isinstance(type_, ParentType) else type_, created_by=created_by, description=description, meta=meta if isinstance(meta, dict) else {}, is_deleted=is_deleted, creation_timestamp=int(java_timestamp()), type_scope=self.build_type_scope(type_, scope, tenant_name, cloud) ) def create_all_scope(self, application_id: str, customer_id: str, type_: ParentType, created_by: str, is_deleted: bool = False, description: Optional[str] = None, meta: Optional[dict] = None, cloud: Optional[str] = None) -> Parent: return self._create( application_id=application_id, customer_id=customer_id, type_=type_, is_deleted=is_deleted, description=description, meta=meta, scope=ParentScope.ALL, cloud=cloud, created_by=created_by ) def create_tenant_scope(self, application_id: str, customer_id: str, type_: ParentType, tenant_name: str, created_by: str, disabled: bool = False, is_deleted: bool = False, description: Optional[str] = None, meta: Optional[dict] = None) -> Parent: return self._create( application_id=application_id, customer_id=customer_id, type_=type_, is_deleted=is_deleted, description=description, meta=meta, scope=ParentScope.DISABLED if disabled else ParentScope.SPECIFIC, tenant_name=tenant_name, created_by=created_by ) def query_by_scope_index(self, customer_id: str, type_: Optional[ParentType] = None, scope: Optional[ParentScope] = None, tenant_or_cloud: Optional[str] = None, by_prefix: Optional[bool] = False, is_deleted: Optional[bool] = False, limit: Optional[int] = None, last_evaluated_key: Optional[dict] = None, ascending: Optional[bool] = True ) -> Iterator[Parent]: """ Low-level query method :param customer_id: :param type_: :param scope: :param tenant_or_cloud: :param by_prefix: :param is_deleted: :param limit: :param last_evaluated_key: :param ascending: :return: """ # can be an empty string is we want to retrieve with literally '' cloud is_tenant_or_cloud = isinstance(tenant_or_cloud, str) if is_tenant_or_cloud and not scope or scope and not type_: raise AssertionError('invalid usage') if type_ and scope and is_tenant_or_cloud: key = COMPOUND_KEYS_SEPARATOR.join((type_, scope, tenant_or_cloud)) if by_prefix: rkc = Parent.type_scope.startswith(key) else: rkc = (Parent.type_scope == key) elif type_ and scope: rkc = Parent.type_scope.startswith(COMPOUND_KEYS_SEPARATOR.join(( type_, scope, '' ))) elif type_: rkc = Parent.type_scope.startswith( f'{type_}{COMPOUND_KEYS_SEPARATOR}') else: rkc = None fc = None if isinstance(is_deleted, bool): fc = (Parent.is_deleted == is_deleted) return Parent.customer_id_scope_index.query( hash_key=customer_id, range_key_condition=rkc, limit=limit, last_evaluated_key=last_evaluated_key, scan_index_forward=ascending, filter_condition=fc ) def get_by_tenant_scope(self, customer_id: str, type_: ParentType, tenant_name: Optional[str] = None, disabled: bool = False, limit: Optional[int] = None, last_evaluated_key: Optional[dict] = None, ascending: bool = True) -> Iterator[Parent]: return self.query_by_scope_index( customer_id=customer_id, type_=type_, scope=ParentScope.DISABLED if disabled else ParentScope.SPECIFIC, tenant_or_cloud=tenant_name, by_prefix=False, limit=limit, last_evaluated_key=last_evaluated_key, ascending=ascending ) def get_by_all_scope(self, customer_id: str, type_: ParentType, cloud: Optional[str] = None, limit: Optional[int] = None, last_evaluated_key: Optional[dict] = None, ascending: bool = True) -> Iterator[Parent]: # todo allow with empty cloud return self.query_by_scope_index( customer_id=customer_id, type_=type_, scope=ParentScope.ALL, tenant_or_cloud=cloud, by_prefix=False, limit=limit, last_evaluated_key=last_evaluated_key, ascending=ascending ) def get_linked_parent_by_tenant(self, tenant: Tenant, type_: ParentType ) -> Optional[Parent]: return self.get_linked_parent( tenant_name=tenant.name, cloud=tenant.cloud, customer_name=tenant.customer_name, type_=type_ ) def get_linked_parent(self, tenant_name: str, cloud: Optional[str], customer_name: str, type_: ParentType) -> Optional[Parent]: """ :param tenant_name: :param cloud: :param customer_name: :param type_: :return: """ _LOG.debug(f'Looking for a disabled parent with type {type_} for ' f'tenant {tenant_name}') disabled = next(self.get_by_tenant_scope( customer_id=customer_name, type_=type_, tenant_name=tenant_name, disabled=True, limit=1 ), None) if disabled: _LOG.info('Disabled parent is found. Returning None') return _LOG.debug(f'Looking for a specific parent with type {type_} for ' f'tenant {tenant_name}') specific = next(self.get_by_tenant_scope( customer_id=customer_name, type_=type_, tenant_name=tenant_name, disabled=False, limit=1 ), None) if specific: _LOG.info('Specific parent is found. Returning it') return specific if cloud: _LOG.debug(f'Looking for a parent with scope ALL and type {type_} ' f'for tenant\'s cloud') all_cloud = next(self.get_by_all_scope( customer_id=customer_name, type_=type_, cloud=cloud.upper() ), None) if all_cloud: _LOG.info('Parent with type ALL and tenant\'s cloud found.') return all_cloud all_ = next(self.get_by_all_scope( customer_id=customer_name, type_=type_, ), None) if all_: _LOG.info('Parent with type ALL found.') return all_ _LOG.info(f'No parent with type {type_} for ' f'tenant {tenant_name} found')