zeex/core/compat.py
def if_we_need_multi_version_support():
def string_to_bool(x):
def string_unquote(x):
def string_to_list(x):
PYQT_VERSION = QtCore.__version_info__
PYQT_VERSION_INT = int(PYQT_VERSION[0])
TRUES = ['TRUE','YES','Y']
UNQUOTE_OPTIONS = {'"':'',
                   "'":'', 
                   "{":'',
                   "}":'',
                   "]":'',
                   '[':'',
                   '(':'',
                   ')':''}  
zeex/core/models/filetree.py
class FileTreeModel(QtGui.QFileSystemModel):
    def __init__(self, parent=None, root_dir=None):
        QtGui.QFileSystemModel.__init__(self, parent)
        self._root_dir = root_dir
        self.setRootPath(self._root_dir)

    def dropMimeData(self, mimedata, action, row, column, parentIndex):
        print(mimedata)
        print(action)
        print(row)
        print(column)
        print(parentIndex)

    def mimeData(self, indexes):
        print("MimeData")
        index = indexes[0]
        mimedata = QtCore.QMimeData()
        mimedata.setData(u'filepath', QtCore.QByteArray(u'testtest'))
        return mimedata

    def setData(self, index, value, role):
        super(FileTreeModel, self).setData(index, value, role)
zeex/core/views/actions/import_file.py
class DataFrameModelImportDialog(QtGui.QDialog, Ui_ImportFileDialog):
    signalImported = QtCore.Signal(str) # file path

    def __init__(self, df_manager: DataFrameModelManager, file_path=None, dir='', **kwargs):
        QtGui.QDialog.__init__(self, **kwargs)
        self.df_manager = df_manager
        self.file_path = file_path
        self.dir = dir
        if file_path is not None and dir == '':
            self.dir = os.path.dirname(file_path)
        self.setupUi(self)
        self.configure()

    def configure(self):
        self.checkBoxHasHeaders.setChecked(True)
        if self.file_path is not None:
            self.set_file_path(self.file_path)
        self.btnBrowseFilePath.clicked.connect(self.browse_file_path)
        self.buttonBox.accepted.connect(self.execute)
        configure_combo_box(self.comboBoxSeparator, list(SEPARATORS.keys()), 'Comma')
        configure_combo_box(self.comboBoxEncoding, list(ENCODINGS.keys()), 'ISO-8859-1')

    def set_file_path(self, file_path=None):
        if file_path is None:
            if self.file_path is None:
                file_path = self.lineEditFilePath.text()
            else:
                file_path = self.file_path
        if not os.path.isfile(file_path):
            last = self.df_manager.last_path_updated
            if last is not None:
                kwargs = dict(dir=os.path.dirname(last))
            else:
                kwargs = dict(dir=self.dir)
            file_path = QtGui.QFileDialog.getOpenFileName(**kwargs)[0]
        self.lineEditFilePath.setText(str(file_path))

    def browse_file_path(self):
        last = self.df_manager.last_path_updated
        if last is not None and self.dir == '':
            kwargs = dict(dir=os.path.dirname(last))
        else:
            kwargs = dict(dir=self.dir)
        file_path = QtGui.QFileDialog.getOpenFileName(**kwargs)[0]
        self.set_file_path(file_path)

    def process_dataframe(self, df, trim_spaces=False, remove_linebreaks=False, parse_dates=False):
        if trim_spaces and not remove_linebreaks:
            for c in df.columns:
                if str(df[c].dtype) == 'object':
                    df.loc[:, c] = pandatools.series_strip(df.loc[:, c])
        elif remove_linebreaks:
            df = pandatools.dataframe_remove_linebreaks(df, copy=False)

        if parse_dates:
            df = pandatools.dataframe_to_datetime(df)
        return df

    def set_separator(self, sep):
        try:
            SEPARATORS[sep]
        except KeyError:
            mapper = {v: k for k, v in SEPARATORS.items()}
            try:
                sep = mapper[sep]
            except KeyError:
                self.radioBtnOtherSeparator.setChecked(True)
                return self.lineEditOtherSeparator.setText(sep)

        self.comboBoxSeparator.setCurrentIndex(self.comboBoxSeparator.findText(sep))

    def set_encoding(self, en):
        try:
            ENCODINGS[en]
        except KeyError as e:
            mapper = {v: k for k, v in ENCODINGS.items()}
            try:
                en = mapper[en]
            except KeyError as e:
                raise KeyError("Invalid Encoding {}".format(e))
        self.comboBoxEncoding.setCurrentIndex(self.comboBoxEncoding.findText(en))

    def execute(self):
        file_path = self.lineEditFilePath.text()
        file_megabytes = os.path.getsize(file_path) / 1000 / 1000
        remove_linebreaks = self.checkBoxScrubLinebreaks.isChecked()
        trim_spaces = self.checkBoxTrimSpaces.isChecked()
        has_headers = self.checkBoxHasHeaders.isChecked()
        parse_dates = self.checkBoxParseDates.isChecked()
        encoding = self.comboBoxEncoding.currentText()

        kwargs = dict()
        if self.radioBtnOtherSeparator.isChecked():
            sep = self.lineEditOtherSeparator.text()
            kwargs['sep'] = sep
        else:
            sep = self.comboBoxSeparator.currentText()
            kwargs['sep'] = SEPARATORS.get(sep, ',')

        if not has_headers:
            kwargs['header'] = 0
        kwargs['first_codec'] = ENCODINGS.get(encoding, 'utf8')

        if file_megabytes > 1000:
            kwargs['chunksize'] = 50 * 1000 * 1000
        df_reader = pandatools.superReadFile(file_path, **kwargs)

        if kwargs.get('chunksize', 0) > 0:
            # Process the dataframe in chunks
            df = pd.DataFrame()
            for chunk in df_reader:

                chunk = self.process_dataframe(chunk, trim_spaces=trim_spaces,
                                               remove_linebreaks=remove_linebreaks,
                                               parse_dates=parse_dates)
                df = df.append(chunk)
        else:

            df = self.process_dataframe(df_reader, trim_spaces=trim_spaces,
                                        remove_linebreaks=remove_linebreaks,
                                        parse_dates=parse_dates)

        dfm = DataFrameModel(dataFrame=df,filePath=file_path)
        self.df_manager.set_model(df_model=dfm, file_path=file_path)
        self.signalImported.emit(file_path)
zeex/core/ctrls/dataframe.py
class DataFrameModelManager(DFMM):
    """
    A central storage unit for managing
    DataFrameModels.
    """
    def __init__(self):
        DFMM.__init__(self)
        self._file_table_windows = {}

    @property
    def file_table_windows(self) -> dict:
        return self._file_table_windows

    def get_df_describe_model(self, filepath) -> DataFrameModel:
        from core.models.dataframe import DataFrameDescriptionModel
        describe_path = DataFrameDescriptionModel.get_describe_path(filepath)
        try:
            return self.models[describe_path]
        except KeyError:
            dfm = self.read_file(filepath)
            dfm2 = DataFrameDescriptionModel(source_model=dfm, filePath=describe_path)
            self.set_model(dfm2, describe_path)
            return self.get_model(describe_path)

    def get_fileview_window(self, file_path, **kwargs):
        from zeex.core.views.file import FileTableWindow
        model = self.read_file(file_path)
        try:
            return self._file_table_windows[file_path]
        except KeyError:
            self._file_table_windows[file_path] = FileTableWindow(model, self, **kwargs)
            return self._file_table_windows[file_path]
zeex/core/views/actions/export.py
class DataFrameModelExportDialog(QtGui.QDialog, Ui_ExportFileDialog):
    signalExported = QtCore.Signal(str, str) # original_path, new_path

    def __init__(self, df_manager: DataFrameModelManager, filename: str=None, allow_multi_source=True, **kwargs):
        self.df_manager = df_manager
        if allow_multi_source:
            self.df_manager.signalNewModelRead.connect(self.append_source_filename)

        if filename is None:

            if df_manager.last_path_updated is not None:
                self._default_path = df_manager.last_path_updated
            else:
                self._default_path = df_manager.last_path_read

        else:
            self._default_path = filename
            assert filename in df_manager.file_paths, "Invalid filename {} not in DataFrameModelManager paths: {}".format(
                                                       filename, df_manager.file_paths)

        QtGui.QDialog.__init__(self, **kwargs)
        self.setupUi(self)
        self.configure()

    def configure(self):
        self.btnBrowseDestination.clicked.connect(self.browse_destination)
        self.btnBrowseSource.clicked.connect(self.browse_source)
        self.btnOverwrite.clicked.connect(self.set_destination_path_from_source)
        configure_combo_box(self.comboBoxSource, self.df_manager.file_paths, self._default_path)
        configure_combo_box(self.comboBoxSeparator, list(SEPARATORS.keys()), 'Comma')
        configure_combo_box(self.comboBoxEncoding, list(ENCODINGS.keys()), 'ISO-8859-1')
        self.buttonBox.accepted.connect(self.export)

        if self._default_path is not None:
            self.lineEditDestination.setText(path_incremented(self._default_path, overwrite=False))

    def browse_destination(self):
        source = self.comboBoxSource.currentText()
        if os.path.isfile(source):
            kwargs = dict(dir=os.path.dirname(source))
        else:
            kwargs = dict()
        filename = QtGui.QFileDialog.getSaveFileName(self, **kwargs)[0]
        if os.path.isfile(filename):
            self.lineEditDestination.setText(filename)

    def set_separator(self, sep):
        try:
            SEPARATORS[sep]
        except KeyError:
            mapper = {v:k for k,v in SEPARATORS.items()}
            try:
                sep = mapper[sep]
            except KeyError:
                self.radioBtnOtherSeparator.setChecked(True)
                return self.lineEditOtherSeparator.setText(sep)

        self.comboBoxSeparator.setCurrentIndex(self.comboBoxSeparator.findText(sep))

    def set_encoding(self, en):
        try:
            ENCODINGS[en]
        except KeyError as e:
            mapper = {v:k for k,v in ENCODINGS.items()}
            try:
                en = mapper[en]
            except KeyError as e:
                raise KeyError("Invalid Encoding {}".format(e))
        self.comboBoxEncoding.setCurrentIndex(self.comboBoxEncoding.findText(en))

    def set_destination_path_from_source(self):
        self.lineEditDestination.setText(self.comboBoxSource.currentText())

    def browse_source(self):
        source = self.comboBoxSource.currentText()
        if os.path.isfile(source):
            kwargs = dict(dir=os.path.dirname(source))
        else:
            kwargs = dict()
        filename = QtGui.QFileDialog.getOpenFileName(self, **kwargs)[0]
        if filename not in self.df_manager.file_paths:
            self.df_manager.read_file(filename)
        idx = self.comboBoxSource.findText(filename)
        self.comboBoxSource.setCurrentIndex(idx)

    @QtCore.Slot(str)
    def append_source_filename(self, filename: str):
        self.comboBoxSource.addItem(filename)

    def export(self):
        source_path = self.comboBoxSource.currentText()
        file_path = self.lineEditDestination.text()
        encoding = self.comboBoxEncoding.currentText()

        if self.radioBtnOtherSeparator.isChecked():
            sep = self.lineEditOtherSeparator.text()
        else:
            sep = self.comboBoxSeparator.currentText()

        kwargs = dict(index=False)
        kwargs['encoding'] = ENCODINGS[encoding]
        kwargs['sep'] = SEPARATORS[sep]

        self.df_manager.save_file(source_path, save_as=file_path, keep_orig=True, **kwargs)
        self.signalExported.emit(source_path, file_path)
        self.hide()
        #box = get_ok_msg_box(self.parent, "Exported {}!".format(file_path))
        #box.show()
        logging.info("Exported {} to {}".format(source_path, file_path))
zeex/core/views/basic/directory.py
class DropBoxViewDialog(DirectoryViewDialog):
    def __init__(self, source_view, parent):
        DirectoryViewDialog.__init__(self, source_view=source_view, parent=parent)
        self.dropbox = QtDropBox()
        self.dialog_add_folder = DropBoxDirectoryPathCreateDialog(self.dropbox, treeview=self.treeView,
                                                                  base_dirname="/", parent=self)
        self.configure()

    def configure(self):
        self.setWindowIcon(QtGui.QIcon(':/standard_icons/add.png'))
        self.setWindowTitle("DropBox")
        self.dialog_add_folder.setWindowTitle("Add DropBox Folder")
        self.dialog_add_folder.signalDirectoryCreated.connect(self.refresh)
        super(DropBoxViewDialog, self).configure()

    def upload(self):
        indexes = self.source_view.selectedIndexes()
        paths = list(set([self.source_view.model().filePath(i) for i in indexes]))
        to = self.treeView.model().directory(self.treeView.selectedIndexes()[0])
        for p in paths:
            self.dropbox.upload_file(p, to)
            logging.info("DropBox - Uploaded {} to {}".format(p, to))

    def download(self):
        index = self.source_view.selectedIndexes()[0]
        to = self.source_view.model().filePath(index)
        from_idx = self.treeView.selectedIndexes()[0]
        from_path = self.treeView.model().filePath(from_idx)

        if os.path.isfile(to):
            to = os.path.dirname(to)
        to = os.path.join(to, os.path.basename(from_path))
        self.dropbox.download_file(from_path, to)
        logging.info("DropBox - Downloaded {} to {}".format(from_path, to))

    def refresh(self):
        self.treeView.setModel(self.dropbox.get_filesystem_model(update=True))

    def delete(self):
        from_idx = self.treeView.selectedIndexes()[0]
        from_path = self.treeView.model().filePath(from_idx)
        # Don't delete if there are children.
        child = from_idx.child(0, 0)
        if child.row() >= 0:
            # Way too easy to make a big mistake otherwise
            raise Exception("Can't delete folder that is not empty.")
        self.dropbox.con.files_delete(from_path)
        logging.info("DropBox - Deleted {}".format(from_path))
        self.refresh()

    def show(self, *args, **kwargs):
        self.refresh()
        super(DirectoryViewDialog, self).show(*args, **kwargs)

    def add_folder(self):
        self.dialog_add_folder.show()
zeex/core/views/basic/line_edit.py
class FilePathRenameDialog(LineEditDialog):
    """
    Simple LineEditDialog for renaming a filepath.
    """
    def __init__(self, original_path, default_rename=None, parent=None):
        LineEditDialog.__init__(self, parent=parent)
        self.original_path = original_path
        self.default_rename = (original_path if not default_rename else original_path)
        self.configure()

    def configure(self):
        orig_base = os.path.basename(self.original_path)
        new_base = os.path.basename(self.default_rename)
        self.setWindowTitle("Rename {}".format(orig_base))
        self.label.setText("New Name:")
        self.lineEdit.setText(new_base)

    def execute(self):
        new_name = self.lineEdit.text()
        new_path = os.path.join(os.path.dirname(self.original_path), new_name)
        if os.path.exists(new_path):
            new_path = path_incremented(new_path, overwrite=False)
        os.rename(self.original_path, new_path)
zeex/core/views/actions/merge_purge.py
class MergePurgeDialog(QtGui.QDialog, Ui_MergePurgeDialog):
    """
    This dialog allows a user to do large updates on a given source DataFrameModel.
        - Merging other file(s) with the source based on common keys/fields
        - Purging records from the source using other file(s) based on common keys/fields
        - Sorting the DataFrame by multiple columns/ascending/descending
        - Deduplicating the DataFrame based on common keys/fields
    Settings can exported to a config.ini file and re-imported at a later time.

    """
    signalMergeFileOpened = QtCore.Signal(str) # file path
    signalSFileOpened = QtCore.Signal(str) # file path
    signalSourcePathSet = QtCore.Signal(str) #file path
    signalExecuted = QtCore.Signal(str, str, str) # source_path, dest_path, report_path

    def __init__(self, df_manager: DataFrameModelManager, parent=None, source_model=None):
        """
        :param df_manager: (DataFrameModelManager)
            This will be used to handle reading/updating of DataFrameModels used
            in the operation.
        :param parent: (QMainWindow)

        :param source_model: (DataFrameModel)
            An optional source DataFrameModel
        """
        self.df_manager = df_manager
        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.source_model = source_model
        self._merge_view_model = FileViewModel()
        self._suppress_view_model = FileViewModel()
        self._purge_files = {}
        self._merge_files = {}
        self._field_map_grids = {}
        self._field_map_data = {}
        self.sortAscHandler = None
        self.sortOnHandler = None
        self.dedupeOnHandler = None
        self.uniqueFieldsHandler = None
        self.gatherFieldsHandler = None
        self.configure()
        if self.source_model is not None:
            self.set_source_model(source_model, configure=True)

    def configure(self, source_path=None, dest_path=None):
        """
        Connects main buttons and actions.
        :param source_path: (str, default None)
            If this is None there must be a valid path already in the sourcePathLineEdit or an AssertionError raises.

        :param dest_path: (str, default None)
            Optional custom destination path to be added to the destPathLineEdit.
        :return: None
        """
        if source_path is None:
            source_path = self.sourcePathLineEdit.text()
        if os.path.isfile(source_path):
            self.set_line_edit_paths(source_path, dest_path=dest_path)
        if self.sortAscHandler is None:
            self.set_handler_sort_asc()
        source_func = partial(self.open_file, model_signal=self.signalSourcePathSet)

        self.signalSourcePathSet.connect(self.set_source_model_from_browse)
        self.btnBrowseSourcePath.clicked.connect(source_func)
        self.btnBrowseDestPath.clicked.connect(self.set_dest_path_from_browse)
        self.signalMergeFileOpened.connect(self.add_merge_file)
        merge_file_func = partial(self.open_file, model_signal=self.signalMergeFileOpened)
        self.btnAddMergeFile.clicked.connect(merge_file_func)
        self.btnBrowseMergeFile.clicked.connect(merge_file_func)
        self.btnDeleteMergeFile.clicked.connect(partial(self.remove_file, self.mergeFileTable))
        self.btnEditMergeFile.clicked.connect(partial(self.open_edit_file_window, self.mergeFileTable, self._merge_files))
        self.mergeFileTable.setModel(self._merge_view_model)

        self.signalSFileOpened.connect(self.add_purge_file)
        sfile_func = partial(self.open_file, model_signal=self.signalSFileOpened)
        self.btnEditSFile.clicked.connect(partial(self.open_edit_file_window, self.sFileTable, self._purge_files))
        self.btnDeleteSFile.clicked.connect(partial(self.remove_file, self.sFileTable))
        self.btnAddSFile.clicked.connect(sfile_func)
        self.btnBrowseSFile.clicked.connect(sfile_func)
        self.sFileTable.setModel(self._suppress_view_model)
        self.btnMapSFields.clicked.connect(partial(self.open_field_map, self.sFileTable, self._purge_files))
        self.btnMapMergeFields.clicked.connect(partial(self.open_field_map, self.mergeFileTable, self._merge_files))
        self.btnExecute.clicked.connect(self.execute)
        self.btnExportTemplate.clicked.connect(self.export_settings)
        self.btnImportTemplate.clicked.connect(self.import_settings)
        self.btnReset.clicked.connect(self.reset)

    def set_source_model_from_browse(self, filepath):
        self.set_line_edit_paths(filepath, dest_path=False)
        self.set_source_model(configure=True)

    def set_dest_path_from_browse(self, filepath=None):
        if filepath is None:
            try:
                dirname = os.path.dirname(self.df_manager.last_path_read)
            except:
                dirname = ''
            filepath = QtGui.QFileDialog.getOpenFileName(self, dir=dirname)[0]
        self.destPathLineEdit.setText(filepath)

    def set_source_model(self, model=None, configure=True):
        """
        Sets the source DataFrameModel for the Dialog.

        :param model: (DataFrameModel)
            The DataFrameModel to be set.
        :param configure:
            True re-configures file path line edits and the listviews.
        :return:
        """
        if not hasattr(model, 'dataFrame'):
            if model is None:
                model = self.sourcePathLineEdit.text()
            if isinstance(model, str) and os.path.exists(model):
                model = self.df_manager.read_file(model)
            else:
                raise Exception("model parameter must be a filepath or a qtpandas.models.DataFrameModel")

        if self.source_model is not None:
            models_different = model.filePath != self.source_model.filePath
            if models_different:
                try:
                    self.source_model.dataFrameChanged.disconnect(self.sync)
                except RuntimeError:
                    pass
        else:
            models_different = True

        if models_different:
            self.source_model = model
            self.source_model.dataFrameChanged.connect(self.sync)

        if configure:
            self.sync()

    def sync(self):

        df = self.source_model.dataFrame()
        cols = df.columns.tolist()

        if self.dedupeOnHandler is None or self.uniqueFieldsHandler is None:
            self.set_push_grid_handlers()
        else:
            self.dedupeOnHandler.set_model_from_list(cols)
            self.gatherFieldsHandler.set_model_from_list(cols)
            self.sortOnHandler.set_model_from_list(cols)
            self.uniqueFieldsHandler.set_model_from_list(cols)

        self.set_primary_key_combo_box()
        self.set_line_edit_paths(source_path=self.source_model.filePath)

    def set_line_edit_paths(self, source_path=None, dest_path=None):
        """
        Sets the source/destination line edits in the Dialog.
        :param source_path: (str, default None)
            An optional valid filepath for the source DataFrameModel.
            If None, :param dest_path cannot be None.
        :param dest_path: (str, default None)
            An optional destination path. One will be created automatically
            if None is given.
            False will prevent the destination path from being set at all.
        :return: None
        """
        assert any([dest_path, source_path]), "source_path or dest_path must be set."

        if dest_path is None:
            dirname = os.path.dirname(source_path)
            base, ext = os.path.splitext(os.path.basename(source_path))
            dest_path = os.path.join(dirname, base + "_merged" + ext)

        if source_path:
            self.sourcePathLineEdit.setText(source_path)

        if dest_path:
            self.destPathLineEdit.setText(dest_path)

    def set_push_grid_handlers(self, column_model=None, sorton_model=None, sortasc_model=None,
                               dedupe_model=None, gather_model=None, unique_model=None):
        """
        Sets all default push grid handlers for the dialog.

        :param column_model: (QStandardItemModel, default None)
        :param sorton_model:  ((QStandardItemModel,list) default None)
        :param sortasc_model: ((QStandardItemModel,list) default None)
        :param dedupe_model: ((QStandardItemModel,list) default None)
        :return:
        """

        if column_model is None:
            column_model = self.get_source_columns_model()

        self.set_handler_sort_on(column_model=None, default_model=sorton_model)
        self.set_handler_sort_asc(default_model=sortasc_model)
        self.set_handler_dedupe_on(column_model=None, default_model=dedupe_model)
        self.set_handler_gather_fields(column_model=None, default_model=gather_model)
        self.set_handler_unique_fields(column_model=None, default_model=unique_model)

    def set_handler_sort_on(self, column_model=None, default_model=None):
        if column_model is None:
            column_model = self.get_source_columns_model()
        self.sortOnHandler = PushGridHandler(left_model=column_model, left_view=self.sortOnLeftView,
                                             left_button=self.sortOnLeftButton,
                                             left_delete=True, right_model=default_model,
                                             right_view=self.sortOnRightView,
                                             right_button=self.sortOnRightButton)

    def set_handler_sort_asc(self, default_model=None, overwrite=False):
        if self.sortAscHandler is None or default_model is not None or overwrite:
            sort_asc = QtGui.QStandardItemModel()
            sort_asc.appendRow(QtGui.QStandardItem('True'))
            sort_asc.appendRow(QtGui.QStandardItem('False'))
            self.sortAscHandler = PushGridHandler(left_model=sort_asc, left_view=self.sortAscLeftView,
                                                  left_button=self.sortAscLeftButton,
                                                  left_delete=False, right_model=default_model,
                                                  right_view=self.sortAscRightView,
                                                  right_button=self.sortAscRightButton)

    def set_handler_dedupe_on(self, column_model=None, default_model=None):
        if column_model is None:
            column_model = self.get_source_columns_model()
        self.dedupeOnHandler = PushGridHandler(left_model=column_model, left_view=self.dedupeOnLeftView,
                                               left_button=self.dedupeOnLeftButton,
                                               left_delete=True, right_model=default_model,
                                               right_view=self.dedupeOnRightView,
                                               right_button=self.dedupeOnRightButton)

    def set_handler_gather_fields(self, column_model=None, default_model=None):
        if column_model is None:
            column_model = self.get_source_columns_model()
        self.gatherFieldsHandler = PushGridHandler(left_model=column_model,
                                                   left_view=self.gatherFieldsListViewLeft,
                                                   left_button=self.gatherFieldsButtonLeft,
                                                   left_delete=True, right_model=default_model,
                                                   right_view=self.gatherFieldsListViewRight,
                                                   right_button=self.gatherFieldsButtonRight)

    def set_handler_unique_fields(self, column_model=None, default_model=None):
        if column_model is None:
            column_model = self.get_source_columns_model()
        self.uniqueFieldsHandler = PushGridHandler(left_model=column_model,
                                                   left_view=self.uniqueFieldsListViewLeft,
                                                   left_button=self.uniqueFieldsPushButtonLeft,
                                                   left_delete=True, right_model=default_model,
                                                   right_view=self.uniqueFieldsListViewRight,
                                                   right_button=self.uniqueFieldsPushButtonRight)

    def get_source_columns_model(self, raise_on_error=True) -> QtGui.QStandardItemModel:
        """
        Quick way to get a QStandardItemModel form the DataFrameModel's columns.
        :param raise_on_error: (bool, default True)
            Raises an error if the source_model has not yet been set.
        :return: (QtGui.QStandardItemModel)
        """
        if self.source_model is None:
            if raise_on_error:
                raise Exception("Cannot get source_columns as source_model is None!")
            else:
                columns = []
        else:
            columns = self.source_model.dataFrame().columns.tolist()

        return create_standard_item_model(columns)

    def open_file(self, file_names: list=None, model_signal=None, allow_multi=True):
        """
        Opens a Merge or Purge file (or really any file) and calls the
        given model signal after registering the DataFrameModel with the DataFrameModelManager.
        :param file_names: (list, default None)
            An optional list of filenames to open.
            The user must select filenames otherwise.
        :param model_signal: (QtCore.Signal)
            A signal to be called after successfully reading the DataFrameModel.
        :param allow_multi: (bool, default True)
            True allows multiple files to be read (and the signal called each time).
            False allows only the first file to be read.
        :return: None
            You can call MergePurgeDialog.df_manager.get_frame(filename) to
            retrieve a DataFrameModel.
        """
        if file_names is None:
            dirname = os.path.dirname(self.sourcePathLineEdit.text())
            file_names = QtGui.QFileDialog.getOpenFileNames(parent=self,
                                                            dir=dirname)[0]

        if isinstance(file_names, str):
            file_names = list(file_names)

        assert not isinstance(file_names, str) and hasattr(file_names, "__iter__"), "file_names is not list-like!"

        if allow_multi is False:
            file_names = list(file_names[0])

        for f in file_names:
            try:
                if not isinstance(f, str) and hasattr(f, '__iter__'):
                    f = f[0]
                if os.path.exists(f):
                    self.df_manager.read_file(f)
                    if model_signal is not None:
                        model_signal.emit(f)
                        logging.info("Emitted signal: {}".format(f))
            except Exception as e:
                logging.error(e)

    @QtCore.Slot(str)
    def add_merge_file(self, file_path):
        """
        Adds a merge file to the merge view and
        also updates the internal dictionary storing the filepath/model.
        :param file_path: (str)
            The file path to add.
        :return: None
        """
        model = self.df_manager.get_model(file_path)
        model.enableEditing(True)
        self._merge_files.update({file_path:model})
        self._merge_view_model.append_df_model(model)
        self.mergeFileTable.setColumnWidth(0, 500)
        self._merge_view_model.setHorizontalHeaderLabels(['filepath', 'count'])

    @QtCore.Slot(str)
    def add_purge_file(self, file_path):
        """
        Adds a purge file to the purge view and
        also updates the internal dictionary storing the filepath/model.
        :param file_path: (str)
            The file path to add.
        :return: None
        """
        model = self.df_manager.get_model(file_path)
        model.enableEditing(True)
        self._purge_files.update({file_path:model})
        self._suppress_view_model.append_df_model(model)
        self.sFileTable.setColumnWidth(0, 500)
        self._suppress_view_model.setHorizontalHeaderLabels(['filepath', 'count'])

    def remove_file(self, view, indexes=None):
        """
        Removes selected file(s) from the given view.
        :param view: (QListView)
            The view to drop the selected indexes on.
        :param indexes: (list, default None)
            A list of given indexes to drop.
            Otherwise relies on selected indexes in the view.
        :return: None
        """
        if indexes is None:
            indexes = [x.row() for x in view.selectedIndexes()]
        model = view.model()
        for idx in indexes:
            model.takeRow(idx)

    def open_field_map(self, view, models):
        """
        Connects a MapGridDialog to help the  user map field names that
        are different between the source DataFrameModel and the
        selected merge or suppression DataFrameModel.

        :param view: (QtGui.QTableView)
            The view that has a selected filepath
        :param models: (dict)
            The dictionary of {file_path:DataFrameModel} where
            dataframe columns can be gathered from.
        :return: None

        """
        idx = view.selectedIndexes()[0]
        view_model = view.model()
        view_item = view_model.item(idx.row())
        view_item_text = view_item.text()

        try:
            self._field_map_grids[view_item_text].show()
        except KeyError:
            dfmodel = models[view_item_text]
            colmodel = dfmodel._dataFrame.columns.tolist()

            if self.source_model is None:
                self.set_source_model()

            source_colmodel = self.source_model._dataFrame.columns.tolist()

            fmap = MapGridDialog(parent=self)
            fmap.load_combo_box(source_colmodel, left=True)
            fmap.load_combo_box(colmodel, left=False)
            fmap.setWindowTitle("Map Fields")
            fmap.labelLeft.setText(os.path.basename(self.source_model.filePath))
            fmap.labelRight.setText(os.path.basename(dfmodel.filePath))
            fmap.signalNewMapping.connect(lambda x: self._field_map_data.update({dfmodel.filePath: x}))

            self._field_map_grids[view_item_text] = fmap
            self._field_map_grids[view_item_text].show()

    def get_map_grid(self, file_path):
        """
        Accessor to the MergePurgeDialog._field_map_grids dictionary.
        Contains map grid dialogs.
        :param file_path: (str)
            The filepath related to the desired MapGridDialog.
        :return: (MapGridDialog, None)
        """
        return self._field_map_grids.get(file_path, None)

    def open_edit_file_window(self, view, models):
        """
        Connects a DataFrameModel selected in the view
        to a FileTableWindow where the model can be edited.

        :param view: (QtGui.QTableView)
            The view that has a selected filepath
        :param models: (dict)
            The dictionary of {file_path:DataFrameModel}
            to supply the FileTableWindow
        :return: None
        """
        try:
            idx = view.selectedIndexes()[0]
        except IndexError:
            raise IndexError("No file selected to open.")
        vmodel = view.model()
        vitem = vmodel.item(idx.row())
        model = models.get(vitem.text())

        fp = model.filePath
        wdw = self.df_manager.get_fileview_window(fp)
        # Prevent wierdos from doing an endless loop of MergePurge windows.
        # That would be pretty funny, though..
        wdw.actionMergePurge.setVisible(False)

        wdw.show()



    def execute(self):
        """
        Executes the merge_purge based upon the given settings.
        :return: None
        """
        if self.source_model is None:
            self.set_source_model()

        suppressed_results = {}
        merged_results = {}
        source_path = self.sourcePathLineEdit.text()
        dest_path = self.destPathLineEdit.text()
        source_df = self.source_model.dataFrame().copy()
        source_df.loc[:, 'ORIG_IDXER'] = source_df.index
        source_size = source_df.index.size
        index_label = self.primaryKeyComboBox.currentText()

        sort_on = self.sortOnHandler.get_model_list(left=False)
        ascending = self.sortAscHandler.get_model_list(left=False)
        dedupe_on = self.dedupeOnHandler.get_model_list(left=False)
        gather_fields = self.gatherFieldsHandler.get_model_list(left=False)
        overwrite_existing = self.gatherFieldsOverWriteCheckBox.isChecked()

        # Make sure ascending/sort_on lists are equal.
        while len(sort_on) < len(ascending):
            ascending.append(False)

        while len(sort_on) > len(ascending):
            ascending.pop()

        # Get all merge models and merge.
        # Absorb all rows and columns
        for file_path, merge_model in self._merge_files.items():
            pre_size = source_df.index.size
            other_df = merge_model.dataFrame()
            if gather_fields:
                assert index_label in other_df.columns, "DataFrameModel for {} missing column {}".format(
                                                         merge_model.filePath, index_label)
                source_df = gather_frame_fields(source_df, other_df, index_label=index_label,
                                                fields=gather_fields, copy_frames=True,
                                                append_missing=True, overwrite=overwrite_existing)
            else:
                source_df = pd.concat([source_df, other_df])
            merged_results.update({merge_model.filePath: source_df.index.size - pre_size})
        # Get all suppression models and suppress.
        for file_path, suppress_model in self._purge_files.items():
            map_dict = self._field_map_data.get(file_path, {})
            sframe = suppress_model.dataFrame().copy()
            sframe.drop(['ORIG_IDXER'], axis=1, inplace=True, errors='ignore')

            if map_dict:
                # A mapping exists - rename the data and get the key_cols
                key_cols = list(map_dict.values())
                sframe.rename(columns=map_dict, inplace=True)
            else:
                # No mapping exists - Try to use the dedupe_on cols as key_cols
                key_cols = dedupe_on.copy()
                missing = [x for x in key_cols if x not in sframe.columns]
                if missing:
                    raise KeyError("Suppression file {} must have a field mapping or \
                                    have the dedupe column labels, it has neither!.".format(
                                    suppress_model.filePath))

            sframe = sframe.loc[:, key_cols].drop_duplicates(key_cols)
            badframe = pd.merge(source_df, sframe, how='inner', left_on=key_cols, right_on=key_cols)
            source_df = source_df.loc[~source_df.index.isin(badframe.loc[:, 'ORIG_IDXER'].tolist()), :]
            suppressed_results.update({suppress_model.filePath: badframe.index.size})

        # Sort the data
        if sort_on and ascending:
            source_df.sort_values(sort_on, ascending=ascending, inplace=True)

        # Deduplicate the data.
        if dedupe_on:
            pre_size = source_df.index.size
            source_df.drop_duplicates(dedupe_on, inplace=True)
            dedupe_lost = pre_size - source_df.index.size
        else:
            dedupe_lost = 0

        # Export the data - done!
        source_df.drop(['ORIG_IDXER'], axis=1, inplace=True, errors='ignore')
        source_df.to_csv(dest_path, index=False)
        logging.info("Exported: {}".format(dest_path))

        merge_string = "\n".join("Gained {} merging {}".format(v, k) for k, v in merged_results.items())
        suppress_string = "\n".join("Lost {} suppressing {}".format(v, k) for k,v in suppressed_results.items())
        report = """
        Merge Purge Report
        ==================
        Original Size: {}
        Final Size: {}
        Source Path: {}
        Output Path: {}


        Merge:
        ==================
        {}


        Purge:
        ==================
        {}


        Sort:
        ==================
            SORT BY: {}
            SORT ASCENDING: {}


        Dedupe:
        ==================
            DEDUPE ON: {}
            RECORDS LOST: {}



        """.format(source_size, source_df.index.size, source_path,
                   dest_path, merge_string, suppress_string,
                   sort_on, ascending, dedupe_on, dedupe_lost)

        report_path = os.path.splitext(dest_path)[0] + "_report.txt"
        with open(report_path, "w") as fh:
            fh.write(report)

        self.signalExecuted.emit(source_path, dest_path, report_path)

    def get_settings(self, dc:DictConfig = None, section="MERGE_PURGE") -> DictConfig:
        """
        Gathers the settings out of the Dialog and
        returns a DictConfig object with updated settings.

        :param dc (DictConfig, default None)
            An optional DictConfig object, one is created if none is given.
        :param section (str, default 'MERGE_PURGE')
            An optional section name to apply settings to.
            A pre-existing section with this name would be overwritten.
        :return: (DictConfig)
            An updated DictConfig object.
        """
        if dc is None:
            dc = DictConfig()
        if dc.has_section(section):
            dc.remove_section(section)
        dc.add_section(section)

        dc.set_safe(section, 'source_path',           self.sourcePathLineEdit.text())
        dc.set_safe(section, 'dest_path',             self.destPathLineEdit.text())
        dc.set_safe(section, 'primary_key',           self.primaryKeyComboBox.currentText())
        dc.set_safe(section, 'dedupe_on',             self.dedupeOnHandler.get_model_list(left=False))
        dc.set_safe(section, 'gather_fields',         self.gatherFieldsHandler.get_model_list(left=False))
        dc.set_safe(section, 'gather_fields_overwrite', self.gatherFieldsOverWriteCheckBox.isChecked())
        dc.set_safe(section, 'sort_on',               self.sortOnHandler.get_model_list(left=False))
        dc.set_safe(section, 'sort_ascending',        self.sortAscHandler.get_model_list(left=False))
        dc.set_safe(section, 'unique_fields',         self.uniqueFieldsHandler.get_model_list(left=False))
        dc.set_safe(section, 'field_map_data',        self._field_map_data)
        dc.set_safe(section, 'merge_files',           list(self._merge_files.keys()))
        dc.set_safe(section, 'purge_files',           list(self._purge_files.keys()))
        return dc

    def set_settings(self, dc:DictConfig, section="MERGE_PURGE"):
        """
        Applies settings from a DictConfig object to the Dialog.

        :param dc (DictConfig, default None)
            The DictConfig object that contains the settings to be applied.
        :param section (str, default 'MERGE_PURGE')
            The section name to read settings from.
        :return:
        """
        source_path = dc.get(section, 'source_path', fallback=self.sourcePathLineEdit.text())
        current_path = self.sourcePathLineEdit.text()
        if source_path != current_path:
            dfm = self.df_manager.read_file(source_path)
            dest = dc.get(section, 'dest_path', fallback=None)
            self.set_source_model(dfm, configure=False)
            self.set_line_edit_paths(source_path, dest_path=dest)
            self.primaryKeyComboBox.clear()
            self.set_primary_key_combo_box()

        key_id = self.primaryKeyComboBox.findText(dc.get(section, 'primary_key',
                                                         fallback=self.primaryKeyComboBox.currentText()))

        dedupe_on     = dc.get_safe(section, 'dedupe_on', fallback=None)
        sort_on       = dc.get_safe(section, 'sort_on', fallback=None)
        gather_fields = dc.get_safe(section, 'gather_fields', fallback=None)
        unique_fields = dc.get_safe(section, 'unique_fields', fallback=None)
        gather_fields_overwrite = dc.getboolean(section, 'gather_fields_overwrite', fallback=False)
        sort_ascending = dc.get_safe(section, 'sort_ascending', fallback=None)
        merge_files    = dc.get_safe(section, 'merge_files', fallback=[])
        purge_files    = dc.get_safe(section, 'purge_files', fallback=[])
        field_map_data = dc.get_safe(section, 'field_map_data', fallback={})

        self.primaryKeyComboBox.setCurrentIndex(key_id)
        self.set_push_grid_handlers(column_model=None, sorton_model=sort_on, sortasc_model=sort_ascending,
                               dedupe_model=dedupe_on, gather_model=gather_fields, unique_model=unique_fields)
        self.gatherFieldsOverWriteCheckBox.setChecked(gather_fields_overwrite)
        self._field_map_data.update(field_map_data)
        self.open_file(file_names=merge_files, model_signal=self.signalMergeFileOpened)
        self.open_file(file_names=purge_files, model_signal=self.signalSFileOpened)

    def reset(self):
        """
        Resets ListViews/CheckBoxes.
        The source/dest line edits are left alone
        The suppression/merge files are also left alone.
        :return: None
        """
        self.set_push_grid_handlers()
        self.set_handler_sort_asc(overwrite=True)
        self.set_primary_key_combo_box(reset=True)
        self.gatherFieldsOverWriteCheckBox.setChecked(False)

    def set_primary_key_combo_box(self, default=None, reset=False):
        """
        Sets the primary key combo box.
        :param default: (str, default None)
            An optional default field name to select.
        :return:
        """
        if default is None and reset is False:
            current_model = self.primaryKeyComboBox.model()
            if current_model:
                default = self.primaryKeyComboBox.currentText()

        combo_model = create_standard_item_model([''] + self.source_model.dataFrame().columns.tolist(),
                                                 editable=False, checkable=True)
        self.primaryKeyComboBox.setModel(combo_model)
        if default is not None:
            self.primaryKeyComboBox.setCurrentIndex(self.primaryKeyComboBox.findText(default))

    def import_settings(self, from_path=None):
        """
        Imports settings to the Dialog from a file.
        :param from_path: (str, default None)
            None makes the user enter a file path.
        :return:
        """
        if from_path is None:
            try:
                dirname = os.path.dirname(self.sourcePathLineEdit.text())
            except:
                dirname = ''
            from_path = QtGui.QFileDialog.getOpenFileName(self, dir=dirname)[0]
        config = SettingsINI(filename=from_path)
        self.set_settings(config)

    def export_settings(self, to=None):
        """
        Exports settings from the Dialog to a file.
        :param to: (str, default None)
            None makes the user enter a file path.
        :return: None
        """
        if to is None:
            try:
                dirname = os.path.dirname(self.sourcePathLineEdit.text())
            except:
                dirname = ''
            to = QtGui.QFileDialog.getSaveFileName(self, dir=dirname)[0]
        config = self.get_settings()
        config.save_as(to, set_self=True)
zeex/core/views/basic/line_edit.py
class DirectoryPathCreateDialog(LineEditDialog):
    signalDirectoryCreated = QtCore.Signal(str)

    def __init__(self, treeview:QtGui.QTreeView=None, base_dirname=None, parent=None):
        LineEditDialog.__init__(self, parent=parent)
        self.treeview = treeview
        self.base_dirname = base_dirname
        self.configure()

    def configure(self):
        self.setWindowTitle("Add Folder")
        self.label.setText("Folder Name:")

    def set_treeview(self, treeview):
        self.treeview = treeview

    def set_base_dirname(self, dirname):
        self.base_dirname = dirname

    def execute(self):
        b = self.base_dirname
        if self.treeview is not None:
            selected = self.treeview.selectedIndexes()
            if selected:
                idx = selected[0]
                b = self.treeview.model().filePath(idx)
        assert b is not None, "No base directory or selected treeview path found."
        if os.path.isfile(b):
            b = os.path.dirname(b)
        dirname = os.path.join(b, self.lineEdit.text())
        os.mkdir(dirname)
        self.signalDirectoryCreated.emit(dirname)
zeex/core/views/project/settings.py
class ProjectSettingsDialog(SettingsDialog):
    """
    A SettingsDialog with a few items hidden or set to read-only.
    This covers the minor differences between general settings and project settings.
    """
    def __init__(self, settings_ini, project_directory=None, log_directory=None, **kwargs):
        if log_directory is None:
            log_directory = settings_ini.get('GENERAL', 'LOG_DIRECTORY')
        if project_directory is None:
            project_directory = settings_ini.get('GENERAL', 'ROOT_DIRECTORY')

        SettingsDialog.__init__(self, settings=settings_ini, **kwargs)
        self.project_directory = project_directory
        self.log_directory = log_directory

        # Adjust the box to remove irrelevant items.
        self.cloudProviderComboBox.hide()
        self.cloudProviderLabel.hide()
        self.btnLogDirectory.hide()
        self.btnRootDirectory.hide()
        self.themeComboBox.hide()
        self.themeLabel.hide()

        # Override the log/root directory options
        self.logDirectoryLineEdit.setText(self.log_directory)
        self.logDirectoryLineEdit.setReadOnly(True)
        self.rootDirectoryLineEdit.setText(self.project_directory)
        self.rootDirectoryLineEdit.setReadOnly(True)
        self.btnSetDefault.setVisible(False)
        self.sync_project_config_path()

    def sync_project_config_path(self):
        orig_path = self.settings_ini.filename
        cur_path = self.settings_ini.default_path
        cur_dir = os.path.dirname(cur_path).replace("\\", "/")
        proj_dir = self.project_directory.replace("\\", "/")
        if cur_dir != proj_dir:
            cur_path = os.path.join(proj_dir, os.path.basename(cur_path))
            self.settings_ini._default_path = cur_path
            self.settings_ini._filename = cur_path
            self.settings_ini._default_backup_path = cur_path
            self.settings_ini.save()

        if not os.path.exists(cur_path):
            shutil.copy2(orig_path, cur_path)

        self.save_settings()


Question: import os
import logging
from zeex.core.compat import QtGui, QtCore
from zeex.core.ctrls.dataframe import DataFrameModelManager
from zeex.core.models.filetree import FileTreeModel
from zeex.core.views.actions.export import DataFrameModelExportDialog
from zeex.core.views.actions.merge_purge import MergePurgeDialog
from zeex.core.views.basic.directory import DropBoxViewDialog
from zeex.core.views.basic.line_edit import DirectoryPathCreateDialog
from zeex.core.views.basic.line_edit import FilePathRenameDialog
from zeex.core.views.actions.import_file import DataFrameModelImportDialog
from zeex.core.views.project.settings import ProjectSettingsDialog
"""
MIT License

Copyright (c) 2016 Zeke Barge

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""


class ProjectController(object):
    def __init__(self, directory, settings_ini, default_dirs=True, tree_view=None, main_control=None,**kwargs):
        self.parent = kwargs.get('parent', None)
        self.main_control = main_control
        if default_dirs is True:
            settings_ini.set_safe('GENERAL', 'ROOT_DIRECTORY', directory)
            settings_ini.set_safe('GENERAL', 'LOG_DIRECTORY', os.path.join(directory, 'logs'))
        self._directory = directory
        self._df_manager = DataFrameModelManager()

Answer:         self._file_tree_model = FileTreeModel(root_dir=directory, parent=self.parent)