def __init__(self, connection: "Connection", name: str = None, id: str = None) -> None: """Initialize UserGroup object by passing name or id. Args: connection: MicroStrategy connection object returned by `connection.Connection()` name: name of User Group id: ID of User Group """ if id is None and name is None: helper.exception_handler( "Please specify either 'name' or 'id' parameter in the constructor." ) if id is None: user_groups = UserGroup._get_usergroup_ids(connection=connection, name_begins=name, name=name) if user_groups: id = user_groups[0] else: helper.exception_handler( "There is no User Group with the given name: '{}'".format( name), exception_type=ValueError) super().__init__(connection=connection, object_id=id)
def _list_all(cls, connection: "Connection", name: str = None, to_dictionary: bool = False, to_dataframe: bool = False, limit: int = None, **filters) -> Union[List["Document"], List[dict]]: msg = "Error retrieving documents from the environment." if to_dictionary and to_dataframe: helper.exception_handler( "Please select either `to_dictionary=True` or `to_dataframe=True`, but not both.", ValueError) objects = helper.fetch_objects_async( connection, api=documents.get_documents, async_api=documents.get_documents_async, dict_unpack_value='result', limit=limit, chunk_size=1000, error_msg=msg, filters=filters, search_term=name) if to_dictionary: return objects elif to_dataframe: return DataFrame(objects) else: return cls._from_bulk_response(connection, objects)
def __definition(self): """Get the definition of a report, including attributes and metrics. Implements GET /v2/reports/<report_id>""" res = reports.report_definition(connection=self._connection, report_id=self._report_id) definition = res.json() grid = definition["definition"]["grid"] if version.parse(self._connection.iserver_version) >= version.parse("11.2.0100"): self._subtotals = grid["subtotals"] self._name = definition["name"] self.cross_tab = grid["crossTab"] available_objects = definition['definition']['availableObjects'] # Check if report have custom groups or consolidations if available_objects['customGroups']: helper.exception_handler(msg="Reports with custom groups are not supported.", exception_type=ImportError) if available_objects['consolidations']: helper.exception_handler(msg="Reports with consolidations are not supported.", exception_type=ImportError) metrics_position = grid["metricsPosition"] full_metrics = grid[metrics_position["axis"]][metrics_position["index"]]["elements"] full_attributes = [] for elem in grid["rows"]: if elem["type"] == "attribute": full_attributes.append(elem) for elem in grid["columns"]: if elem["type"] == "attribute": full_attributes.append(elem) self._attributes = [{'name': attr['name'], 'id': attr['id']} for attr in full_attributes] self._metrics = [{'name': metr['name'], 'id': metr['id']} for metr in full_metrics]
def __already_recipient(recipient): if recipient in exisiting_recipients: helper.exception_handler( f"{recipient} is already a recipient of subscription", UserWarning) else: ready_recipients.append(recipient)
def __prepare_recipients(self, recipients): exisiting_recipients = [rec['id'] for rec in self.recipients] ready_recipients = [] def __already_recipient(recipient): if recipient in exisiting_recipients: helper.exception_handler( f"{recipient} is already a recipient of subscription", UserWarning) else: ready_recipients.append(recipient) for recipient in recipients: not_dict_msg = """Wrong recipient format, expected format is {"id": recipient_id, "type": "CONTACT_GROUP" / "USER_GROUP" / "CONTACT" / "USER" / "PERSONAL_ADDRESS" / "UNSUPPORTED" "includeType": "TO" / "CC" / "BCC" (optional) }""" if isinstance(recipient, dict): if list(recipient.keys()) in [['id', 'type', 'includeType'], ['id', 'type']]: __already_recipient(recipient['id']) else: helper.exception_handler(not_dict_msg, TypeError) if isinstance(recipient, str): __already_recipient(recipient) return ready_recipients
def __multitable_definition(self): """Return all tables names and columns as a dictionary""" if not self._table_definition: try: res_tables = datasets.dataset_definition( connection=self._connection, dataset_id=self._cube_id, fields=['tables', 'columns']) _ds_definition = res_tables.json() for table in _ds_definition['result']['definition'][ 'availableObjects']['tables']: column_list = [ column['columnName'] for column in _ds_definition['result']['definition'] ['availableObjects']['columns'] if table['name'] == column['tableName'] ] self._table_definition[table['name']] = column_list except: helper.exception_handler( "Some functionality is not available with this type of cube at the moment.", throw_error=False, exception_type=Warning, stack_lvl=3) return self._table_definition
def is_loaded(self, application_id: str = None, application_name: str = None) -> bool: """Check if application is loaded, by passing application ID or name, returns True or False. Args: application_id: Application ID application_name: Application name """ if application_id is None and application_name is None: helper.exception_handler( "Please specify either 'application_name' or 'application_id' argument." ) if application_id is None: app_list = Application._list_application_ids(self.connection, name=application_name) application_id = app_list[ 0] if app_list else helper.exception_handler( "There is no application with the given name: '{}'".format( application_name), exception_type=ValueError) nodes = self.list_nodes(application_id=application_id) loaded = False for node in nodes: status = node['projects'][0]['status'] loaded = True if status == 'loaded' else False if loaded: break return loaded
def __init__(self, connection: "Connection", name: str = None, id: str = None): """Initialize Security Role object by passing name or id. Args: connection: MicroStrategy connection object returned by `connection.Connection()`. name: name of Security Role id: ID of Security Role """ # initialize either by ID or name if id is None and name is None: helper.exception_handler( "Please specify either 'name' or 'id' parameter in the constructor.", ValueError) if id is None: security_roles = SecurityRole._list_security_role_ids( connection=connection, name=name) if security_roles: id = security_roles[0] else: helper.exception_handler( "There is no Security Role associated with the given name: '{}'" .format(name), exception_type=ValueError) super().__init__(connection=connection, object_id=id, name=name)
def revoke_from(self, members: Union[List[str], List["User"], List["UserGroup"]], application: Union["Application", str]) -> None: """Remove users/usergroups from a Security Role. Args: members(list): List of objects or IDs of Users or User Groups which will be removed from this Security Role. application(Application, str): Application object or name to which this removal will apply. """ from mstrio.admin.application import Application from mstrio.admin.user import User from mstrio.admin.usergroup import UserGroup if isinstance(application, Application): application_id = application.id application_name = application.name elif isinstance(application, str): application_list = Application._list_applications( connection=self.connection, to_dictionary=True, name=application) if application_list: application_id = application_list[0]['id'] application_name = application_list[0]['name'] else: helper.exception_handler( "Application name '{}' does not exist.".format( application)) else: helper.exception_handler( "Application parameter must be of type str or Application.") # create list of objects from strings/objects/lists members_list = members if isinstance(members, list) else [members] members_list = [ obj.id if isinstance(obj, (User, UserGroup)) else str(obj) for obj in members_list ] existing_ids = [ obj['id'] for obj in self.list_members(application_name=application_name) ] succeeded = list(set(members_list).intersection(set(existing_ids))) failed = list(set(members_list) - set(succeeded)) value = {"projectId": application_id, "memberIds": members_list} self._update_nested_properties(objects=value, path="members", op='remove') if succeeded: if config.verbose: print("Revoked Security Role '{}' from {}".format( self.name, succeeded)) if failed and config.verbose: print("Security Role '{}' does not have member(s) {}".format( self.name, failed))
def get_task(self, task_index: int) -> "SchemaTask": """Get all details of the task which is stored at a given `task_index` in a list from property `tasks`. Args: task_index (int): Index of the task in the list stored in property `tasks`. Returns: `SchemaTask` object with all details about the task from the given index. When index is not proper then `None` is returned and warning with explanation message is shown. """ try: task_id = self.tasks[task_index].id except IndexError: msg = ( f"Cannot get task with index {task_index} from the list of tasks for this " "schema management object. Check the list using property `tasks`." ) exception_handler(msg, Warning) return res = schema.read_task_status(self.connection, task_id, self.project_id) return self._save_task(res.json())
def remove_subscription(connection, subscription_id, project_id, error_msg=None, exception_type=None): """Remove (Unsubscribe) the subscription using subscription id. Args: connection(object): MicroStrategy connection object returned by `connection.Connection()`. subscription_id(str): ID of the subscription project_id(str): ID of the project error_msg(str, optional): Customized error message. exception_type (Exception): Instance of Exception or Warning class Returns: HTTP response object returned by the MicroStrategy REST server. """ response = connection.session.delete( url=connection.base_url + '/api/subscriptions/' + subscription_id, headers={'X-MSTR-ProjectID': project_id}) if config.debug: print(response.url) if not response.ok: if error_msg is None: error_msg = "Error unsubscribing Subscription {}".format( subscription_id) if exception_type is None: response_handler(response, error_msg) else: exception_handler(error_msg, exception_type) return response
def perform_full_migration(self) -> bool: """Perform 'create_package()' and 'migrate_package()' using configuration provided when creating `Migration` object. """ if not isinstance(self.source_connection, Connection) or self.source_connection is None: exception_handler( msg=("Migration object missing `source_connection`. " "`perform_full_migration()` unavailable."), exception_type=AttributeError) if not isinstance(self.target_connection, Connection) or self.target_connection is None: exception_handler( msg=("Migration object missing `target_connection`. " "`perform_full_migration()` unavailable."), exception_type=AttributeError) self._display_progress_bar( desc='Migration status: ', unit='Migration', total=4, bar_format='{desc} |{bar:50}| {percentage:3.0f}%') if not self.create_package(): return False if not self.migrate_package(): return False self._close_progress_bar() return True
def __init__(self, connection: "Connection", name: Optional[str] = None, id: Optional[str] = None) -> None: """Initialize DatasourceInstance object by passing name or id. To explore all available DatasourceInstance objects use the `list_datasource_instance()` method. Args: connection: MicroStrategy connection object returned by `connection.Connection()`. name: exact name of Datasource Instance id: ID of Datasource Instance """ if id is None and name is None: helper.exception_handler( "Please specify either 'id' or 'name' parameter in the constructor." ) if id is None: objects_info = DatasourceInstance._list_datasource_instances( connection=connection, name=name, to_dictionary=True) if objects_info: object_info, object_info["connection"] = objects_info[ 0], connection self._init_variables(**object_info) else: helper.exception_handler( f"There is no Datasource Instance: '{name}'", exception_type=ValueError) else: super().__init__(connection=connection, object_id=id)
def save_as(self, name, description=None, folder_id=None, table_name=None): """Creates a new single-table cube with the data frame stored in the Cube instance (cube.dataframe). Before the update, make sure that the data exists. Args: name(str): Name of cube. description(str): Description of the cube. folder_id (str, optional): ID of the shared folder that the dataset should be created within. If `None`, defaults to the user's My Reports folder. table_name (str, optional): Name of the table. If None (default), the first table name of the original cube will be used. """ if len(self._tables) > 1: helper.exception_handler( msg="""This feature works only for the single-table cubes. \rTo export multi-table cube use Dataset class.""" ) else: if table_name is None: table_name = self._tables[0]["name"] dataset = Dataset(self._connection, name=name, description=description) dataset.add_table(name=table_name, data_frame=self.dataframe, update_policy="add") dataset.create(folder_id=folder_id)
def _list_security_roles( cls, connection: "Connection", to_dictionary: bool = False, to_dataframe: bool = False, limit: int = None, **filters ) -> Union[List["SecurityRole"], List[Dict[str, Any]], DataFrame]: if to_dictionary and to_dataframe: helper.exception_handler( "Please select either to_dictionary=True or to_dataframe=True, but not both.", ValueError) objects = helper.fetch_objects( connection=connection, api=security.get_security_roles, limit=limit, filters=filters, ) if to_dictionary: return objects elif to_dataframe: return DataFrame(objects) else: return [ cls.from_dict(source=obj, connection=connection) for obj in objects ]
def revoke_privilege(self, privilege: Union[str, List[str], "Privilege", List["Privilege"]]) -> None: """Revoke directly granted User Group privileges. Args: privilege: List of privilege objects, ids or names """ from mstrio.access_and_security.privilege import Privilege privileges = set( [priv['id'] for priv in Privilege._validate_privileges(self.connection, privilege)]) existing_ids = [ privilege['privilege']['id'] for privilege in self.list_privileges(mode='ALL') ] directly_granted = set( [privilege['privilege']['id'] for privilege in self.list_privileges(mode='GRANTED')]) to_revoke = list(privileges.intersection(directly_granted)) not_directly_granted = list( (set(existing_ids) - directly_granted).intersection(privileges)) if not_directly_granted: msg = (f"Privileges {sorted(not_directly_granted)} are inherited and will be " "ommited. Only directly granted privileges can be revoked by this method.") helper.exception_handler(msg, exception_type=Warning) succeeded, failed = self._update_nested_properties(to_revoke, "privileges", "remove", existing_ids) if succeeded: self.fetch('privileges') # fetch the object privileges if config.verbose: print("Revoked privilege(s) {} from '{}'".format(succeeded, self.name)) if failed and config.verbose: print("User group '{}' does not have privilege(s) {}".format(self.name, failed))
def _list_applications(cls, connection: "Connection", to_dictionary: bool = False, limit: int = None, **filters) -> List["Application"]: msg = "Error getting information for a set of Applications." objects = helper.fetch_objects_async(connection, monitors.get_projects, monitors.get_projects_async, dict_unpack_value='projects', limit=limit, chunk_size=50, error_msg=msg, filters=filters) if to_dictionary: return objects else: apps = cls._from_bulk_response(connection, objects) apps_loaded = Application._list_loaded_applications( connection, to_dictionary=True) apps_loaded_ids = [app['id'] for app in apps_loaded] unloaded = [app for app in apps if app.id not in apps_loaded_ids] if unloaded: msg = "Applications {} are either unloaded or idled. Change status using the 'load()' or 'resume()' method to use all functionality.".format( [app.name for app in unloaded]) helper.exception_handler(msg, exception_type=UserWarning) return apps
def update_acl(self, op: str, rights: int, ids: List[str], propagate_to_children: bool = None, denied: bool = None, inheritable: bool = None): """Updates the access control list for this entity, performs operation defined by the `op` parameter on all objects from `ids` list. Args: op (str): ACL update operator, available values are "ADD", "REMOVE" and "REPLACE" rights (int): value of rights to use by the operator ids (list of str): list of ids to update the acl on propagate_to_children (optional, bool): used for folder objects only, default value is None, if set to True/False adds `propagateACLToChildren` keyword to the request body and sets its value accordingly """ # TODO move (op, rights, ids, propagate_to_children=None, # denied=None, inheritable=None, types=None) to separate AccesControlEntry class if op not in ["ADD", "REMOVE", "REPLACE"]: helper.exception_handler( "Wrong ACL operator passed. Please use ADD, REMOVE or REPLACE") if rights not in range(256) and rights not in range( 536_870_912, 536_871_168): helper.exception_handler( "Wrong `rights` value, please provide value in range 0-255, or to control inheritability use value 536870912" )
def is_loaded(self, project_id: Optional[str] = None, project_name: Optional[str] = None) -> bool: """Check if project is loaded, by passing project ID or name, returns True or False. Args: project_id: Project ID project_name: Project name """ if project_id is None and project_name is None: helper.exception_handler( "Please specify either 'project_name' or 'project_id' argument." ) if project_id is None: project_list = Project._list_project_ids(self.connection, name=project_name) if project_list: project_id = project_list[0] else: msg = f"There is no project with the given name: '{project_name}'" raise ValueError(msg) nodes = self.list_nodes(project=project_id) loaded = False for node in nodes: status = node['projects'][0]['status'] loaded = True if status == 'loaded' else False if loaded: break return loaded
def __init__(self, connection: Connection, id: str = None, name: str = None) -> None: """Initialize the Schedule object, populates it with I-Server data. Args: connection: MicroStrategy connection object returned by `connection.Connection()`. id: Schedule ID name: Schedule name """ if id is None and name is None: helper.exception_handler( "Please specify either 'name' or 'id' parameter in the constructor." ) if id is None: sm = ScheduleManager(connection) schedule = sm.list_schedules(name=name) if schedule: id = schedule[0]['id'] else: helper.exception_handler( "There is no schedule with the given name: '{}'".format( name), exception_type=ValueError) self.connection = connection self.id = id self.__fetch()
def _migrate_package(self, binary: bytes, is_undo: bool = False, custom_package_path: Optional[str] = None) -> bool: if binary is None: exception_handler( msg=("Import package is None. Run `create_package()` first, " "or specify `custom_package_path`."), exception_type=AttributeError) self.__private_status = MigrationStatus.MIGRATION_IN_PROGRESS self._create_package_holder(self.target_connection) self._upload_package_binary(binary) if not self._create_import(self.target_connection): self.__private_status = MigrationStatus.MIGRATION_FAILED return False if not is_undo and self.create_undo: self._download_undo_binary() file_path = custom_package_path if custom_package_path else self.save_path filename, file_extension = self._decompose_file_path(file_path) self._save_package_binary_locally(filename=f"{filename}_undo", file_extension=file_extension, _bytes=self.undo_binary) self._delete_package_holder() self._delete_import() self.__private_status = MigrationStatus.MIGRATION_COMPLETED return True
def migrate_package(self, custom_package_path: Optional[str] = None, is_undo=False) -> bool: """Performs migration of already created package to the target environment. Import package will be loaded from `custom_package_path`. If `custom_package_path` not provided, the object previously acquired with the `create_package()` will be used. If `create_undo` parameter is set to True, package needed for undo process will be downloaded. Raises AttributeError if `target_connection` is not specified. """ if not isinstance(self.target_connection, Connection) or self.target_connection is None: exception_handler( msg=("Migration object does not have `target_connection`. " "Export unavailable."), exception_type=AttributeError) if custom_package_path and self._check_file_path( custom_package_path, "custom_package_path"): self.save_path = custom_package_path filename, file_extension = self._decompose_file_path( custom_package_path) with open(f"{filename}{file_extension}", "rb") as f: return self._migrate_package(f.read(), is_undo=is_undo) return self._migrate_package(self._package_binary, custom_package_path=custom_package_path, is_undo=is_undo)
def _validate_settings(self, settings: dict = None, bad_setting=Warning, bad_type=Warning, bulk_error=True) -> None: """Validate setting-value pairs and raise AttributeError or TypeError if invalid. If `bad_setting` or `bad_type` is of type Exception, then Exception is raised as soon as the first invalid pair is found. If they are of type Warning the validation will continue and not raise error. Raises: ValueError if `bulk_error` True, tries to evaluate all settings. """ settings = settings if settings else self.list_properties(show_names=False) bad_settings_keys = [] for setting, value in settings.items(): if setting not in self._CONFIG.keys(): msg = "Setting '{}' is not supported.".format(setting) helper.exception_handler(msg, bad_setting) bad_settings_keys.append((setting, value)) else: setting_obj = getattr(self, setting) if isinstance(setting_obj, DeprecatedSetting): continue else: valid = setting_obj._validate_value(value, exception=not bulk_error) if not valid: bad_settings_keys.append((setting, value)) if bulk_error and bad_settings_keys: helper.exception_handler( "Invalid settings: {}".format( [item[0] + ': ' + str(item[1]) for item in bad_settings_keys]), exception_type=ValueError)
def list_privileges(cls, connection: Connection, to_dictionary: bool = False, to_dataframe: bool = False, **filters) -> Union[List["Privilege"], List[dict], DataFrame]: """Get list of privilege objects or privilege dicts. Filter the privileges by specifying the `filters` keyword arguments. Optionally use `to_dictionary` or `to_dataframe` to choose output format. Args: connection: MicroStrategy connection object returned by `connection.Connection()`. to_dictionary: If `True` returns dict, by default (False) returns User objects. to_dataframe: If `True`, returns `DataFrame`. **filters: Available filter parameters: ['id', 'name', 'description', 'categories', 'is_project_level_privilege'] Examples: >>> Privilege.list_privileges(connection, to_dataframe=True, >>> is_project_level_privilege='True', >>> id=[1,2,3,4,5]) """ if to_dictionary and to_dataframe: helper.exception_handler( "Please select either `to_dictionary=True` or `to_dataframe=True`, but not both.", ValueError) objects = helper.fetch_objects(connection=connection, api=security.get_privileges, limit=None, filters=filters) if to_dictionary: return objects elif to_dataframe: return DataFrame(objects) else: return [cls.from_dict(source=obj, connection=connection) for obj in objects]
def _list_all(cls, connection: Connection, name: Optional[str] = None, to_dictionary: bool = False, to_dataframe: bool = False, limit: Optional[int] = None, **filters) -> Union[List["Dossier"], List[dict], DataFrame]: msg = "Error retrieving documents from the environment." if to_dictionary and to_dataframe: helper.exception_handler( "Please select either to_dictionary=True or to_dataframe=True, but not both.", ValueError) objects = helper.fetch_objects_async( connection, api=documents.get_dossiers, async_api=documents.get_dossiers_async, dict_unpack_value='result', limit=limit, chunk_size=1000, error_msg=msg, filters=filters, search_term=name) if to_dictionary: return objects elif to_dataframe: return DataFrame(objects) else: return [ cls.from_dict(source=obj, connection=connection) for obj in objects ]
def __init__(self, connection: Connection, name: Optional[str] = None, id: Optional[str] = None) -> None: """Initialize UserGroup object by passing `name` or `id`. When `id` is provided (not `None`), `name` is omitted. Args: connection: MicroStrategy connection object returned by `connection.Connection()` name: name of User Group id: ID of User Group """ if id is None and name is None: helper.exception_handler( "Please specify either 'name' or 'id' parameter in the constructor." ) if id is None: user_groups = UserGroup._get_user_group_ids(connection=connection, name_begins=name, name=name) if user_groups: id = user_groups[0] else: helper.exception_handler( f"There is no User Group with the given name: '{name}'", exception_type=ValueError) super().__init__(connection=connection, object_id=id, name=name)
def _select(self, object_id): attr_form_object_id = None if isinstance(object_id, list): for i in set(object_id): self._select(object_id=i) else: # object_id = object_id.split(";") # if isinstance(object_id, list): if len(object_id) > 32 and object_id[32] == ';': attr_form_object_id = [object_id[:32], object_id[33:]] object_id = attr_form_object_id[0] if self.__invalid(object_id): raise ValueError(self.err_msg_invalid.format(object_id)) if self.__duplicated(object_id): helper.exception_handler(msg=self.err_msg_duplicated.format(object_id), exception_type=Warning) else: typ = self.__type(object_id) if typ == "attribute": if attr_form_object_id: self.attr_selected.append(attr_form_object_id) else: self.attr_selected.append([object_id]) if typ == "metric": self.metr_selected.append(object_id)
def dataframe(self): if self._dataframe is None: helper.exception_handler( msg= "Dataframe not loaded. Retrieve with Report.to_dataframe().", exception_type=Warning) return self._dataframe
def __init__(self, id: str, action: Union[Action, str] = Action.USE_EXISTING, name: Optional[str] = None, version: Optional[str] = None, type: Optional[ObjectTypes] = None, owner: Optional[Owner] = None, date_created: Optional[str] = None, date_modified: Optional[str] = None, include_dependents: Optional[bool] = None, explicit_included: Optional[bool] = None, level: Optional[Union[Level, str]] = None): self.id = id self.name = name self.version = version self.type = type self.owner = owner self.date_created = date_created self.date_modified = date_modified self.include_dependents = include_dependents self.explicit_included = explicit_included try: self.level = PackageContentInfo.Level(level) if isinstance( level, str) else level self.action = PackageContentInfo.Action(action) if isinstance( action, str) else action except ValueError: exception_handler(msg="Wrong enum value", exception_type=ValueError)
def disconnect_users(self, connection_ids: Union[str, List[str]] = None, users: Union[List["User"], List[str]] = None, nodes: Union[str, List[str]] = None, force: bool = False, **filters) -> None: """Disconnect user connections by passing in users (objects) or connection_ids. Optionally disconnect users by specifying the `filters` keyword arguments. Args: connection_ids: chosen ids that can be retrieved with `list_connections()` users: List of User objects or usernames nodes: Node (server) names on which users will be disconnected force: if True, no additional prompt will be showed before disconnecting users **filters: Available filter parameters: ['id', 'parent_id', 'username', 'user_full_name', 'project_index', 'project_id', 'project_name', 'open_jobs_count', 'application_type', 'date_connection_created', 'duration', 'session_id', 'client', 'config_level'] """ from mstrio.admin.user import User # import here to avoid circular imports if self.connection and not connection_ids and not users and not filters and not force: helper.exception_handler( "You need to pass connection_ids or users or specify filters. To disconnect all connections use `disconnect_all_users()` method." ) if connection_ids: # disconnect specific user connections without fetching connections self.__disconnect_by_connection_id(connection_ids) else: # get all user connections to filter locally all_connections = self.list_connections(nodes, **filters) if users: # filter user connections by user objects users = users if isinstance(users, list) else [users] usernames = [] for user in users: if isinstance(user, User): usernames.append(user.username) elif isinstance(user, str): usernames.append(user) else: helper.exception_handler( "'user' param must be a list of User objects or usernames.", exception_type=TypeError) all_connections = list( filter(lambda conn: conn['username'] in usernames, all_connections)) if all_connections: # extract connection ids and disconnect connection_ids = [conn['id'] for conn in all_connections] self.__disconnect_by_connection_id(connection_ids) elif config.verbose: print("Selected user(s) do not have any active connections")