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