diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9aadf5e..4f5adc7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -13,12 +13,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.11, 3.12, 3.13] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install tools diff --git a/.gitignore b/.gitignore index 4d2b563..b8eb205 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dist/ __pycache__ *.egg-info .vscode -test_files \ No newline at end of file +test_files +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 10fa288..ffd3ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ This document records all notable changes to [Converter CSV by LimberDuck][1]. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.1] - 2025-02-05 + +### Changed + +- code formatted with [black](https://black.readthedocs.io) +- requirements update + - from: + - chardet>=4.0.0 + - imageio>=2.9.0 + - PyQt5>=5.15.4 + - XlsxWriter>=3.0.1 + - to: + - chardet>=5.2.0 + - imageio>=2.37.0 + - PyQt5>=5.15.11 + - XlsxWriter>=3.2.2 +- tests for python + - added: 3.10, 3.11, 3.12, 3.13 + - removed: 3.6, 3.7, 3.8, 3.9, 3.10 + ## [0.3.0] - 2021-08-30 ### Added @@ -43,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial public release +[0.3.1]: https://github.com/LimberDuck/converter-csv/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/LimberDuck/converter-csv/compare/v0.2.3...v0.3.0 [0.2.3]: https://github.com/LimberDuck/converter-csv/compare/v0.2.2...v0.2.3 [0.2.2]: https://github.com/LimberDuck/converter-csv/compare/v0.2.1...v0.2.2 diff --git a/README.md b/README.md index 6160e14..348b613 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ operating system no matter if it is Windows, MacOS or Linux. It's free and open source tool. The reason this tool was created is to speed-up your tasks. -[![PyPI - Downloads](https://img.shields.io/pypi/dm/converter-csv?logo=PyPI)](https://pypistats.org/packages/converter-csv) [![License](https://img.shields.io/github/license/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv/blob/main/LICENSE) [![Repo size](https://img.shields.io/github/repo-size/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv) [![Code size](https://img.shields.io/github/languages/code-size/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv) [![Supported platform](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-lightgrey.svg)](https://github.com/LimberDuck/converter-csv) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/converter-csv?logo=PyPI)](https://pypistats.org/packages/converter-csv) +[![License](https://img.shields.io/github/license/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv/blob/main/LICENSE) +[![Repo size](https://img.shields.io/github/repo-size/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv) +[![Code size](https://img.shields.io/github/languages/code-size/LimberDuck/converter-csv.svg)](https://github.com/LimberDuck/converter-csv) +[![Supported platform](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-lightgrey.svg)](https://github.com/LimberDuck/converter-csv) ![](https://user-images.githubusercontent.com/9287709/57588063-d4b2f280-750e-11e9-9ba8-e2d301d38cbc.png) @@ -60,7 +64,7 @@ your tasks. > > Run with `&` at the end to start the process in the background. -3. Make a shortcut with **Converter CSV** +3. Make a shortcut for **Converter CSV** **Windows:** @@ -189,13 +193,13 @@ sudo apt-get install --reinstall libxcb-xinerama0 pip install -r requirements.txt ``` -6. Run **Converter CSV** using below command +3. Run **Converter CSV** using below command ``` python -m converter_csv ``` -7. Upgrade setuptools using below command +4. Upgrade setuptools using below command ``` pip install --upgrade setuptools @@ -207,13 +211,13 @@ sudo apt-get install --reinstall libxcb-xinerama0 pip install PyInstaller ``` -8. Build your own executable file using below command +6. Build your own executable file using below command ``` pyinstaller --onefile --windowed --icon=./icons/LimberDuck-converter-csv.ico --name converter-csv converter_csv/__main__.py ``` -9. Go to `dist` catalog to find executable file `converter-csv`. +7. Go to `dist` catalog to find executable file `converter-csv`. @@ -276,8 +280,10 @@ GNU GPLv3: [LICENSE]. ### Authors -[Damian Krawczyk] created **Converter CSV** by LimberDuck. +[Damian Krawczyk] created **[Converter CSV]** by [LimberDuck]. [Damian Krawczyk]: https://damiankrawczyk.com +[Converter CSV]: https://limberduck.org/en/latest/tools/converter-csv +[LimberDuck]: https://limberduck.org [CHANGELOG]: https://github.com/LimberDuck/converter-csv/blob/master/CHANGELOG.md [LICENSE]: https://github.com/LimberDuck/converter-csv/blob/master/LICENSE diff --git a/converter_csv/__about__.py b/converter_csv/__about__.py index 9173c29..4c2c845 100644 --- a/converter_csv/__about__.py +++ b/converter_csv/__about__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -u""" +""" Converter CSV by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a GUI tool which lets you convert multiple large csv files to xlsx files. Copyright (C) 2018 Damian Krawczyk @@ -21,19 +21,30 @@ import datetime __all__ = [ - "__title__", "__icon__", "__summary__", "__uri__", "__version__", "__release_date__", "__author__", - "__email__", "__license_name__", "__license_link__", "__copyright__" + "__title__", + "__icon__", + "__summary__", + "__uri__", + "__version__", + "__release_date__", + "__author__", + "__email__", + "__license_name__", + "__license_link__", + "__copyright__", ] __title__ = "Converter CSV by LimberDuck" __package_name__ = "converter-csv" __icon__ = "LimberDuck-converter-csv.ico" __summary__ = "Converter CSV by LimberDuck is a GUI tool to convert multiple large csv files to xlsx files." -__uri__ = "https://limberduck.org/en/latest/converter-csv" -__version__ = "0.3.0" -__release_date__ = "2021.8.30" -__author__ = u"Damian Krawczyk" +__uri__ = "https://limberduck.org" +__version__ = "0.3.1" +__release_date__ = "2025.2.05" +__author__ = "Damian Krawczyk" __email__ = "damian.krawczyk@limberduck.org" __license_name__ = "GNU GPLv3" __license_link__ = "https://www.gnu.org/licenses/gpl-3.0.en.html" -__copyright__ = u"\N{COPYRIGHT SIGN} 2018-{} by {}".format(datetime.datetime.now().year, __author__) +__copyright__ = "\N{COPYRIGHT SIGN} 2018-{} by {}".format( + datetime.datetime.now().year, __author__ +) diff --git a/converter_csv/__init__.py b/converter_csv/__init__.py index 2cba245..d57282d 100644 --- a/converter_csv/__init__.py +++ b/converter_csv/__init__.py @@ -1,9 +1,27 @@ from .__about__ import ( - __title__, __icon__, __summary__, __uri__, __version__, __release_date__, __author__, - __email__, __license_name__, __license_link__, __copyright__ + __title__, + __icon__, + __summary__, + __uri__, + __version__, + __release_date__, + __author__, + __email__, + __license_name__, + __license_link__, + __copyright__, ) __all__ = [ - "__title__", "__icon__", "__summary__", "__uri__", "__version__", "__release_date__", "__author__", - "__email__", "__license_name__", "__license_link__", "__copyright__" -] \ No newline at end of file + "__title__", + "__icon__", + "__summary__", + "__uri__", + "__version__", + "__release_date__", + "__author__", + "__email__", + "__license_name__", + "__license_link__", + "__copyright__", +] diff --git a/converter_csv/__main__.py b/converter_csv/__main__.py index 8f20048..e9bee03 100644 --- a/converter_csv/__main__.py +++ b/converter_csv/__main__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -u""" +""" Converter CSV by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a GUI tool which lets you convert multiple large csv files to xlsx files. Copyright (C) 2018 Damian Krawczyk @@ -27,6 +27,6 @@ def main(): app.main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/converter_csv/_version.py b/converter_csv/_version.py index f2b3589..260c070 100644 --- a/converter_csv/_version.py +++ b/converter_csv/_version.py @@ -1 +1 @@ -__version__ = "0.3.0" \ No newline at end of file +__version__ = "0.3.1" diff --git a/converter_csv/app.py b/converter_csv/app.py index d56cb8e..fc0e0fd 100644 --- a/converter_csv/app.py +++ b/converter_csv/app.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -u""" +""" Converter CSV by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a GUI tool which lets you convert multiple large csv files to xlsx files. Copyright (C) 2018 Damian Krawczyk @@ -36,26 +36,27 @@ from converter_csv.dialogs import about from converter_csv import __about__ -class MainWindow(QMainWindow, mainwindow.Ui_MainWindow): +class MainWindow(QMainWindow, mainwindow.Ui_MainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) self.__files_to_pars = [] - self.__delimiter = '' + self.__delimiter = "" self.__parsing_settings = {} - self.__target_directory = '' + self.__target_directory = "" self.__target_directory_changed = False self.__file_conversion_counter = 0 - self.__suffix = '' - self.__suffix_template = '' - + self.__suffix = "" + self.__suffix_template = "" - self.parsing_thread = ParsingThread(files_to_pars=self.__files_to_pars, - target_directory=self.__target_directory, - target_directory_changed=self.__target_directory_changed, - parsing_settings=self.__parsing_settings) + self.parsing_thread = ParsingThread( + files_to_pars=self.__files_to_pars, + target_directory=self.__target_directory, + target_directory_changed=self.__target_directory_changed, + parsing_settings=self.__parsing_settings, + ) self.actionOpen_file.triggered.connect(self.open_files) self.actionOpen_directory.triggered.connect(self.open_directory) @@ -63,11 +64,15 @@ def __init__(self, parent=None): self.actionStart_conversion.triggered.connect(self.parsing_thread_start) self.actionChange_separator.triggered.connect(self.change_delimiter) - self.actionChange_target_directory.triggered.connect(self.change_target_directory) + self.actionChange_target_directory.triggered.connect( + self.change_target_directory + ) self.actionOpen_target_directory.triggered.connect(self.open_target_directory) self.actionAbout.triggered.connect(self.open_dialog_about) - self.checkBox_suffix_timestamp.stateChanged.connect(self.suffix_timestamp_changed) + self.checkBox_suffix_timestamp.stateChanged.connect( + self.suffix_timestamp_changed + ) self.checkBox_suffix_custom.stateChanged.connect(self.suffix_custom_changed) self.pushButton_start.clicked.connect(self.parsing_thread_start) @@ -91,7 +96,7 @@ def __init__(self, parent=None): self.set_target_directory(source_file_path) self.get_target_directory_from_file() - self.set_delimiter(',') + self.set_delimiter(",") self.get_delimiter() self.progressBar.setRange(0, 10) @@ -100,73 +105,91 @@ def suffix_timestamp_changed(self): """ Function sets suffix appropriately if checkBox_suffix_timestamp has changed. """ - if self.checkBox_suffix_timestamp.isChecked() and not self.checkBox_suffix_custom.isChecked(): + if ( + self.checkBox_suffix_timestamp.isChecked() + and not self.checkBox_suffix_custom.isChecked() + ): time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y-%m-%d_%H%M%S') - self.change_suffix('_' + time_now_formatted) - self.__suffix_template = 'suffix_timestamp' - - elif not self.checkBox_suffix_timestamp.isChecked() and self.checkBox_suffix_custom.isChecked(): + time_now_formatted = time_now.strftime("%Y-%m-%d_%H%M%S") + self.change_suffix("_" + time_now_formatted) + self.__suffix_template = "suffix_timestamp" + + elif ( + not self.checkBox_suffix_timestamp.isChecked() + and self.checkBox_suffix_custom.isChecked() + ): suffix_custom_value = self.lineEdit_suffix_custom_value.text() if suffix_custom_value: - space = '_' - self.__suffix_template = 'suffix_custom' + space = "_" + self.__suffix_template = "suffix_custom" else: - space = '' - self.__suffix_template = 'suffix_custom_empty' + space = "" + self.__suffix_template = "suffix_custom_empty" self.change_suffix(space + suffix_custom_value) - elif self.checkBox_suffix_timestamp.isChecked() and self.checkBox_suffix_custom.isChecked(): + elif ( + self.checkBox_suffix_timestamp.isChecked() + and self.checkBox_suffix_custom.isChecked() + ): time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y-%m-%d_%H%M%S') + time_now_formatted = time_now.strftime("%Y-%m-%d_%H%M%S") suffix_custom_value = self.lineEdit_suffix_custom_value.text() if suffix_custom_value: - space = '_' - self.__suffix_template = 'suffix_custom_timestamp' + space = "_" + self.__suffix_template = "suffix_custom_timestamp" else: - space = '' - self.__suffix_template = 'suffix_custom_empty_timestamp' - self.change_suffix(space + suffix_custom_value + '_' + time_now_formatted) + space = "" + self.__suffix_template = "suffix_custom_empty_timestamp" + self.change_suffix(space + suffix_custom_value + "_" + time_now_formatted) else: - self.change_suffix('') - self.__suffix_template = 'empty' + self.change_suffix("") + self.__suffix_template = "empty" def suffix_custom_changed(self): """ Function sets suffix appropriately if checkBox_suffix_custom has changed. """ - if self.checkBox_suffix_custom.isChecked() and not self.checkBox_suffix_timestamp.isChecked(): + if ( + self.checkBox_suffix_custom.isChecked() + and not self.checkBox_suffix_timestamp.isChecked() + ): suffix_custom_value = self.lineEdit_suffix_custom_value.text() if suffix_custom_value: - space = '_' - self.__suffix_template = 'suffix_custom' + space = "_" + self.__suffix_template = "suffix_custom" else: - space = '' - self.__suffix_template = 'suffix_custom_empty' + space = "" + self.__suffix_template = "suffix_custom_empty" self.change_suffix(space + suffix_custom_value) - elif not self.checkBox_suffix_custom.isChecked() and self.checkBox_suffix_timestamp.isChecked(): + elif ( + not self.checkBox_suffix_custom.isChecked() + and self.checkBox_suffix_timestamp.isChecked() + ): time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y-%m-%d_%H%M%S') - self.change_suffix('_' + time_now_formatted) - self.__suffix_template = 'suffix_timestamp' - - elif self.checkBox_suffix_custom.isChecked() and self.checkBox_suffix_timestamp.isChecked(): + time_now_formatted = time_now.strftime("%Y-%m-%d_%H%M%S") + self.change_suffix("_" + time_now_formatted) + self.__suffix_template = "suffix_timestamp" + + elif ( + self.checkBox_suffix_custom.isChecked() + and self.checkBox_suffix_timestamp.isChecked() + ): time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y-%m-%d_%H%M%S') + time_now_formatted = time_now.strftime("%Y-%m-%d_%H%M%S") suffix_custom_value = self.lineEdit_suffix_custom_value.text() if suffix_custom_value: - space = '_' - self.__suffix_template = 'suffix_timestamp_custom' + space = "_" + self.__suffix_template = "suffix_timestamp_custom" else: - space = '' - self.__suffix_template = 'suffix_timestamp_custom_empty' - self.change_suffix('_' + time_now_formatted + space + suffix_custom_value) + space = "" + self.__suffix_template = "suffix_timestamp_custom_empty" + self.change_suffix("_" + time_now_formatted + space + suffix_custom_value) else: - self.change_suffix('') - self.__suffix_template = 'empty' + self.change_suffix("") + self.__suffix_template = "empty" def open_dialog_about(self): """ @@ -182,70 +205,82 @@ def parsing_thread_start(self): self.statusbar.clearMessage() self.progressBar.setVisible(True) - info = 'Converting started:' - color = 'black' + info = "Converting started:" + color = "black" self.print_log(info, color=color) try: - if self.__suffix_template == 'suffix_timestamp': + if self.__suffix_template == "suffix_timestamp": time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y%m%d_%H%M%S') - self.update_parsing_settings('suffix', '_' + time_now_formatted) + time_now_formatted = time_now.strftime("%Y%m%d_%H%M%S") + self.update_parsing_settings("suffix", "_" + time_now_formatted) - elif self.__suffix_template == 'suffix_custom': + elif self.__suffix_template == "suffix_custom": suffix_custom_value = self.lineEdit_suffix_custom_value.text() - self.update_parsing_settings('suffix', '_' + suffix_custom_value) + self.update_parsing_settings("suffix", "_" + suffix_custom_value) - elif self.__suffix_template == 'suffix_custom_empty': - self.update_parsing_settings('suffix', '') + elif self.__suffix_template == "suffix_custom_empty": + self.update_parsing_settings("suffix", "") - elif self.__suffix_template == 'suffix_custom_timestamp': + elif self.__suffix_template == "suffix_custom_timestamp": time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y%m%d_%H%M%S') + time_now_formatted = time_now.strftime("%Y%m%d_%H%M%S") suffix_custom_value = self.lineEdit_suffix_custom_value.text() - self.update_parsing_settings('suffix', '_' + suffix_custom_value + '_' + time_now_formatted) + self.update_parsing_settings( + "suffix", "_" + suffix_custom_value + "_" + time_now_formatted + ) - elif self.__suffix_template == 'suffix_custom_empty_timestamp': + elif self.__suffix_template == "suffix_custom_empty_timestamp": time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y%m%d_%H%M%S') - self.update_parsing_settings('suffix', '_' + time_now_formatted) + time_now_formatted = time_now.strftime("%Y%m%d_%H%M%S") + self.update_parsing_settings("suffix", "_" + time_now_formatted) - elif self.__suffix_template == 'suffix_timestamp_custom': + elif self.__suffix_template == "suffix_timestamp_custom": time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y%m%d_%H%M%S') + time_now_formatted = time_now.strftime("%Y%m%d_%H%M%S") suffix_custom_value = self.lineEdit_suffix_custom_value.text() - self.update_parsing_settings('suffix', '_' + time_now_formatted + '_' + suffix_custom_value) + self.update_parsing_settings( + "suffix", "_" + time_now_formatted + "_" + suffix_custom_value + ) - elif self.__suffix_template == 'suffix_timestamp_custom_empty': + elif self.__suffix_template == "suffix_timestamp_custom_empty": time_now = datetime.datetime.now() - time_now_formatted = time_now.strftime('%Y%m%d_%H%M%S') - self.update_parsing_settings('suffix', '_' + time_now_formatted) - - elif self.__suffix_template == 'empty': - self.update_parsing_settings('suffix', '') - - - self.parsing_thread = ParsingThread(files_to_pars=self.__files_to_pars, - target_directory=self.__target_directory, - target_directory_changed=self.__target_directory_changed, - parsing_settings=self.__parsing_settings) + time_now_formatted = time_now.strftime("%Y%m%d_%H%M%S") + self.update_parsing_settings("suffix", "_" + time_now_formatted) + + elif self.__suffix_template == "empty": + self.update_parsing_settings("suffix", "") + + self.parsing_thread = ParsingThread( + files_to_pars=self.__files_to_pars, + target_directory=self.__target_directory, + target_directory_changed=self.__target_directory_changed, + parsing_settings=self.__parsing_settings, + ) self.parsing_thread.start() self.parsing_thread.signal.connect(self.parsing_thread_done) self.parsing_thread.progress.connect(self.conversion_progress) - self.parsing_thread.file_conversion_ended.connect(self.file_conversion_ended) + self.parsing_thread.file_conversion_ended.connect( + self.file_conversion_ended + ) except Exception as e: - color = 'black' - self.print_log('\nUps... ERROR occurred. \n\n' + str(e), color=color) + color = "black" + self.print_log("\nUps... ERROR occurred. \n\n" + str(e), color=color) traceback.print_exc() - print('>>>', e, '<<<') + print(">>>", e, "<<<") def file_conversion_ended(self): """ Function sends notification via status bar about ending of conversion for each input file. """ self.__file_conversion_counter += 1 - info = str(self.__file_conversion_counter) + ' of ' + str(len(self.__files_to_pars)) + ' converted' + info = ( + str(self.__file_conversion_counter) + + " of " + + str(len(self.__files_to_pars)) + + " converted" + ) self.statusbar.showMessage(info) self.statusbar.repaint() @@ -262,14 +297,14 @@ def conversion_progress(self, row_number, all_rows_number): self.progressBar.setValue(row_number) def parsing_thread_done(self, info): - if '[action=start]' in info: - color = 'green' + if "[action=start]" in info: + color = "green" self.print_log(info, color) - elif '[action=end]' in info: - color = 'green' + elif "[action=end]" in info: + color = "green" self.print_log(info, color) else: - color = 'black' + color = "black" self.print_log(info, color) def update_parsing_settings(self, setting_name, setting_value): @@ -286,7 +321,7 @@ def set_suffix(self, suffix_value): :param suffix_value: input suffix """ self.__suffix = suffix_value - self.update_parsing_settings('suffix', suffix_value) + self.update_parsing_settings("suffix", suffix_value) def change_suffix(self, suffix_value_new): """ @@ -300,7 +335,7 @@ def change_suffix(self, suffix_value_new): self.set_suffix(new_suffix) - color = 'green' + color = "green" info = 'Suffix changed from "' + old_suffix + '" to "' + self.__suffix + '"' self.print_log(info, color) @@ -311,7 +346,7 @@ def set_delimiter(self, delimiter_value): :param delimiter_value: input delimiter """ self.__delimiter = delimiter_value - self.update_parsing_settings('csv_delimiter', delimiter_value) + self.update_parsing_settings("csv_delimiter", delimiter_value) def get_delimiter(self): """ @@ -330,8 +365,14 @@ def change_delimiter(self): self.set_delimiter(new_delimiter) - color = 'green' - info = 'Delimiter changed from "' + old_delimiter + '" to "' + self.__delimiter + '"' + color = "green" + info = ( + 'Delimiter changed from "' + + old_delimiter + + '" to "' + + self.__delimiter + + '"' + ) self.print_log(info, color) def set_target_directory(self, target_directory_value): @@ -360,12 +401,14 @@ def get_target_directory_from_directory(self): information is displayed in GUI. """ - number_of_subdirectories = self.check_if_subdirectory_exist(self.__target_directory) + number_of_subdirectories = self.check_if_subdirectory_exist( + self.__target_directory + ) # print(number_of_subdirectories) if number_of_subdirectories: - info = ' (and subdirectories)' + info = " (and subdirectories)" else: - info = '' + info = "" self.lineEdit_target_directory.setText(self.__target_directory + info) @@ -377,26 +420,34 @@ def change_target_directory(self): """ old_target_directory = self.__target_directory - title = 'Choose new target directory' - starting_directory = '' + title = "Choose new target directory" + starting_directory = "" options = QFileDialog.Options() options |= QFileDialog.ShowDirsOnly file_dialog = QFileDialog() - directories = file_dialog.getExistingDirectory(None, title, starting_directory, options=options) + directories = file_dialog.getExistingDirectory( + None, title, starting_directory, options=options + ) if directories: self.set_target_directory(directories) self.get_target_directory_from_file() - color = 'green' - info = 'Target directory changed from "' + old_target_directory + '" to "' + self.__target_directory + '"' + color = "green" + info = ( + 'Target directory changed from "' + + old_target_directory + + '" to "' + + self.__target_directory + + '"' + ) self.print_log(info, color) self.__target_directory_changed = True else: - info = 'Target directory not changed.' - color = 'black' + info = "Target directory not changed." + color = "black" self.print_log(info, color=color) def open_files(self): @@ -405,22 +456,24 @@ def open_files(self): Possible to select one or more files. """ - info = 'File\-s opening.' - color = 'black' + info = "File\-s opening." + color = "black" self.print_log(info, color=color) - extension = '*.csv' + extension = "*.csv" - title = 'Open {0} files'.format(extension) - starting_directory = '' - file_filter = 'CSV ({0})'.format(extension) + title = "Open {0} files".format(extension) + starting_directory = "" + file_filter = "CSV ({0})".format(extension) options = QFileDialog.Options() options |= QFileDialog.ReadOnly file_dialog = QFileDialog() file_dialog.setFileMode(QFileDialog.ExistingFiles) - files = file_dialog.getOpenFileNames(self, title, starting_directory, filter=file_filter, options=options) + files = file_dialog.getOpenFileNames( + self, title, starting_directory, filter=file_filter, options=options + ) files_only = files[0] # print(len(files_only),files_only) if len(files_only): @@ -439,8 +492,8 @@ def open_files(self): self.__target_directory_changed = False else: number_of_files = 0 - info = 'Selected {0} files.'.format(str(number_of_files)) - color = 'black' + info = "Selected {0} files.".format(str(number_of_files)) + color = "black" self.print_log(info, color=color) @staticmethod @@ -452,7 +505,7 @@ def check_if_subdirectory_exist(main_directory): :return: 0 if there is no subdirectories number > 0 if there is any directory, where number gives information about number of subdirectories. """ - pattern = os.path.join(main_directory, '*') + pattern = os.path.join(main_directory, "*") number_of_directories = 0 for candidate in glob.glob(pattern): if os.path.isdir(candidate): @@ -466,26 +519,31 @@ def open_directory(self): Possible to get files from selected directory and subdirectories. """ - info = 'Files from directory and subdirectories opening.' - color = 'black' + info = "Files from directory and subdirectories opening." + color = "black" self.print_log(info, color=color) - extension = '*.csv' + extension = "*.csv" - title = 'Open {0} files directory'.format(extension) - starting_directory = '' + title = "Open {0} files directory".format(extension) + starting_directory = "" options = QFileDialog.Options() options |= QFileDialog.ShowDirsOnly file_dialog = QFileDialog() - directories = file_dialog.getExistingDirectory(None, title, starting_directory, options=options) + directories = file_dialog.getExistingDirectory( + None, title, starting_directory, options=options + ) os_separator = os.path.sep if directories: self.set_target_directory(directories) self.get_target_directory_from_directory() - files = glob.glob(directories + os_separator + '**' + os_separator + extension, recursive=True) + files = glob.glob( + directories + os_separator + "**" + os_separator + extension, + recursive=True, + ) print(files) self.list_of_files_to_pars(files) self.pushButton_start.setEnabled(True) @@ -493,8 +551,8 @@ def open_directory(self): else: number_of_files = 0 - info = 'Selected {0} files.'.format(str(number_of_files)) - color = 'black' + info = "Selected {0} files.".format(str(number_of_files)) + color = "black" self.print_log(info, color=color) def open_target_directory(self): @@ -506,16 +564,17 @@ def open_target_directory(self): """ os_name = platform.system() - if os_name == 'Darwin': - subprocess.call(['open', self.__target_directory]) - elif os_name == 'Windows': - subprocess.call(['explorer', self.__target_directory]) - elif os_name == 'Linux': - subprocess.call(['nautilus', self.__target_directory]) + if os_name == "Darwin": + subprocess.call(["open", self.__target_directory]) + elif os_name == "Windows": + subprocess.call(["explorer", self.__target_directory]) + elif os_name == "Linux": + subprocess.call(["nautilus", self.__target_directory]) else: - info = 'Can\'t open directory, your Operating System {0} is not supported. Please report it to us.'.format( - os_name) - color = 'red' + info = "Can't open directory, your Operating System {0} is not supported. Please report it to us.".format( + os_name + ) + color = "red" self.print_log(info, color=color) def print_log(self, log_value, color): @@ -525,16 +584,21 @@ def print_log(self, log_value, color): :param log_value: information to display :param color: color for given information """ - if color == 'black': + if color == "black": color_set = QColor(0, 0, 0) - elif color == 'red': + elif color == "red": color_set = QColor(230, 30, 30) - elif color == 'green': + elif color == "green": color_set = QColor(60, 160, 60) else: color_set = QColor(0, 0, 0) - log_output = '[' + datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f') + '] ' + log_value + log_output = ( + "[" + + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + + "] " + + log_value + ) self.textEdit_progress.setTextColor(color_set) self.textEdit_progress.append(log_output) @@ -545,9 +609,9 @@ def exit_application(self): """ Function to exit from application. """ - info = 'Exit application' + info = "Exit application" print(info) - color = 'black' + color = "black" self.print_log(info, color=color) self.close() @@ -561,31 +625,40 @@ def list_of_files_to_pars(self, files): """ number_of_files = len(files) if number_of_files == 1: - suffix = '' + suffix = "" else: - suffix = 's' - info = 'Selected {0} file{1}:'.format(str(number_of_files), suffix) - color = 'black' + suffix = "s" + info = "Selected {0} file{1}:".format(str(number_of_files), suffix) + color = "black" self.print_log(info, color=color) self.__files_to_pars = files - color = 'black' + color = "black" for file in self.__files_to_pars: # self.print_log(file, color=color) - action_name = 'info ' - notification_info = '[action={0}] [source_file={1}]'.format(action_name, file) + action_name = "info " + notification_info = "[action={0}] [source_file={1}]".format( + action_name, file + ) self.print_log(notification_info, color=color) self.__file_conversion_counter = 0 class ParsingThread(QThread): - signal = pyqtSignal('PyQt_PyObject') + signal = pyqtSignal("PyQt_PyObject") progress = pyqtSignal(int, int) file_conversion_ended = pyqtSignal(int) file_conversion_started = pyqtSignal(int) - def __init__(self, files_to_pars, target_directory, target_directory_changed, parsing_settings, parent=None): + def __init__( + self, + files_to_pars, + target_directory, + target_directory_changed, + parsing_settings, + parent=None, + ): super(ParsingThread, self).__init__(parent) self.files_to_pars = files_to_pars @@ -597,52 +670,81 @@ def run(self): files_to_pars = self.files_to_pars target_directory_changed = self.target_directory_changed target_directory = self.target_directory - source_file_delimiter = self.parsing_settings['csv_delimiter'] + source_file_delimiter = self.parsing_settings["csv_delimiter"] - if 'suffix' in self.parsing_settings: - suffix = self.parsing_settings['suffix'] + if "suffix" in self.parsing_settings: + suffix = self.parsing_settings["suffix"] print("suffix: ", suffix) else: - suffix = '' + suffix = "" for source_file_name_with_path in files_to_pars: source_file_name = os.path.basename(source_file_name_with_path) source_file_name_without_extension = os.path.splitext(source_file_name)[0] source_file_extension = os.path.splitext(source_file_name)[1] - source_file_path = os.path.dirname(os.path.abspath(source_file_name_with_path)) - target_file_name = source_file_name_without_extension + suffix + '.xlsx' - target_file_path = os.path.dirname(os.path.abspath(source_file_name_with_path)) + source_file_path = os.path.dirname( + os.path.abspath(source_file_name_with_path) + ) + target_file_name = source_file_name_without_extension + suffix + ".xlsx" + target_file_path = os.path.dirname( + os.path.abspath(source_file_name_with_path) + ) if target_directory_changed: directory_to_save = target_directory else: directory_to_save = target_file_path - final_path_to_save = directory_to_save + '/' + target_file_name + final_path_to_save = directory_to_save + "/" + target_file_name start_time = time.time() - self.log_emitter('start', source_file_name) + self.log_emitter("start", source_file_name) self.file_conversion_started.emit(1) - self.log_emitter('info', source_file_name, '[source_file_path={0}]'.format(source_file_path)) - self.log_emitter('info', source_file_name, '[source_file_extension={0}]'.format(source_file_extension)) + self.log_emitter( + "info", + source_file_name, + "[source_file_path={0}]".format(source_file_path), + ) + self.log_emitter( + "info", + source_file_name, + "[source_file_extension={0}]".format(source_file_extension), + ) source_file_size = utilities.size_of_file_human(source_file_name_with_path) - self.log_emitter('info', source_file_name, '[source_file_size={0}]'.format(source_file_size)) - - source_file_encoding = utilities.check_file_encoding(source_file_name_with_path) - self.log_emitter('info', source_file_name, '[source_file_encoding={0}]'.format(source_file_encoding)) - - source_file_lines_number = utilities.csv_file_row_counter(source_file_name_with_path, source_file_delimiter) - self.log_emitter('info', source_file_name, '[source_file_lines_number={0}]'.format(str( - source_file_lines_number))) - - file = open(source_file_name_with_path, 'r', encoding=source_file_encoding) - csv.register_dialect('colons', delimiter=source_file_delimiter) - reader = csv.reader(file, dialect='colons') - - workbook = xlsxwriter.Workbook(final_path_to_save, {'constant_memory': True}) - worksheet = workbook.add_worksheet('details') + self.log_emitter( + "info", + source_file_name, + "[source_file_size={0}]".format(source_file_size), + ) + + source_file_encoding = utilities.check_file_encoding( + source_file_name_with_path + ) + self.log_emitter( + "info", + source_file_name, + "[source_file_encoding={0}]".format(source_file_encoding), + ) + + source_file_lines_number = utilities.csv_file_row_counter( + source_file_name_with_path, source_file_delimiter + ) + self.log_emitter( + "info", + source_file_name, + "[source_file_lines_number={0}]".format(str(source_file_lines_number)), + ) + + file = open(source_file_name_with_path, "r", encoding=source_file_encoding) + csv.register_dialect("colons", delimiter=source_file_delimiter) + reader = csv.reader(file, dialect="colons") + + workbook = xlsxwriter.Workbook( + final_path_to_save, {"constant_memory": True} + ) + worksheet = workbook.add_worksheet("details") for row_index, row in enumerate(reader): row_info = row_index + 1 @@ -652,20 +754,36 @@ def run(self): # print(">",cell,"<") worksheet.write(row_index, column_index, cell) - self.log_emitter('info', source_file_name, '[target_file_name={0}] [file saving]'.format(target_file_name)) + self.log_emitter( + "info", + source_file_name, + "[target_file_name={0}] [file saving]".format(target_file_name), + ) workbook.close() - self.log_emitter('end', source_file_name, '[target_file_name={0}] [file saved]'.format(target_file_name)) + self.log_emitter( + "end", + source_file_name, + "[target_file_name={0}] [file saved]".format(target_file_name), + ) end_time = time.time() elapsed_time = end_time - start_time - elapsed_time_parsed = time.strftime('%H:%M:%S', time.gmtime(elapsed_time)) - - self.log_emitter('info', source_file_name, '[elapsed_time={0}]'.format(elapsed_time_parsed)) - self.log_emitter('info', source_file_name, '[target_file_path={0}]'.format(directory_to_save)) + elapsed_time_parsed = time.strftime("%H:%M:%S", time.gmtime(elapsed_time)) + + self.log_emitter( + "info", + source_file_name, + "[elapsed_time={0}]".format(elapsed_time_parsed), + ) + self.log_emitter( + "info", + source_file_name, + "[target_file_path={0}]".format(directory_to_save), + ) self.file_conversion_ended.emit(1) - def log_emitter(self, action_name, source_file_name, additional_info=''): + def log_emitter(self, action_name, source_file_name, additional_info=""): """ Function emits information from thread to display it in GUI. @@ -673,8 +791,9 @@ def log_emitter(self, action_name, source_file_name, additional_info=''): :param source_file_name: information about related source file :param additional_info: add more information if needed """ - notification = '[action={0}] [source_file_name={1}] {2}'.format(action_name, source_file_name, - additional_info) + notification = "[action={0}] [source_file_name={1}] {2}".format( + action_name, source_file_name, additional_info + ) self.signal.emit(notification) diff --git a/converter_csv/ico_generator.py b/converter_csv/ico_generator.py index a0f6610..066120d 100644 --- a/converter_csv/ico_generator.py +++ b/converter_csv/ico_generator.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -u""" +""" Converter CSV by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a GUI tool which lets you convert multiple large csv files to xlsx files. Copyright (C) 2018 Damian Krawczyk @@ -20,6 +20,6 @@ from converter_csv import utilities -png_filename = '../icons/LimberDuck-converter-csv.png' +png_filename = "../icons/LimberDuck-converter-csv.png" -print(utilities.file_to_base64(utilities.png_to_ico(png_filename))) \ No newline at end of file +print(utilities.file_to_base64(utilities.png_to_ico(png_filename))) diff --git a/converter_csv/ldcc_ico.py b/converter_csv/ldcc_ico.py index 61b670e..63e8b48 100644 --- a/converter_csv/ldcc_ico.py +++ b/converter_csv/ldcc_ico.py @@ -1,4 +1,3 @@ -ico = \ -''' +ico = """ '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' -''' \ No newline at end of file +""" diff --git a/converter_csv/utilities.py b/converter_csv/utilities.py index e7ae0e3..cb4b1ea 100644 --- a/converter_csv/utilities.py +++ b/converter_csv/utilities.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -u""" +""" Converter CSV by LimberDuck (pronounced *ˈlɪm.bɚ dʌk*) is a GUI tool which lets you convert multiple large csv files to xlsx files. Copyright (C) 2018 Damian Krawczyk @@ -32,7 +32,7 @@ def file_to_base64(filename): :param filename: input file name with path :return: base64 string """ - with open(filename, 'rb') as image_file: + with open(filename, "rb") as image_file: encoded_string = base64.b64encode(image_file.read()) return encoded_string @@ -45,7 +45,7 @@ def png_to_ico(filename): :return: ico file name """ filename_without_extension = os.path.splitext(filename)[0] - target_file_name = filename_without_extension + '.ico' + target_file_name = filename_without_extension + ".ico" img = imageio.imread(filename) imageio.imwrite(target_file_name, img) @@ -65,14 +65,15 @@ def base64_to_ico(ico_in_base64, filename): :return: ico file name """ filename_without_extension = os.path.splitext(filename)[0] - target_file_name = filename_without_extension + '.ico' + target_file_name = filename_without_extension + ".ico" icondata = base64.b64decode(ico_in_base64) - iconfile = open(target_file_name, 'wb') + iconfile = open(target_file_name, "wb") iconfile.write(icondata) iconfile.close() return target_file_name + def check_file_encoding(file): """ Function checks encoding for input file. @@ -80,14 +81,14 @@ def check_file_encoding(file): :param file: input file path :return: file encoding eg. 'ascii', 'utf8' """ - raw_data = open(file, 'rb').read() + raw_data = open(file, "rb").read() result = chardet.detect(raw_data) - char_enc = result['encoding'] + char_enc = result["encoding"] return char_enc -def size_of_file_human(file, suffix='B'): +def size_of_file_human(file, suffix="B"): """ Function convert provided file size into human readable form :param file: source file name with path @@ -96,11 +97,11 @@ def size_of_file_human(file, suffix='B'): """ num = os.path.getsize(file) - for unit in [' b', ' Ki', ' Mi', ' Gi', ' Ti', ' Pi', ' Ei', ' Zi']: + for unit in [" b", " Ki", " Mi", " Gi", " Ti", " Pi", " Ei", " Zi"]: if abs(num) < 1024.0: - return '%3.1f%s%s' % (num, unit, suffix) + return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 - return '%.1f%s%s' % (num, 'Yi', suffix) + return "%.1f%s%s" % (num, "Yi", suffix) def csv_file_row_counter(file, source_file_delimiter): @@ -112,9 +113,9 @@ def csv_file_row_counter(file, source_file_delimiter): :return: number of rows """ source_file_encoding = check_file_encoding(file) - file = open(file, 'r', encoding=source_file_encoding) - csv.register_dialect('colons', delimiter=source_file_delimiter) - reader = csv.reader(file, dialect='colons') + file = open(file, "r", encoding=source_file_encoding) + csv.register_dialect("colons", delimiter=source_file_delimiter) + reader = csv.reader(file, dialect="colons") row_count = sum(1 for row in reader) return row_count diff --git a/requirements.txt b/requirements.txt index 951292d..971b709 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -chardet>=4.0.0 -imageio>=2.9.0 -PyQt5>=5.15.4 -XlsxWriter>=3.0.1 \ No newline at end of file +chardet>=5.2.0 +imageio>=2.37.0 +PyQt5>=5.15.11 +XlsxWriter>=3.2.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 2e0674b..da86c5f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ with open("README.md", "r") as fh: long_description = fh.read() -with open('requirements.txt') as f: +with open("requirements.txt") as f: required = f.read().splitlines() about = {} @@ -13,6 +13,7 @@ setuptools.setup( name="converter_csv", version=about["__version__"], + license="GPLv3", author="Damian Krawczyk", author_email="damian.krawczyk@limberduck.org", description="Converter CSV by LimberDuck is a GUI tool to convert multiple large csv files to xlsx files.", @@ -21,16 +22,11 @@ url="https://github.com/LimberDuck/converter-csv", packages=setuptools.find_packages(), install_requires=required, - entry_points={ - "gui_scripts": [ - "converter-csv = converter_csv.__main__:main" - ] - }, + entry_points={"gui_scripts": ["converter-csv = converter_csv.__main__:main"]}, classifiers=[ - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", "Development Status :: 4 - Beta",