From 2dc4f4697383ab4108acc440aefe6a8bf0c93b9e Mon Sep 17 00:00:00 2001 From: Daniel <d.hornung@indiscale.com> Date: Wed, 6 Mar 2024 14:39:51 +0100 Subject: [PATCH] WIP: Filling XLSX --- .../table_json_conversion/fill_xlsx.py | 139 ++++++++++++++++-- .../table_json_conversion/table_generator.py | 2 +- .../table_json_conversion/multiple_refs.xlsx | Bin 12238 -> 12349 bytes .../table_json_conversion/test_fill_xlsx.py | 4 +- .../test_table_template_generator.py | 2 +- 5 files changed, 131 insertions(+), 16 deletions(-) diff --git a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py index 79c7bfea..cca0735f 100644 --- a/src/caosadvancedtools/table_json_conversion/fill_xlsx.py +++ b/src/caosadvancedtools/table_json_conversion/fill_xlsx.py @@ -5,6 +5,7 @@ # # Copyright (C) 2024 Indiscale GmbH <info@indiscale.com> # Copyright (C) 2024 Henrik tom Wörden <h.tomwoerden@indiscale.com> +# Copyright (C) 2024 Daniel Hornung <d.hornung@indiscale.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -19,6 +20,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. +import json +from types import SimpleNamespace +from typing import List, Union, TextIO + from openpyxl import load_workbook from .table_generator import ColumnType, RowType @@ -34,7 +39,7 @@ def _fill_leaves(json_doc: dict, workbook): workbook.cell(1, 2, el) -def _get_row_type_column(worksheet): +def _get_row_type_column_index(worksheet): for col in worksheet.columns: for cell in col: if cell.value == RowType.COL_TYPE.name: @@ -44,7 +49,7 @@ def _get_row_type_column(worksheet): def _get_path_rows(worksheet): rows = [] - rt_col = _get_row_type_column(worksheet) + rt_col = _get_row_type_column_index(worksheet) for cell in list(worksheet.columns)[rt_col-1]: print(cell.value) if cell.value == RowType.PATH.name: @@ -53,18 +58,128 @@ def _get_path_rows(worksheet): def _generate_path_col_mapping(workbook): - rt_col = _get_row_type_column(workbook) + rt_col = _get_row_type_column_index(workbook) for col in workbook.columns: pass -def fill_template(template_path: str, json_path: str, result_path: str) -> None: - """ - Fill the contents of the JSON document stored at ``json_path`` into the template stored at - ``template_path`` and store the result under ``result_path``. - """ - template = load_workbook(template_path) +class TemplateFiller: + def __init__(self, workbook): + self._workbook = workbook + self._create_index() + + def fill_data(self, data: dict): + """Fill the data into the workbook.""" + self._handle_data(data=data, current_path=[]) + + def _create_index(self, ): + """Create a sheet index for the workbook. + + Index the sheets by their relevant path array. Also create a simple column index by column + type and path. + + """ + self._sheet_index = {} + for sheetname in self._workbook.sheetnames: + sheet = self._workbook[sheetname] + type_column = [x.value for x in list(sheet.columns)[ + _get_row_type_column_index(sheet) - 1]] + # 0-indexed, as everything outside of sheet.cell(...): + coltype_idx = type_column.index(RowType.COL_TYPE.name) + path_indices = [i for i, typ in enumerate(type_column) if typ == RowType.PATH.name] + + # Get the paths, use without the leaf component for sheet indexing, with type prefix and + # leaf for column indexing. + paths = [] + col_index = {} + for col_idx, col in enumerate(sheet.columns): + if col[coltype_idx].value == RowType.COL_TYPE.name: + continue + path = [] + for path_idx in path_indices: + if col[path_idx].value is not None: + path.append(col[path_idx].value) + col_key = ".".join([col[coltype_idx].value] + path) + col_index[col_key] = SimpleNamespace(column=col, col_index=col_idx) + if col[coltype_idx].value not in [ColumnType.SCALAR.name, ColumnType.LIST.name]: + continue + paths.append(path[:-1]) + + # Find common components: + common_path = [] + for idx, component in enumerate(paths[0]): + for path in paths: + if not path[idx] == component: + break + else: + common_path.append(component) + assert len(common_path) >= 1 + + self._sheet_index[".".join(common_path)] = SimpleNamespace( + common_path=common_path, sheetname=sheetname, sheet=sheet) + + def _handle_data(self, data: dict, current_path: List[str] = None): + """Handle the data and write it into ``workbook``. + """ + if current_path is None: + current_path = [] + for name, content in data.items(): + path = current_path + [name] + if isinstance(content, list): + if not content: + continue + assert len(set(type(entry) for entry in content)) == 1 + if isinstance(content[0], dict): + # An array of objects: must go into exploded sheet + for entry in content: + self._handle_data(data=entry, current_path=path) + continue + for entry in content: + pass + else: + self._handle_single_data(data=content, current_path=path) + + def _handle_single_data(self, data, current_path: List[str]): + """Enter this single data item into the workbook. + """ + sheet = self._sheet_index[".".join(current_path)].sheet + for name, content in data.items(): + if isinstance(content, list): + # TODO handle later + continue + if isinstance(content, dict): + pass + # from IPython import embed + # embed() + + +def fill_template(data: Union[dict, str, TextIO], template: str, result: str) -> None: + """Insert json data into an xlsx file, according to a template. + +This function fills the json data into the template stored at ``template`` and stores the result as +``result``. + +Parameters +---------- +data: Union[dict, str, TextIO] + The data, given as Python dict, path to a file or a file-like object. +template: str + Path to the XLSX template. +result: str + Path for the result XLSX. +""" + if isinstance(data, dict): + pass + elif isinstance(data, str): + with open(data, encoding="utf-8") as infile: + data = json.load(infile) + elif hasattr(data, "read"): + data = json.load(data) + else: + raise ValueError(f"I don't know how to handle the datatype of `data`: {type(data)}") + result_wb = load_workbook(template) + template_filler = TemplateFiller(result_wb) # For each top level key in the json we iterate the values (if it is an array). Those are the # root elements that belong to a particular sheet. # After treating a root element, the row index for the corresponding sheet needs to be @@ -72,7 +187,6 @@ def fill_template(template_path: str, json_path: str, result_path: str) -> None: # When we finished treating an object that goes into a lower ranked sheet (see below), we # increase the row index of that sheet. # - # We can generate a hierarchy of sheets in the beginning (using the paths). The lower sheets # are for objects referenced by objects in higher ranked sheets. # We can detect the sheet corresponding to a root element by looking at the first path element: @@ -80,10 +194,11 @@ def fill_template(template_path: str, json_path: str, result_path: str) -> None: # Suggestion: # row indices: Dict[str, int] string is the sheet name # sheet_hirarchy: List[Tuple[str]] elements are sheet names - # + template_filler.fill_data(data=data) + # Question: # We can create an internal representation where we assign as sheet_names the same names that # are used in table generator. Or should we create another special row that contains this # somehow? - template.save(result_path) + result_wb.save(result) diff --git a/src/caosadvancedtools/table_json_conversion/table_generator.py b/src/caosadvancedtools/table_json_conversion/table_generator.py index 8794496f..0074e4ae 100644 --- a/src/caosadvancedtools/table_json_conversion/table_generator.py +++ b/src/caosadvancedtools/table_json_conversion/table_generator.py @@ -310,8 +310,8 @@ class XLSXTemplateGenerator(TableTemplateGenerator): """Create and return a nice workbook for the given sheets.""" wb = Workbook() yellowfill = PatternFill(fill_type="solid", fgColor='00FFFFAA') - assert wb.sheetnames == ["Sheet"] # remove initial sheet + assert wb.sheetnames == ["Sheet"] del wb['Sheet'] for sheetname, sheetdef in sheets.items(): diff --git a/unittests/table_json_conversion/multiple_refs.xlsx b/unittests/table_json_conversion/multiple_refs.xlsx index 34bea9b9b9d29c9308ec5cff496c91e7ecaa45c9..cff3dad99a3c296e360d660ed5178a0eee48cd40 100644 GIT binary patch delta 8538 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 Stbg0YK?~UE5#_;u9RCj%-f8Oq delta 8452 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 UyM@``ZD<k*xf#*t*zO$v3!r5)A^-pY diff --git a/unittests/table_json_conversion/test_fill_xlsx.py b/unittests/table_json_conversion/test_fill_xlsx.py index 10ddcc32..c3f26251 100644 --- a/unittests/table_json_conversion/test_fill_xlsx.py +++ b/unittests/table_json_conversion/test_fill_xlsx.py @@ -23,7 +23,7 @@ import os import tempfile from caosadvancedtools.table_json_conversion.fill_xlsx import ( - _get_path_rows, _get_row_type_column, fill_template) + _get_path_rows, _get_row_type_column_index, fill_template) from openpyxl import load_workbook @@ -37,7 +37,7 @@ def rfp(*pathcomponents): def test_detect(): example = load_workbook(rfp("example_template.xlsx")) - assert 1 == _get_row_type_column(example['Person']) + assert 1 == _get_row_type_column_index(example['Person']) assert [2, 3] == _get_path_rows(example['Person']) diff --git a/unittests/table_json_conversion/test_table_template_generator.py b/unittests/table_json_conversion/test_table_template_generator.py index 8115c187..670f7df1 100644 --- a/unittests/table_json_conversion/test_table_template_generator.py +++ b/unittests/table_json_conversion/test_table_template_generator.py @@ -61,7 +61,7 @@ out: tuple foreign_keys=foreign_keys, filepath=outpath) assert os.path.exists(outpath) - generated = load_workbook(outpath) # workbook can be read + generated = load_workbook(outpath) good = load_workbook(known_good) assert generated.sheetnames == good.sheetnames for sheetname in good.sheetnames: -- GitLab