diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 0fe0588..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - - - -**Description** - - - -**Steps to reproduce the issue:** -1. -2. -3. - -**Describe the results you received:** - -**Describe the results you expected:** - -**Output of `python --version`:** - -```text -(paste your output here) -``` - -**Output of `go version`:** - -```text -(paste your output here) -``` - -**Excelize version or commit ID:** - -```text -(paste here) -``` - -**Environment details (OS, Microsoft Excelâ„¢ version, Browser version, physical, etc.):** diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..6d67b93 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,81 @@ +name: Bug report +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. + + - type: textarea + id: description + attributes: + label: Description + description: Briefly describe the problem you are having in a few paragraphs. + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce the issue + description: Explain how to cause the issue in the provided reproduction. + placeholder: | + 1. + 2. + 3. + validations: + required: true + + - type: textarea + id: received + attributes: + label: Describe the results you received + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Describe the results you expected + validations: + required: true + + - type: input + id: py-version + attributes: + label: Python version + description: | + Output of `python --version`: + placeholder: e.g. 3.8.10 + validations: + required: true + + - type: input + id: excelize-version + attributes: + label: Excelize version or commit ID + description: | + Which version of Excelize are you using? + placeholder: e.g. 0.0.1 + validations: + required: true + + - type: textarea + id: env + attributes: + label: Environment + description: Environment details (OS, Microsoft Excel™ version, physical, etc.) + render: shell + validations: + required: true + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate. + required: true + - label: The provided reproduction is a minimal reproducible example of the bug. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 7ad8b95..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - - - -**Description** - - - -**Steps to reproduce the issue:** -1. -2. -3. - -**Describe the results you received:** - -**Describe the results you expected:** - -**Output of `python --version`:** - -```text -(paste your output here) -``` - -**Output of `go version`:** - -```text -(paste your output here) -``` - -**Excelize version or commit ID:** - -```text -(paste here) -``` - -**Environment details (OS, Microsoft Excelâ„¢ version, Browser version, physical, etc.):** diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..af626b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,30 @@ +name: Feature request +description: Suggest an idea for this project +body: + - type: markdown + attributes: + value: | + If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. + + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature that you would like added + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Any other context or screenshots about the feature request here? + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that requests the same feature to avoid creating a duplicate. + required: true diff --git a/excelize.py b/excelize.py index 47817cc..126878d 100644 --- a/excelize.py +++ b/excelize.py @@ -439,10 +439,89 @@ def py_value_to_c_interface(py_value): return py_value_to_c(interface, types_go._Interface()) +class StreamWriter: + sw_index: int + + def __init__(self, sw_index: int): + self.sw_index = sw_index + + def add_table(self, table: Table) -> Optional[Exception]: + """ + Creates an Excel table for the stream writer using the given cell range + and format set. + + Note that the table must be at least two lines including the header. The + header cells must contain strings and must be unique. Currently, only + one table is allowed for a stream writer. The function must be called + after the rows are written but before 'flush'. + + Args: + table (Table): The table options + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + + Example: + For example, create a table of A1:D5 on Sheet1: + + .. code-block:: python + + err = sw.add_table(excelize.Table(range="A1:D5")) + """ + lib.StreamAddTable.restype = c_char_p + options = py_value_to_c(table, types_go._Table()) + err = lib.StreamAddTable(self.sw_index, byref(options)).decode(ENCODE) + return None if err == "" else Exception(err) + + def set_row( + self, + cell: str, + values: List[Union[None, int, str, bool, datetime, date]], + ) -> Optional[Exception]: + """ + Writes an array to stream rows by giving starting cell reference and a + pointer to an array of values. Note that you must call the 'flush' + function to end the streaming writing process. + + Args: + cell (str): The cell reference + values (List[Union[None, int, str, bool, datetime, date]]): The cell + values + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.StreamSetRow.restype = c_char_p + vals = (types_go._Interface * len(values))() + for i, value in enumerate(values): + vals[i] = py_value_to_c_interface(value) + err = lib.StreamSetRow( + self.sw_index, + cell.encode(ENCODE), + byref(vals), + len(vals), + ).decode(ENCODE) + return None if err == "" else Exception(err) + + def flush(self) -> Optional[Exception]: + """ + Ending the streaming writing process. + + Returns: + Optional[Exception]: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.StreamFlush.restype = c_char_p + err = lib.StreamFlush(self.sw_index).decode(ENCODE) + return None if err == "" else Exception(err) + + class File: file_index: int - def __init__(self, file_index): + def __init__(self, file_index: int): self.file_index = file_index def save(self, *opts: Options) -> Optional[Exception]: @@ -1660,6 +1739,65 @@ def new_sheet(self, sheet: str) -> Tuple[int, Optional[Exception]]: err = res.err.decode(ENCODE) return res.val, None if err == "" else Exception(err) + def new_stream_writer( + self, sheet: str + ) -> Tuple[Optional[StreamWriter], Optional[Exception]]: + """ + Returns stream writer struct by given worksheet name used for writing + data on a new existing empty worksheet with large amounts of data. Note + that after writing data with the stream writer for the worksheet, you + must call the 'flush' method to end the streaming writing process, + ensure that the order of row numbers is ascending when set rows, and the + normal mode functions and stream mode functions can not be work mixed to + writing data on the worksheets. The stream writer will try to use + temporary files on disk to reduce the memory usage when in-memory chunks + data over 16MB, and you can't get cell value at this time. + + Args: + sheet (str): The worksheet name + + Returns: + Tuple[Optional[StreamWriter], Optional[Exception]]: A tuple + containing stream writer object if successful, or None and an + Exception if an error occurred. + + Example: + For example, set data for worksheet of size 102400 rows x 50 columns + with numbers: + + .. code-block:: python + + import excelize, random + + f = excelize.new_file() + sw, err = f.new_stream_writer("Sheet1") + if err: + print(err) + for r in range(2, 102401): + row = [random.randrange(640000) for _ in range(1, 51)] + cell, err = excelize.coordinates_to_cell_name(1, r, False) + if err: + print(err) + err = sw.set_row(cell, row) + if err: + print(err) + err = sw.flush() + if err: + print(err) + err = f.save_as("Book1.xlsx") + if err: + print(err) + err = f.close() + if err: + print(err) + """ + lib.NewStreamWriter.restype = types_go._IntErrorResult + res = lib.NewStreamWriter(self.file_index, sheet.encode(ENCODE)) + err = res.err.decode(ENCODE) + if err == "": + return StreamWriter(res.val), None + return None, Exception(err) + def new_style(self, style: Style) -> Tuple[int, Optional[Exception]]: """ Create the style for cells by a given style options, and returns style diff --git a/go.mod b/go.mod index 4ec8a51..e57317c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/xuri/excelize-py go 1.20 require ( - github.com/xuri/excelize/v2 v2.9.1-0.20241229043028-3f6ecffcca89 + github.com/xuri/excelize/v2 v2.9.1-0.20250105013731-af422e17009b golang.org/x/image v0.23.0 ) diff --git a/go.sum b/go.sum index 593c674..ad39a13 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/tiendc/go-deepcopy v1.2.0 h1:6vCCs+qdLQHzFqY1fcPirsAWOmrLbuccilfp8UzD github.com/tiendc/go-deepcopy v1.2.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.9.1-0.20241229043028-3f6ecffcca89 h1:KlbACs1A0q8+TJ3keiTh6ir/L0HBFwNe416d1c+dez4= -github.com/xuri/excelize/v2 v2.9.1-0.20241229043028-3f6ecffcca89/go.mod h1:NBRx6e5FHFx4mHLiYG1QBONNvNNSs/wrtzS+h56/A6k= +github.com/xuri/excelize/v2 v2.9.1-0.20250105013731-af422e17009b h1:IgzLFLfCA10Lu5Ac9XgC8wBvPAVqHe2ilgIA8nouiPc= +github.com/xuri/excelize/v2 v2.9.1-0.20250105013731-af422e17009b/go.mod h1:NBRx6e5FHFx4mHLiYG1QBONNvNNSs/wrtzS+h56/A6k= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= diff --git a/main.go b/main.go index 9216e18..c98e086 100644 --- a/main.go +++ b/main.go @@ -45,10 +45,11 @@ const ( ) var ( - files = sync.Map{} - emptyString string - errFilePtr = "can not find file pointer" - errArgType = errors.New("invalid argument data type") + files, sw = sync.Map{}, sync.Map{} + emptyString string + errFilePtr = "can not find file pointer" + errStreamWriterPtr = "can not find stream writer pointer" + errArgType = errors.New("invalid argument data type") // goBaseTypes defines Go's basic data types. goBaseTypes = map[reflect.Kind]bool{ @@ -592,7 +593,7 @@ func AddPictureFromBytes(idx int, sheet, cell *C.char, pic *C.struct_Picture) *C } options := goVal.Elem().Interface().(excelize.Picture) if err := f.(*excelize.File).AddPictureFromBytes(C.GoString(sheet), C.GoString(cell), &options); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -717,7 +718,7 @@ func AddVBAProject(idx int, file *C.uchar, fileLen C.int) *C.char { } buf := C.GoBytes(unsafe.Pointer(file), fileLen) if err := f.(*excelize.File).AddVBAProject(buf); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -1391,6 +1392,91 @@ func NewSheet(idx int, sheet *C.char) C.struct_IntErrorResult { return C.struct_IntErrorResult{val: C.int(idx), err: C.CString(emptyString)} } +// NewStreamWriter returns stream writer struct by given worksheet name used for +// writing data on a new existing empty worksheet with large amounts of data. +// Note that after writing data with the stream writer for the worksheet, you +// must call the 'Flush' method to end the streaming writing process, ensure +// that the order of row numbers is ascending when set rows, and the normal +// mode functions and stream mode functions can not be work mixed to writing +// data on the worksheets. The stream writer will try to use temporary files on +// disk to reduce the memory usage when in-memory chunks data over 16MB, and +// you can't get cell value at this time. +// +//export NewStreamWriter +func NewStreamWriter(idx int, sheet *C.char) C.struct_IntErrorResult { + f, ok := files.Load(idx) + if !ok { + return C.struct_IntErrorResult{val: C.int(0), err: C.CString(errFilePtr)} + } + streamWriter, err := f.(*excelize.File).NewStreamWriter(C.GoString(sheet)) + if err != nil { + return C.struct_IntErrorResult{val: C.int(0), err: C.CString(err.Error())} + } + var swIdx int + sw.Range(func(_, _ interface{}) bool { + swIdx++ + return true + }) + swIdx++ + sw.Store(swIdx, streamWriter) + return C.struct_IntErrorResult{val: C.int(swIdx), err: C.CString(emptyString)} +} + +// StreamAddTable creates an Excel table for the StreamWriter using the given +// cell range and format set. +// +//export StreamAddTable +func StreamAddTable(swIdx int, table *C.struct_Table) *C.char { + var tbl excelize.Table + streamWriter, ok := sw.Load(swIdx) + if !ok { + return C.CString(errStreamWriterPtr) + } + goVal, err := cValueToGo(reflect.ValueOf(*table), reflect.TypeOf(excelize.Table{})) + if err != nil { + return C.CString(err.Error()) + } + tbl = goVal.Elem().Interface().(excelize.Table) + if err := streamWriter.(*excelize.StreamWriter).AddTable(&tbl); err != nil { + return C.CString(err.Error()) + } + return C.CString(emptyString) +} + +// StreamSetRow writes an array to stream rows by giving starting cell reference +// and a pointer to an array of values. Note that you must call the 'StreamFlush' +// function to end the streaming writing process. +// +//export StreamSetRow +func StreamSetRow(swIDx int, cell *C.char, row *C.struct_Interface, length int) *C.char { + streamWriter, ok := sw.Load(swIDx) + if !ok { + return C.CString(errStreamWriterPtr) + } + cells := make([]interface{}, length) + for i, val := range unsafe.Slice(row, length) { + cells[i] = cInterfaceToGo(val) + } + if err := streamWriter.(*excelize.StreamWriter).SetRow(C.GoString(cell), cells); err != nil { + return C.CString(err.Error()) + } + return C.CString(emptyString) +} + +// StreamFlush ending the streaming writing process. +// +//export StreamFlush +func StreamFlush(swIDx int, sheet *C.char) *C.char { + streamWriter, ok := sw.Load(swIDx) + if !ok { + return C.CString(errStreamWriterPtr) + } + if err := streamWriter.(*excelize.StreamWriter).Flush(); err != nil { + return C.CString(err.Error()) + } + return C.CString(emptyString) +} + // NewStyle provides a function to create the style for cells by given options. // Note that the color field uses RGB color code. // @@ -1641,7 +1727,7 @@ func SetCellBool(idx int, sheet, cell *C.char, value bool) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetCellBool(C.GoString(sheet), C.GoString(cell), value); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -1901,7 +1987,7 @@ func SetDefaultFont(idx int, fontName *C.char) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetDefaultFont(C.GoString(fontName)); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2020,7 +2106,7 @@ func SetRowHeight(idx int, sheet *C.char, row int, height float64) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetRowHeight(C.GoString(sheet), row, height); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2036,7 +2122,7 @@ func SetRowOutlineLevel(idx int, sheet *C.char, row, level int) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetRowOutlineLevel(C.GoString(sheet), row, uint8(level)); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2052,7 +2138,7 @@ func SetRowStyle(idx int, sheet *C.char, start, end, styleID int) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetRowStyle(C.GoString(sheet), start, end, styleID); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2067,7 +2153,7 @@ func SetRowVisible(idx int, sheet *C.char, row int, visible bool) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetRowVisible(C.GoString(sheet), row, visible); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2083,7 +2169,7 @@ func SetSheetBackground(idx int, sheet, picture *C.char) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetSheetBackground(C.GoString(sheet), C.GoString(picture)); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2100,7 +2186,7 @@ func SetSheetBackgroundFromBytes(idx int, sheet, extension *C.char, picture *C.u } buf := C.GoBytes(unsafe.Pointer(picture), pictureLen) if err := f.(*excelize.File).SetSheetBackgroundFromBytes(C.GoString(sheet), C.GoString(extension), buf); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2119,7 +2205,7 @@ func SetSheetCol(idx int, sheet, cell *C.char, slice *C.struct_Interface, length cells[i] = cInterfaceToGo(val) } if err := f.(*excelize.File).SetSheetCol(C.GoString(sheet), C.GoString(cell), &cells); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2137,7 +2223,7 @@ func SetSheetDimension(idx int, sheet, rangeRef *C.char) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetSheetDimension(C.GoString(sheet), C.GoString(rangeRef)); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2155,7 +2241,7 @@ func SetSheetName(idx int, source, target *C.char) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetSheetName(C.GoString(source), C.GoString(target)); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2174,7 +2260,7 @@ func SetSheetProps(idx int, sheet *C.char, opts *C.struct_SheetPropsOptions) *C. } options := goVal.Elem().Interface().(excelize.SheetPropsOptions) if err := f.(*excelize.File).SetSheetProps(C.GoString(sheet), &options); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2194,7 +2280,7 @@ func SetSheetRow(idx int, sheet, cell *C.char, row *C.struct_Interface, length i cells[i] = cInterfaceToGo(val) } if err := f.(*excelize.File).SetSheetRow(C.GoString(sheet), C.GoString(cell), &cells); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2214,7 +2300,7 @@ func SetSheetView(idx int, sheet *C.char, viewIndex int, opts *C.struct_ViewOpti } options := goVal.Elem().Interface().(excelize.ViewOptions) if err := f.(*excelize.File).SetSheetView(C.GoString(sheet), viewIndex, &options); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } @@ -2231,7 +2317,7 @@ func SetSheetVisible(idx int, sheet *C.char, visible, veryHidden bool) *C.char { return C.CString(errFilePtr) } if err := f.(*excelize.File).SetSheetVisible(C.GoString(sheet), visible, veryHidden); err != nil { - C.CString(err.Error()) + return C.CString(err.Error()) } return C.CString(emptyString) } diff --git a/test_excelize.py b/test_excelize.py index 47ce756..ca2f57c 100644 --- a/test_excelize.py +++ b/test_excelize.py @@ -77,6 +77,31 @@ def test_default_font(self): self.assertEqual(val, font_name) self.assertIsNone(err) + def test_stream_writer(self): + f = excelize.new_file() + _, err = f.new_stream_writer("SheetN") + self.assertEqual(str(err), "sheet SheetN does not exist") + sw, err = f.new_stream_writer("Sheet1") + self.assertIsNone(err) + self.assertIsNone(sw.set_row("A1", ["Column1", "Column2", "Column3"])) + for r in range(4, 11): + row = [random.randrange(640000) for _ in range(1, 4)] + cell, err = excelize.coordinates_to_cell_name(1, r, False) + self.assertIsNone(err) + self.assertIsNone(sw.set_row(cell, row)) + + self.assertIsNone( + sw.add_table( + excelize.Table( + name="Table1", + range="A1:C3", + ), + ) + ) + + self.assertIsNone(sw.flush()) + self.assertIsNone(f.save_as(os.path.join("test", "TestStreamWriter.xlsx"))) + def test_style(self): f = excelize.new_file() s = excelize.Style( diff --git a/types_c.h b/types_c.h index 4d895bc..82e0025 100644 --- a/types_c.h +++ b/types_c.h @@ -82,6 +82,25 @@ struct AppProperties char *AppVersion; }; +// Cell can be used directly in StreamWriter.SetRow to specify a style and +// a value. +struct Cell +{ + int StyleID; + char *Formula; + struct Interface Value; +}; + +// RowOpts define the options for the set row, it can be used directly in +// StreamWriter.SetRow to specify the style and properties of the row. +struct RowOpts +{ + double Height; + bool Hidden; + int StyleID; + int OutlineLevel; +}; + // Border directly maps the border settings of the cells. struct Border { diff --git a/types_go.py b/types_go.py index 35e8e04..ec07614 100644 --- a/types_go.py +++ b/types_go.py @@ -60,6 +60,23 @@ class _AppProperties(Structure): ] +class _Cell(Structure): + _fields_ = [ + ("StyleID", c_int), + ("Formula", c_char_p), + ("Value", _Interface), + ] + + +class _RowOpts(Structure): + _fields_ = [ + ("Height", c_double), + ("Hidden", c_bool), + ("StyleID", c_int), + ("OutlineLevel", c_int), + ] + + class _Border(Structure): _fields_ = [ ("Type", c_char_p), diff --git a/types_py.py b/types_py.py index e9edb8a..a5acfbd 100644 --- a/types_py.py +++ b/types_py.py @@ -205,6 +205,21 @@ class AppProperties: app_version: str = "" +@dataclass +class Cell: + style_id: int = 0 + formula: str = "" + value: Optional[Interface] = None + + +@dataclass +class RowOpts: + height: float = 0 + hidden: bool = False + style_id: int = 0 + outline_level: int = 0 + + @dataclass class Border: type: str = ""