diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 82f1c4d4..6bd3aa54 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -41,6 +41,7 @@ jobs: run: | pylint conflowgen pylint conflowgen.tests + pylint conflowgen.command_line_interface.spreadsheet_interface - name: Check code quality with flake8 run: | diff --git a/.gitignore b/.gitignore index 487aaeb9..61e2acad 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ examples/Python_Script/databases/ conflowgen/data/tools/ docs/notebooks/data/prepared_dbs/ .tools/ +examples/Spreadsheet_Interface/SpreadsheetInterface_CSV_Export/ diff --git a/conflowgen/command_line_interface/spreadsheet_interface.py b/conflowgen/command_line_interface/spreadsheet_interface.py new file mode 100644 index 00000000..f2b8cb0c --- /dev/null +++ b/conflowgen/command_line_interface/spreadsheet_interface.py @@ -0,0 +1,195 @@ +""" +The command line tool allows to create an XLSX file with all input distributions +which the user can change and then read in again to create synthetic container flows. +""" +import datetime +import os +import argparse +import typing + +import openpyxl.worksheet.worksheet +import openpyxl.workbook +import openpyxl.styles + +from conflowgen import metadata +from conflowgen.api.container_flow_generation_manager import ContainerFlowGenerationManager +from conflowgen.api.database_chooser import DatabaseChooser +from conflowgen.api.container_length_distribution_manager import ContainerLengthDistributionManager +from conflowgen.api.export_container_flow_manager import ExportContainerFlowManager + +DEFAULT_ROW_OFFSET_OF_TABLE = 4 + + +def _create_sheet( + workbook: openpyxl.workbook.Workbook, title: str, index: typing.Optional[int] = None +) -> openpyxl.worksheet.worksheet.Worksheet: + sheet = workbook.create_sheet(title, index) + sheet["B2"] = title + sheet["B2"].font = openpyxl.styles.Font(bold=True) + sheet["B2"].alignment = openpyxl.styles.Alignment(horizontal="center") + sheet.merge_cells('B2:C2') + return sheet + + +def create_input_spreadsheet(file_path: str, overwrite: bool) -> None: + """ + file_path: The path to the XLSX file to create + """ + print(f"Create XLSX file at {file_path}") + if os.path.isfile(file_path): + if not overwrite: + print(f"The file {file_path} already exists!") + return + print(f"The file {file_path} will be overwritten") + + workbook = openpyxl.Workbook() + + metadata_sheet = _create_sheet(workbook, "ConFlowGen metadata", 0) + metadata_sheet["B4"] = "Version" + metadata_sheet["C4"] = metadata.__version__ + metadata_sheet["B5"] = "Description" + metadata_sheet["C5"] = metadata.__description__ + + DatabaseChooser().create_new_sqlite_database(":memory:") + + container_flow_generation_sheet = _create_sheet(workbook, "Container flow generation") + container_flow_generation_properties = ContainerFlowGenerationManager().get_properties() + for i, (key, value) in enumerate(container_flow_generation_properties.items()): + row_index = i + DEFAULT_ROW_OFFSET_OF_TABLE + container_flow_generation_sheet["B" + str(row_index)] = key + container_flow_generation_sheet["C" + str(row_index)] = value + + length_distribution_sheet = _create_sheet(workbook, "Container Length Distribution") + container_length_distribution = ContainerLengthDistributionManager().get_container_length_distribution() + for i, (container_length, percentage) in enumerate(container_length_distribution.items()): + row_index = i + DEFAULT_ROW_OFFSET_OF_TABLE + length_distribution_sheet["B" + str(row_index)] = str(container_length) + length_distribution_sheet["C" + str(row_index)] = percentage + + del workbook["Sheet"] # the default sheet we haven't used + for sheet in workbook: + for column in "ABCDEFG": + sheet.column_dimensions[column].width = 20 + + workbook.active = metadata_sheet + workbook.save(file_path) + + +def create_container_flow(file_path: str, overwrite: bool): + """ + file_path: The path to the XLSX file to read in + """ + print(f"Read XLSX file at {file_path} to create container flows") + + if not os.path.isfile(file_path): + print(f"The file {file_path} does not exists!") + return + + workbook = openpyxl.load_workbook(file_path) + + file_path_without_ending = ".".join(file_path.split(".")[:-1]) + + path_to_sqlite_file = os.path.join( + os.path.dirname(file_path), + file_path_without_ending + ".sqlite" + ) + if os.path.isfile(path_to_sqlite_file): + if not overwrite: + print(f"The file {path_to_sqlite_file} already exists!") + return + print(f"The file {path_to_sqlite_file} will be overwritten") + + DatabaseChooser().create_new_sqlite_database( + path_to_sqlite_file, + overwrite=overwrite + ) + print(f"Create container flow database: {path_to_sqlite_file}") + container_flow_generation_manager = ContainerFlowGenerationManager() + + container_flow_generation_sheet = workbook["Container flow generation"] + container_flow_generation_properties = {} + for row_index in range(DEFAULT_ROW_OFFSET_OF_TABLE, container_flow_generation_sheet.max_row + 1): + key = container_flow_generation_sheet["B" + str(row_index)].value + value = container_flow_generation_sheet["C" + str(row_index)].value + if key in [ + "conflowgen_version" + ]: # some information can be ignored + continue + container_flow_generation_properties[key] = value + print(f"Parsed container flow generation properties: {container_flow_generation_properties}") + if not container_flow_generation_properties["end_date"]: + container_flow_generation_properties["end_date"] = datetime.datetime.now() + if not container_flow_generation_properties["start_date"]: + container_flow_generation_properties["start_date"] = ( + container_flow_generation_properties["end_date"] - datetime.timedelta(days=21) + ) + print(f"Updated container flow generation properties: {container_flow_generation_properties} (start and end date " + "must be set)") + container_flow_generation_manager.set_properties(**container_flow_generation_properties) + print(f"Set container flow generation properties: {container_flow_generation_manager.get_properties()}") + + length_distribution_sheet = workbook["Container Length Distribution"] + container_length_distribution = {} + for row_index in range(DEFAULT_ROW_OFFSET_OF_TABLE, length_distribution_sheet.max_row + 1): + key = length_distribution_sheet["B" + str(row_index)].value + value = length_distribution_sheet["C" + str(row_index)].value + container_length_distribution[key] = value + print(f"Parsed container length distribution: {container_length_distribution}") + container_length_distribution_manager = ContainerLengthDistributionManager() + container_length_distribution_manager.set_container_length_distribution(container_length_distribution) + print(f"Set container length distribution: " + f"{container_length_distribution_manager.get_container_length_distribution()}") + + print("Generating container flow data...") + container_flow_generation_manager.generate() + + print("Exporting container flow data to CSV...") + path_to_export_folder = os.path.dirname(file_path_without_ending) + ExportContainerFlowManager().export( + folder_name=file_path_without_ending + "_CSV_Export", + path_to_export_folder=path_to_export_folder, + overwrite=overwrite, + ) + print("Creating container flow data has finished!") + + +def command_line_interface(): + parser = argparse.ArgumentParser( + prog="ConFlowGen Spreadsheet Interface", + description=( + "The command line tool allows to create an XLSX file with all input distributions " + "which the user can change and then read in again to create synthetic container flows." + ), + ) + valid_actions = [ + "create_input_spreadsheet", + "create_container_flow" + ] + parser.add_argument( + "action", + choices=valid_actions, + help="The action to perform", + ) + parser.add_argument( + "filepath", + help="The path to the XLSX file", + ) + parser.add_argument( + "--overwrite", + help="Whether existing files shall be overwritten", + default=False, + action="store_true", + ) + args = parser.parse_args() + filepath = os.path.abspath(os.path.join( + os.curdir, + args.filepath + )) + if args.action == "create_input_spreadsheet": + create_input_spreadsheet(filepath, args.overwrite) + elif args.action == "create_container_flow": + create_container_flow(filepath, args.overwrite) + + +if __name__ == "__main__": + command_line_interface() diff --git a/examples/Spreadsheet_Interface/Readme.md b/examples/Spreadsheet_Interface/Readme.md new file mode 100644 index 00000000..f063e516 --- /dev/null +++ b/examples/Spreadsheet_Interface/Readme.md @@ -0,0 +1,50 @@ +# ConFlowGen Spreadsheet Interface + +For convenience, ConFlowGen come shipped with a spreadsheet interface. +This allows to create a spreadsheet file with all input data to be easily modified in +MS Excel, LibreOffice Calc, and similar +without the need to directly use the Python API. +Once ConFlowGen is installed, the command `conflowgen_spreadsheet_interface` is available on your command line. +It comes with its own help function: + +``` +$ conflowgen_spreadsheet_interface -h +usage: ConFlowGen Spreadsheet Interface [-h] [--overwrite] {create_input_spreadsheet,create_container_flow} filepath + +The command line tool allows to create an XLSX file with all input distributions which the user can change and then read in again to create synthetic container flows. + +positional arguments: + {create_input_spreadsheet,create_container_flow} + The action to perform + filepath The path to the XLSX file + +options: + -h, --help show this help message and exit + --overwrite Whether existing files shall be overwritten +``` + +A short explanation of the commands: +- The command `create_input_spreadsheet` creates a spreadsheet file based on the current default distributions in + ConFlowGen. +- The command `create_container_flow` reads in a prepared spreadsheet file and creates synthetic container book data. + The result is stored in an SQLite file right next to the spreadsheet file, using the same file name but a suitable + file extension. + +## Example + +The spreadsheet in this folder has been created by running the following command on the CLI: + +``` +$ conflowgen_spreadsheet_interface create_input_spreadsheet SpreadsheetInterface.xlsx +``` + +You can open it in your preferred spreadsheet application such as +MS Excel or LibreOffice Calc. + +After having modified all the input data as desired, the following command on the CLI generated the container flow data: + +``` +$ conflowgen_spreadsheet_interface create_container_flow SpreadsheetInterface.xlsx +``` + +This results in the SQLite database `SpreadsheetInterface.sqlite` \ No newline at end of file diff --git a/examples/Spreadsheet_Interface/SpreadsheetInterface.sqlite b/examples/Spreadsheet_Interface/SpreadsheetInterface.sqlite new file mode 100644 index 00000000..8916231e --- /dev/null +++ b/examples/Spreadsheet_Interface/SpreadsheetInterface.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:407e56c0e6e8c642f61fdba958760c637efd484a7c3cbdddc982cb7700df02d8 +size 184320 diff --git a/examples/Spreadsheet_Interface/SpreadsheetInterface.xlsx b/examples/Spreadsheet_Interface/SpreadsheetInterface.xlsx new file mode 100644 index 00000000..3f64a5ad --- /dev/null +++ b/examples/Spreadsheet_Interface/SpreadsheetInterface.xlsx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2175d6dad9341613a6a3e3ec3518b092422c6f6e9ceddf8490c5d4cf6f0aad7 +size 6510 diff --git a/pyproject.toml b/pyproject.toml index 4a7908f5..2d7b0ac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,3 +81,6 @@ jupyterlab = [ "jupyterlab-lsp", # better autocomplete "python-lsp-server[all]", # better autocomplete ] + +[project.scripts] +conflowgen_spreadsheet_interface = "conflowgen.command_line_interface.spreadsheet_interface:command_line_interface" diff --git a/run_ci_light.bat b/run_ci_light.bat index a68f2f17..620b4472 100644 --- a/run_ci_light.bat +++ b/run_ci_light.bat @@ -69,21 +69,30 @@ ruff check || ( EXIT /B ) +ECHO.Linting ConFlowGen main library pylint conflowgen || ( ECHO.While linting the conflowgen module, pylint failed! EXIT /B ) +ECHO.Linting ConFlowGen setup.py pylint setup.py || ( ECHO.While linting setup.py, pylint failed! EXIT /B ) +ECHO.Linting ConFlowGen tests pylint conflowgen.tests || ( ECHO.While linting the conflowgen tests, pylint failed! EXIT /B ) +ECHO.Linting ConFlowGen spreadsheet interface +pylint conflowgen.command_line_interface.spreadsheet_interface || ( + ECHO.While linting the conflowgen spreadsheet interface, pylint failed! + EXIT /B +) + REM build docs CALL docs/make clean || ( ECHO.Cleaning up the last built of the documentation failed!