about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/openpyxl
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/openpyxl')
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/_constants.py13
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py136
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/cell.py332
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py136
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py202
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/cell/text.py184
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py105
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py19
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py199
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py106
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/axis.py401
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py144
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/bubble_chart.py67
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py195
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py246
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/descriptors.py43
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/error_bar.py62
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/label.py127
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/layout.py74
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/legend.py75
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py129
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/marker.py90
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/picture.py35
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py177
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py65
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py162
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/print_settings.py57
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/radar_chart.py55
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/reader.py32
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/reference.py124
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/scatter_chart.py53
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/series.py197
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/series_factory.py41
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/shapes.py89
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/stock_chart.py54
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/surface_chart.py119
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/text.py78
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/title.py76
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/trendline.py98
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chart/updown_bars.py31
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/chartsheet.py107
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/custom.py61
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/properties.py28
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/protection.py41
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/publish.py58
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/relation.py97
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/chartsheet/views.py51
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/comments/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/comments/author.py21
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/comments/comment_sheet.py211
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/comments/comments.py62
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/comments/shape_writer.py112
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/__init__.py54
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/abc.py8
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/numbers.py43
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/product.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/singleton.py40
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/compat/strings.py25
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/__init__.py58
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/base.py272
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/container.py41
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/excel.py112
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/namespace.py12
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/nested.py129
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/sequence.py136
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/serialisable.py240
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/descriptors/slots.py18
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/colors.py435
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/connector.py144
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/drawing.py92
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/effect.py407
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/fill.py425
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/geometry.py584
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/graphic.py177
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/image.py65
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/line.py144
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/picture.py144
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/properties.py174
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/relation.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/spreadsheet_drawing.py382
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/text.py717
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/drawing/xdr.py33
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formatting/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formatting/formatting.py114
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formatting/rule.py291
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formula/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formula/tokenizer.py446
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py166
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/core.py115
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/custom.py289
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/extended.py137
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/interface.py56
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/manifest.py194
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/relationship.py158
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/packaging/workbook.py185
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/pivot/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/pivot/cache.py965
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/pivot/fields.py326
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/pivot/record.py111
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/pivot/table.py1261
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/reader/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/reader/drawings.py71
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/reader/excel.py349
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/reader/strings.py44
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/reader/workbook.py133
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/__init__.py11
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/alignment.py62
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/borders.py103
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/builtins.py1397
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/cell_style.py206
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/colors.py172
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/differential.py95
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/fills.py224
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/fonts.py113
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/named_styles.py282
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/numbers.py200
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/protection.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/proxy.py62
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/styleable.py151
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/stylesheet.py274
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/styles/table.py94
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py26
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py240
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py87
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py140
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py43
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py34
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py24
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py49
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py60
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py22
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/utils/units.py108
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/__init__.py4
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/_writer.py197
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/child.py166
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/defined_name.py189
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/external.py190
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/external_reference.py18
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/function_group.py36
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/properties.py151
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/protection.py163
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/smart_tags.py56
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/views.py155
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/web.py98
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/workbook/workbook.py438
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/_read_only.py190
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/_reader.py472
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/_write_only.py160
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/_writer.py390
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_range.py512
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_watch.py34
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/controls.py107
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/copier.py70
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/custom.py35
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/datavalidation.py202
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/dimensions.py306
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/drawing.py14
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/errors.py93
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/filters.py486
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/formula.py51
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/header_footer.py270
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/hyperlink.py46
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/merge.py141
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/ole.py133
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/page.py174
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/pagebreak.py94
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/picture.py8
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/print_settings.py184
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/properties.py97
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/protection.py120
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/related.py17
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/scenario.py105
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/smart_tag.py78
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/table.py385
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/views.py155
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/worksheet/worksheet.py907
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/writer/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/writer/excel.py295
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/writer/theme.py291
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/xml/__init__.py42
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/xml/constants.py129
-rw-r--r--.venv/lib/python3.12/site-packages/openpyxl/xml/functions.py87
190 files changed, 29137 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/__init__.py
new file mode 100644
index 00000000..14e84323
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2010-2024 openpyxl
+
+DEBUG = False
+
+from openpyxl.compat.numbers import NUMPY
+from openpyxl.xml import DEFUSEDXML, LXML
+from openpyxl.workbook import Workbook
+from openpyxl.reader.excel import load_workbook as open
+from openpyxl.reader.excel import load_workbook
+import openpyxl._constants as constants
+
+# Expose constants especially the version number
+
+__author__ = constants.__author__
+__author_email__ = constants.__author_email__
+__license__ = constants.__license__
+__maintainer_email__ = constants.__maintainer_email__
+__url__ = constants.__url__
+__version__ = constants.__version__
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/_constants.py b/.venv/lib/python3.12/site-packages/openpyxl/_constants.py
new file mode 100644
index 00000000..e7ff6b94
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/_constants.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Package metadata
+"""
+
+__author__ = "See AUTHORS"
+__author_email__ = "charlie.clark@clark-consulting.eu"
+__license__ = "MIT"
+__maintainer_email__ = "openpyxl-users@googlegroups.com"
+__url__ = "https://openpyxl.readthedocs.io"
+__version__ = "3.1.5"
+__python__ = "3.8"
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/__init__.py
new file mode 100644
index 00000000..0c1ca3ff
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .cell import Cell, WriteOnlyCell, MergedCell
+from .read_only import ReadOnlyCell
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py
new file mode 100644
index 00000000..4a27d680
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import Element, SubElement, whitespace, XML_NS
+from openpyxl import LXML
+from openpyxl.utils.datetime import to_excel, to_ISO8601
+from datetime import timedelta
+
+from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
+from openpyxl.cell.rich_text import CellRichText
+
+def _set_attributes(cell, styled=None):
+    """
+    Set coordinate and datatype
+    """
+    coordinate = cell.coordinate
+    attrs = {'r': coordinate}
+    if styled:
+        attrs['s'] = f"{cell.style_id}"
+
+    if cell.data_type == "s":
+        attrs['t'] = "inlineStr"
+    elif cell.data_type != 'f':
+        attrs['t'] = cell.data_type
+
+    value = cell._value
+
+    if cell.data_type == "d":
+        if hasattr(value, "tzinfo") and value.tzinfo is not None:
+            raise TypeError("Excel does not support timezones in datetimes. "
+                    "The tzinfo in the datetime/time object must be set to None.")
+
+        if cell.parent.parent.iso_dates and not isinstance(value, timedelta):
+            value = to_ISO8601(value)
+        else:
+            attrs['t'] = "n"
+            value = to_excel(value, cell.parent.parent.epoch)
+
+    if cell.hyperlink:
+        cell.parent._hyperlinks.append(cell.hyperlink)
+
+    return value, attrs
+
+
+def etree_write_cell(xf, worksheet, cell, styled=None):
+
+    value, attributes = _set_attributes(cell, styled)
+
+    el = Element("c", attributes)
+    if value is None or value == "":
+        xf.write(el)
+        return
+
+    if cell.data_type == 'f':
+        attrib = {}
+
+        if isinstance(value, ArrayFormula):
+            attrib = dict(value)
+            value = value.text
+
+        elif isinstance(value, DataTableFormula):
+            attrib = dict(value)
+            value = None
+
+        formula = SubElement(el, 'f', attrib)
+        if value is not None and not attrib.get('t') == "dataTable":
+            formula.text = value[1:]
+            value = None
+
+    if cell.data_type == 's':
+        if isinstance(value, CellRichText):
+            el.append(value.to_tree())
+        else:
+            inline_string = Element("is")
+            text = Element('t')
+            text.text = value
+            whitespace(text)
+            inline_string.append(text)
+            el.append(inline_string)
+
+    else:
+        cell_content = SubElement(el, 'v')
+        if value is not None:
+            cell_content.text = safe_string(value)
+
+    xf.write(el)
+
+
+def lxml_write_cell(xf, worksheet, cell, styled=False):
+    value, attributes = _set_attributes(cell, styled)
+
+    if value == '' or value is None:
+        with xf.element("c", attributes):
+            return
+
+    with xf.element('c', attributes):
+        if cell.data_type == 'f':
+            attrib = {}
+
+            if isinstance(value, ArrayFormula):
+                attrib = dict(value)
+                value = value.text
+
+            elif isinstance(value, DataTableFormula):
+                attrib = dict(value)
+                value = None
+
+            with xf.element('f', attrib):
+                if value is not None and not attrib.get('t') == "dataTable":
+                    xf.write(value[1:])
+                    value = None
+
+        if cell.data_type == 's':
+            if isinstance(value, CellRichText):
+                el = value.to_tree()
+                xf.write(el)
+            else:
+                with xf.element("is"):
+                    if isinstance(value, str):
+                        attrs = {}
+                        if value != value.strip():
+                            attrs["{%s}space" % XML_NS] = "preserve"
+                        el = Element("t", attrs) # lxml can't handle xml-ns
+                        el.text = value
+                        xf.write(el)
+
+        else:
+            with xf.element("v"):
+                if value is not None:
+                    xf.write(safe_string(value))
+
+
+if LXML:
+    write_cell = lxml_write_cell
+else:
+    write_cell = etree_write_cell
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/cell.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/cell.py
new file mode 100644
index 00000000..d29be280
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/cell.py
@@ -0,0 +1,332 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Manage individual cells in a spreadsheet.
+
+The Cell class is required to know its value and type, display options,
+and any other features of an Excel cell.  Utilities for referencing
+cells using Excel's 'A1' column/row nomenclature are also provided.
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+# Python stdlib imports
+from copy import copy
+import datetime
+import re
+
+
+from openpyxl.compat import (
+    NUMERIC_TYPES,
+)
+
+from openpyxl.utils.exceptions import IllegalCharacterError
+
+from openpyxl.utils import get_column_letter
+from openpyxl.styles import numbers, is_date_format
+from openpyxl.styles.styleable import StyleableObject
+from openpyxl.worksheet.hyperlink import Hyperlink
+from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
+from openpyxl.cell.rich_text import CellRichText
+
+# constants
+
+TIME_TYPES = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
+TIME_FORMATS = {
+    datetime.datetime:numbers.FORMAT_DATE_DATETIME,
+    datetime.date:numbers.FORMAT_DATE_YYYYMMDD2,
+    datetime.time:numbers.FORMAT_DATE_TIME6,
+    datetime.timedelta:numbers.FORMAT_DATE_TIMEDELTA,
+                }
+
+STRING_TYPES = (str, bytes, CellRichText)
+KNOWN_TYPES = NUMERIC_TYPES + TIME_TYPES + STRING_TYPES + (bool, type(None))
+
+ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
+ERROR_CODES = ('#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!',
+               '#N/A')
+
+TYPE_STRING = 's'
+TYPE_FORMULA = 'f'
+TYPE_NUMERIC = 'n'
+TYPE_BOOL = 'b'
+TYPE_NULL = 'n'
+TYPE_INLINE = 'inlineStr'
+TYPE_ERROR = 'e'
+TYPE_FORMULA_CACHE_STRING = 'str'
+
+VALID_TYPES = (TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
+               TYPE_NULL, TYPE_INLINE, TYPE_ERROR, TYPE_FORMULA_CACHE_STRING)
+
+
+_TYPES = {int:'n', float:'n', str:'s', bool:'b'}
+
+
+def get_type(t, value):
+    if isinstance(value, NUMERIC_TYPES):
+        dt = 'n'
+    elif isinstance(value, STRING_TYPES):
+        dt = 's'
+    elif isinstance(value, TIME_TYPES):
+        dt = 'd'
+    elif isinstance(value, (DataTableFormula, ArrayFormula)):
+        dt = 'f'
+    else:
+        return
+    _TYPES[t] = dt
+    return dt
+
+
+def get_time_format(t):
+    value = TIME_FORMATS.get(t)
+    if value:
+        return value
+    for base in t.mro()[1:]:
+        value = TIME_FORMATS.get(base)
+        if value:
+            TIME_FORMATS[t] = value
+            return value
+    raise ValueError("Could not get time format for {0!r}".format(value))
+
+
+class Cell(StyleableObject):
+    """Describes cell associated properties.
+
+    Properties of interest include style, type, value, and address.
+
+    """
+    __slots__ = (
+        'row',
+        'column',
+        '_value',
+        'data_type',
+        'parent',
+        '_hyperlink',
+        '_comment',
+                 )
+
+    def __init__(self, worksheet, row=None, column=None, value=None, style_array=None):
+        super().__init__(worksheet, style_array)
+        self.row = row
+        """Row number of this cell (1-based)"""
+        self.column = column
+        """Column number of this cell (1-based)"""
+        # _value is the stored value, while value is the displayed value
+        self._value = None
+        self._hyperlink = None
+        self.data_type = 'n'
+        if value is not None:
+            self.value = value
+        self._comment = None
+
+
+    @property
+    def coordinate(self):
+        """This cell's coordinate (ex. 'A5')"""
+        col = get_column_letter(self.column)
+        return f"{col}{self.row}"
+
+
+    @property
+    def col_idx(self):
+        """The numerical index of the column"""
+        return self.column
+
+
+    @property
+    def column_letter(self):
+        return get_column_letter(self.column)
+
+
+    @property
+    def encoding(self):
+        return self.parent.encoding
+
+    @property
+    def base_date(self):
+        return self.parent.parent.epoch
+
+
+    def __repr__(self):
+        return "<Cell {0!r}.{1}>".format(self.parent.title, self.coordinate)
+
+    def check_string(self, value):
+        """Check string coding, length, and line break character"""
+        if value is None:
+            return
+        # convert to str string
+        if not isinstance(value, str):
+            value = str(value, self.encoding)
+        value = str(value)
+        # string must never be longer than 32,767 characters
+        # truncate if necessary
+        value = value[:32767]
+        if next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
+            raise IllegalCharacterError(f"{value} cannot be used in worksheets.")
+        return value
+
+    def check_error(self, value):
+        """Tries to convert Error" else N/A"""
+        try:
+            return str(value)
+        except UnicodeDecodeError:
+            return u'#N/A'
+
+
+    def _bind_value(self, value):
+        """Given a value, infer the correct data type"""
+
+        self.data_type = "n"
+        t = type(value)
+        try:
+            dt = _TYPES[t]
+        except KeyError:
+            dt = get_type(t, value)
+
+        if dt is None and value is not None:
+            raise ValueError("Cannot convert {0!r} to Excel".format(value))
+
+        if dt:
+            self.data_type = dt
+
+        if dt == 'd':
+            if not is_date_format(self.number_format):
+                self.number_format = get_time_format(t)
+
+        elif dt == "s" and not isinstance(value, CellRichText):
+            value = self.check_string(value)
+            if len(value) > 1 and value.startswith("="):
+                self.data_type = 'f'
+            elif value in ERROR_CODES:
+                self.data_type = 'e'
+
+        self._value = value
+
+
+    @property
+    def value(self):
+        """Get or set the value held in the cell.
+
+        :type: depends on the value (string, float, int or
+            :class:`datetime.datetime`)
+        """
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        """Set the value and infer type and display options."""
+        self._bind_value(value)
+
+    @property
+    def internal_value(self):
+        """Always returns the value for excel."""
+        return self._value
+
+    @property
+    def hyperlink(self):
+        """Return the hyperlink target or an empty string"""
+        return self._hyperlink
+
+
+    @hyperlink.setter
+    def hyperlink(self, val):
+        """Set value and display for hyperlinks in a cell.
+        Automatically sets the `value` of the cell with link text,
+        but you can modify it afterwards by setting the `value`
+        property, and the hyperlink will remain.
+        Hyperlink is removed if set to ``None``."""
+        if val is None:
+            self._hyperlink = None
+        else:
+            if not isinstance(val, Hyperlink):
+                val = Hyperlink(ref="", target=val)
+            val.ref = self.coordinate
+            self._hyperlink = val
+            if self._value is None:
+                self.value = val.target or val.location
+
+
+    @property
+    def is_date(self):
+        """True if the value is formatted as a date
+
+        :type: bool
+        """
+        return self.data_type == 'd' or (
+            self.data_type == 'n' and is_date_format(self.number_format)
+            )
+
+
+    def offset(self, row=0, column=0):
+        """Returns a cell location relative to this cell.
+
+        :param row: number of rows to offset
+        :type row: int
+
+        :param column: number of columns to offset
+        :type column: int
+
+        :rtype: :class:`openpyxl.cell.Cell`
+        """
+        offset_column = self.col_idx + column
+        offset_row = self.row + row
+        return self.parent.cell(column=offset_column, row=offset_row)
+
+
+    @property
+    def comment(self):
+        """ Returns the comment associated with this cell
+
+            :type: :class:`openpyxl.comments.Comment`
+        """
+        return self._comment
+
+
+    @comment.setter
+    def comment(self, value):
+        """
+        Assign a comment to a cell
+        """
+
+        if value is not None:
+            if value.parent:
+                value = copy(value)
+            value.bind(self)
+        elif value is None and self._comment:
+            self._comment.unbind()
+        self._comment = value
+
+
+class MergedCell(StyleableObject):
+
+    """
+    Describes the properties of a cell in a merged cell and helps to
+    display the borders of the merged cell.
+
+    The value of a MergedCell is always None.
+    """
+
+    __slots__ = ('row', 'column')
+
+    _value = None
+    data_type = "n"
+    comment = None
+    hyperlink = None
+
+
+    def __init__(self, worksheet, row=None, column=None):
+        super().__init__(worksheet)
+        self.row = row
+        self.column = column
+
+
+    def __repr__(self):
+        return "<MergedCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
+
+    coordinate = Cell.coordinate
+    _comment = comment
+    value = _value
+
+
+def WriteOnlyCell(ws=None, value=None):
+    return Cell(worksheet=ws, column=1, row=1, value=value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py
new file mode 100644
index 00000000..2eec09e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.cell import Cell
+from openpyxl.utils import get_column_letter
+from openpyxl.utils.datetime import from_excel
+from openpyxl.styles import is_date_format
+from openpyxl.styles.numbers import BUILTIN_FORMATS, BUILTIN_FORMATS_MAX_SIZE
+
+
+class ReadOnlyCell:
+
+    __slots__ =  ('parent', 'row', 'column', '_value', 'data_type', '_style_id')
+
+    def __init__(self, sheet, row, column, value, data_type='n', style_id=0):
+        self.parent = sheet
+        self._value = None
+        self.row = row
+        self.column = column
+        self.data_type = data_type
+        self.value = value
+        self._style_id = style_id
+
+
+    def __eq__(self, other):
+        for a in self.__slots__:
+            if getattr(self, a) != getattr(other, a):
+                return
+        return True
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+    def __repr__(self):
+        return "<ReadOnlyCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
+
+
+    @property
+    def coordinate(self):
+        column = get_column_letter(self.column)
+        return "{1}{0}".format(self.row, column)
+
+
+    @property
+    def coordinate(self):
+        return Cell.coordinate.__get__(self)
+
+
+    @property
+    def column_letter(self):
+        return Cell.column_letter.__get__(self)
+
+
+    @property
+    def style_array(self):
+        return self.parent.parent._cell_styles[self._style_id]
+
+
+    @property
+    def has_style(self):
+        return self._style_id != 0
+
+
+    @property
+    def number_format(self):
+        _id = self.style_array.numFmtId
+        if _id < BUILTIN_FORMATS_MAX_SIZE:
+            return BUILTIN_FORMATS.get(_id, "General")
+        else:
+            return self.parent.parent._number_formats[
+                _id - BUILTIN_FORMATS_MAX_SIZE]
+
+    @property
+    def font(self):
+        _id = self.style_array.fontId
+        return self.parent.parent._fonts[_id]
+
+    @property
+    def fill(self):
+        _id = self.style_array.fillId
+        return self.parent.parent._fills[_id]
+
+    @property
+    def border(self):
+        _id = self.style_array.borderId
+        return self.parent.parent._borders[_id]
+
+    @property
+    def alignment(self):
+        _id = self.style_array.alignmentId
+        return self.parent.parent._alignments[_id]
+
+    @property
+    def protection(self):
+        _id = self.style_array.protectionId
+        return self.parent.parent._protections[_id]
+
+
+    @property
+    def is_date(self):
+        return Cell.is_date.__get__(self)
+
+
+    @property
+    def internal_value(self):
+        return self._value
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        if self._value is not None:
+            raise AttributeError("Cell is read only")
+        self._value = value
+
+
+class EmptyCell:
+
+    __slots__ = ()
+
+    value = None
+    is_date = False
+    font = None
+    border = None
+    fill = None
+    number_format = None
+    alignment = None
+    data_type = 'n'
+
+
+    def __repr__(self):
+        return "<EmptyCell>"
+
+EMPTY_CELL = EmptyCell()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py
new file mode 100644
index 00000000..373e263e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py
@@ -0,0 +1,202 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+RichText definition
+"""
+from copy import copy
+from openpyxl.compat import NUMERIC_TYPES
+from openpyxl.cell.text import InlineFont, Text
+from openpyxl.descriptors import (
+    Strict,
+    String,
+    Typed
+)
+
+from openpyxl.xml.functions import Element, whitespace
+
+class TextBlock(Strict):
+    """ Represents text string in a specific format
+
+    This class is used as part of constructing a rich text strings.
+    """
+    font = Typed(expected_type=InlineFont)
+    text = String()
+
+    def __init__(self, font, text):
+        self.font = font
+        self.text = text
+
+
+    def __eq__(self, other):
+        return self.text == other.text and self.font == other.font
+
+
+    def __str__(self):
+        """Just retun the text"""
+        return self.text
+
+
+    def __repr__(self):
+        font = self.font != InlineFont() and self.font or "default"
+        return f"{self.__class__.__name__} text={self.text}, font={font}"
+
+
+    def to_tree(self):
+        el = Element("r")
+        el.append(self.font.to_tree(tagname="rPr"))
+        t = Element("t")
+        t.text = self.text
+        whitespace(t)
+        el.append(t)
+        return el
+
+#
+# Rich Text class.
+# This class behaves just like a list whose members are either simple strings, or TextBlock() instances.
+# In addition, it can be initialized in several ways:
+# t = CellRFichText([...]) # initialize with a list.
+# t = CellRFichText((...)) # initialize with a tuple.
+# t = CellRichText(node) # where node is an Element() from either lxml or xml.etree (has a 'tag' element)
+class CellRichText(list):
+    """Represents a rich text string.
+
+    Initialize with a list made of pure strings or :class:`TextBlock` elements
+    Can index object to access or modify individual rich text elements
+    it also supports the + and += operators between rich text strings
+    There are no user methods for this class
+
+    operations which modify the string will generally call an optimization pass afterwards,
+    that merges text blocks with identical formats, consecutive pure text strings,
+    and remove empty strings and empty text blocks
+    """
+
+    def __init__(self, *args):
+        if len(args) == 1:
+            args = args[0]
+            if isinstance(args, (list, tuple)):
+                CellRichText._check_rich_text(args)
+            else:
+                CellRichText._check_element(args)
+                args = [args]
+        else:
+            CellRichText._check_rich_text(args)
+        super().__init__(args)
+
+
+    @classmethod
+    def _check_element(cls, value):
+        if not isinstance(value, (str, TextBlock, NUMERIC_TYPES)):
+            raise TypeError(f"Illegal CellRichText element {value}")
+
+
+    @classmethod
+    def _check_rich_text(cls, rich_text):
+        for t in rich_text:
+            CellRichText._check_element(t)
+
+    @classmethod
+    def from_tree(cls, node):
+        text = Text.from_tree(node)
+        if text.t:
+            return (text.t.replace('x005F_', ''),)
+        s = []
+        for r in text.r:
+            t = ""
+            if r.t:
+                t = r.t.replace('x005F_', '')
+            if r.rPr:
+                s.append(TextBlock(r.rPr, t))
+            else:
+                s.append(t)
+        return cls(s)
+
+    # Merge TextBlocks with identical formatting
+    # remove empty elements
+    def _opt(self):
+        last_t = None
+        l = CellRichText(tuple())
+        for t in self:
+            if isinstance(t, str):
+                if not t:
+                    continue
+            elif not t.text:
+                continue
+            if type(last_t) == type(t):
+                if isinstance(t, str):
+                    last_t += t
+                    continue
+                elif last_t.font == t.font:
+                    last_t.text += t.text
+                    continue
+            if last_t:
+                l.append(last_t)
+            last_t = t
+        if last_t:
+            # Add remaining TextBlock at end of rich text
+            l.append(last_t)
+        super().__setitem__(slice(None), l)
+        return self
+
+
+    def __iadd__(self, arg):
+        # copy used here to create new TextBlock() so we don't modify the right hand side in _opt()
+        CellRichText._check_rich_text(arg)
+        super().__iadd__([copy(e) for e in list(arg)])
+        return self._opt()
+
+
+    def __add__(self, arg):
+        return CellRichText([copy(e) for e in list(self) + list(arg)])._opt()
+
+
+    def __setitem__(self, indx, val):
+        CellRichText._check_element(val)
+        super().__setitem__(indx, val)
+        self._opt()
+
+
+    def append(self, arg):
+        CellRichText._check_element(arg)
+        super().append(arg)
+
+
+    def extend(self, arg):
+        CellRichText._check_rich_text(arg)
+        super().extend(arg)
+
+
+    def __repr__(self):
+        return "CellRichText([{}])".format(', '.join((repr(s) for s in self)))
+
+
+    def __str__(self):
+        return ''.join([str(s) for s in self])
+
+
+    def as_list(self):
+        """
+        Returns a list of the strings contained.
+        The main reason for this is to make editing easier.
+        """
+        return [str(s) for s in self]
+
+
+    def to_tree(self):
+        """
+        Return the full XML representation
+        """
+        container = Element("is")
+        for obj in self:
+            if isinstance(obj, TextBlock):
+                container.append(obj.to_tree())
+
+            else:
+                el = Element("r")
+                t = Element("t")
+                t.text = obj
+                whitespace(t)
+                el.append(t)
+                container.append(el)
+
+        return container
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/cell/text.py b/.venv/lib/python3.12/site-packages/openpyxl/cell/text.py
new file mode 100644
index 00000000..54923dd8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/cell/text.py
@@ -0,0 +1,184 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Richtext definition
+"""
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    Integer,
+    Set,
+    NoneSet,
+    Bool,
+    String,
+    Sequence,
+)
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedInteger,
+    NestedString,
+    NestedText,
+)
+from openpyxl.styles.fonts import Font
+
+
+class PhoneticProperties(Serialisable):
+
+    tagname = "phoneticPr"
+
+    fontId = Integer()
+    type = NoneSet(values=(['halfwidthKatakana', 'fullwidthKatakana',
+                            'Hiragana', 'noConversion']))
+    alignment = NoneSet(values=(['noControl', 'left', 'center', 'distributed']))
+
+    def __init__(self,
+                 fontId=None,
+                 type=None,
+                 alignment=None,
+                ):
+        self.fontId = fontId
+        self.type = type
+        self.alignment = alignment
+
+
+class PhoneticText(Serialisable):
+
+    tagname = "rPh"
+
+    sb = Integer()
+    eb = Integer()
+    t = NestedText(expected_type=str)
+    text = Alias('t')
+
+    def __init__(self,
+                 sb=None,
+                 eb=None,
+                 t=None,
+                ):
+        self.sb = sb
+        self.eb = eb
+        self.t = t
+
+
+class InlineFont(Font):
+
+    """
+    Font for inline text because, yes what you need are different objects with the same elements but different constraints.
+    """
+
+    tagname = "RPrElt"
+
+    rFont = NestedString(allow_none=True)
+    charset = Font.charset
+    family = Font.family
+    b =Font.b
+    i = Font.i
+    strike = Font.strike
+    outline = Font.outline
+    shadow = Font.shadow
+    condense = Font.condense
+    extend = Font.extend
+    color = Font.color
+    sz = Font.sz
+    u = Font.u
+    vertAlign = Font.vertAlign
+    scheme = Font.scheme
+
+    __elements__ = ('rFont', 'charset', 'family', 'b', 'i', 'strike',
+                    'outline', 'shadow', 'condense', 'extend', 'color', 'sz', 'u',
+                    'vertAlign', 'scheme')
+
+    def __init__(self,
+                 rFont=None,
+                 charset=None,
+                 family=None,
+                 b=None,
+                 i=None,
+                 strike=None,
+                 outline=None,
+                 shadow=None,
+                 condense=None,
+                 extend=None,
+                 color=None,
+                 sz=None,
+                 u=None,
+                 vertAlign=None,
+                 scheme=None,
+                ):
+        self.rFont = rFont
+        self.charset = charset
+        self.family = family
+        self.b = b
+        self.i = i
+        self.strike = strike
+        self.outline = outline
+        self.shadow = shadow
+        self.condense = condense
+        self.extend = extend
+        self.color = color
+        self.sz = sz
+        self.u = u
+        self.vertAlign = vertAlign
+        self.scheme = scheme
+
+
+class RichText(Serialisable):
+
+    tagname = "RElt"
+
+    rPr = Typed(expected_type=InlineFont, allow_none=True)
+    font = Alias("rPr")
+    t = NestedText(expected_type=str, allow_none=True)
+    text = Alias("t")
+
+    __elements__ = ('rPr', 't')
+
+    def __init__(self,
+                 rPr=None,
+                 t=None,
+                ):
+        self.rPr = rPr
+        self.t = t
+
+
+class Text(Serialisable):
+
+    tagname = "text"
+
+    t = NestedText(allow_none=True, expected_type=str)
+    plain = Alias("t")
+    r = Sequence(expected_type=RichText, allow_none=True)
+    formatted = Alias("r")
+    rPh = Sequence(expected_type=PhoneticText, allow_none=True)
+    phonetic = Alias("rPh")
+    phoneticPr = Typed(expected_type=PhoneticProperties, allow_none=True)
+    PhoneticProperties = Alias("phoneticPr")
+
+    __elements__ = ('t', 'r', 'rPh', 'phoneticPr')
+
+    def __init__(self,
+                 t=None,
+                 r=(),
+                 rPh=(),
+                 phoneticPr=None,
+                ):
+        self.t = t
+        self.r = r
+        self.rPh = rPh
+        self.phoneticPr = phoneticPr
+
+
+    @property
+    def content(self):
+        """
+        Text stripped of all formatting
+        """
+        snippets = []
+        if self.plain is not None:
+            snippets.append(self.plain)
+        for block in self.formatted:
+            if block.t is not None:
+                snippets.append(block.t)
+        return u"".join(snippets)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py
new file mode 100644
index 00000000..1651a993
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import Typed, Alias
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedInteger,
+    NestedMinMax,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from .marker import PictureOptions
+from .shapes import GraphicalProperties
+
+
+class View3D(Serialisable):
+
+    tagname = "view3D"
+
+    rotX = NestedMinMax(min=-90, max=90, allow_none=True)
+    x_rotation = Alias('rotX')
+    hPercent = NestedMinMax(min=5, max=500, allow_none=True)
+    height_percent = Alias('hPercent')
+    rotY = NestedInteger(min=-90, max=90, allow_none=True)
+    y_rotation = Alias('rotY')
+    depthPercent = NestedInteger(allow_none=True)
+    rAngAx = NestedBool(allow_none=True)
+    right_angle_axes = Alias('rAngAx')
+    perspective = NestedInteger(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('rotX', 'hPercent', 'rotY', 'depthPercent', 'rAngAx',
+                    'perspective',)
+
+    def __init__(self,
+                 rotX=15,
+                 hPercent=None,
+                 rotY=20,
+                 depthPercent=None,
+                 rAngAx=True,
+                 perspective=None,
+                 extLst=None,
+                ):
+        self.rotX = rotX
+        self.hPercent = hPercent
+        self.rotY = rotY
+        self.depthPercent = depthPercent
+        self.rAngAx = rAngAx
+        self.perspective = perspective
+
+
+class Surface(Serialisable):
+
+    tagname = "surface"
+
+    thickness = NestedInteger(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('thickness', 'spPr', 'pictureOptions',)
+
+    def __init__(self,
+                 thickness=None,
+                 spPr=None,
+                 pictureOptions=None,
+                 extLst=None,
+                ):
+        self.thickness = thickness
+        self.spPr = spPr
+        self.pictureOptions = pictureOptions
+
+
+class _3DBase(Serialisable):
+
+    """
+    Base class for 3D charts
+    """
+
+    tagname = "ChartBase"
+
+    view3D = Typed(expected_type=View3D, allow_none=True)
+    floor = Typed(expected_type=Surface, allow_none=True)
+    sideWall = Typed(expected_type=Surface, allow_none=True)
+    backWall = Typed(expected_type=Surface, allow_none=True)
+
+    def __init__(self,
+                 view3D=None,
+                 floor=None,
+                 sideWall=None,
+                 backWall=None,
+                 ):
+        if view3D is None:
+            view3D = View3D()
+        self.view3D = view3D
+        if floor is None:
+            floor = Surface()
+        self.floor = floor
+        if sideWall is None:
+            sideWall = Surface()
+        self.sideWall = sideWall
+        if backWall is None:
+            backWall = Surface()
+        self.backWall = backWall
+        super(_3DBase, self).__init__()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py
new file mode 100644
index 00000000..ecc4d8bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .area_chart import AreaChart, AreaChart3D
+from .bar_chart import BarChart, BarChart3D
+from .bubble_chart import BubbleChart
+from .line_chart import LineChart, LineChart3D
+from .pie_chart import (
+    PieChart,
+    PieChart3D,
+    DoughnutChart,
+    ProjectedPieChart
+)
+from .radar_chart import RadarChart
+from .scatter_chart import ScatterChart
+from .stock_chart import StockChart
+from .surface_chart import SurfaceChart, SurfaceChart3D
+
+from .series_factory import SeriesFactory as Series
+from .reference import Reference
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py
new file mode 100644
index 00000000..6a613546
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py
@@ -0,0 +1,199 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from collections import OrderedDict
+from operator import attrgetter
+
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Alias,
+    MinMax,
+    Bool,
+    Set,
+)
+from openpyxl.descriptors.sequence import ValueSequence
+from openpyxl.descriptors.serialisable import Serialisable
+
+from ._3d import _3DBase
+from .data_source import AxDataSource, NumRef
+from .layout import Layout
+from .legend import Legend
+from .reference import Reference
+from .series_factory import SeriesFactory
+from .series import attribute_mapping
+from .shapes import GraphicalProperties
+from .title import TitleDescriptor
+
+class AxId(Serialisable):
+
+    val = Integer()
+
+    def __init__(self, val):
+        self.val = val
+
+
+def PlotArea():
+    from .chartspace import PlotArea
+    return PlotArea()
+
+
+class ChartBase(Serialisable):
+
+    """
+    Base class for all charts
+    """
+
+    legend = Typed(expected_type=Legend, allow_none=True)
+    layout = Typed(expected_type=Layout, allow_none=True)
+    roundedCorners = Bool(allow_none=True)
+    axId = ValueSequence(expected_type=int)
+    visible_cells_only = Bool(allow_none=True)
+    display_blanks = Set(values=['span', 'gap', 'zero'])
+    graphical_properties = Typed(expected_type=GraphicalProperties, allow_none=True)
+
+    _series_type = ""
+    ser = ()
+    series = Alias('ser')
+    title = TitleDescriptor()
+    anchor = "E15" # default anchor position
+    width = 15 # in cm, approx 5 rows
+    height = 7.5 # in cm, approx 14 rows
+    _id = 1
+    _path = "/xl/charts/chart{0}.xml"
+    style = MinMax(allow_none=True, min=1, max=48)
+    mime_type = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
+    graphical_properties = Typed(expected_type=GraphicalProperties, allow_none=True) # mapped to chartspace
+
+    __elements__ = ()
+
+
+    def __init__(self, axId=(), **kw):
+        self._charts = [self]
+        self.title = None
+        self.layout = None
+        self.roundedCorners = None
+        self.legend = Legend()
+        self.graphical_properties = None
+        self.style = None
+        self.plot_area = PlotArea()
+        self.axId = axId
+        self.display_blanks = 'gap'
+        self.pivotSource = None
+        self.pivotFormats = ()
+        self.visible_cells_only = True
+        self.idx_base = 0
+        self.graphical_properties = None
+        super().__init__()
+
+
+    def __hash__(self):
+        """
+        Just need to check for identity
+        """
+        return id(self)
+
+    def __iadd__(self, other):
+        """
+        Combine the chart with another one
+        """
+        if not isinstance(other, ChartBase):
+            raise TypeError("Only other charts can be added")
+        self._charts.append(other)
+        return self
+
+
+    def to_tree(self, namespace=None, tagname=None, idx=None):
+        self.axId = [id for id in self._axes]
+        if self.ser is not None:
+            for s in self.ser:
+                s.__elements__ = attribute_mapping[self._series_type]
+        return super().to_tree(tagname, idx)
+
+
+    def _reindex(self):
+        """
+        Normalise and rebase series: sort by order and then rebase order
+
+        """
+        # sort data series in order and rebase
+        ds = sorted(self.series, key=attrgetter("order"))
+        for idx, s in enumerate(ds):
+            s.order = idx
+        self.series = ds
+
+
+    def _write(self):
+        from .chartspace import ChartSpace, ChartContainer
+        self.plot_area.layout = self.layout
+
+        idx_base = self.idx_base
+        for chart in self._charts:
+            if chart not in self.plot_area._charts:
+                chart.idx_base = idx_base
+                idx_base += len(chart.series)
+        self.plot_area._charts = self._charts
+
+        container = ChartContainer(plotArea=self.plot_area, legend=self.legend, title=self.title)
+        if isinstance(chart, _3DBase):
+            container.view3D = chart.view3D
+            container.floor = chart.floor
+            container.sideWall = chart.sideWall
+            container.backWall = chart.backWall
+        container.plotVisOnly = self.visible_cells_only
+        container.dispBlanksAs = self.display_blanks
+        container.pivotFmts = self.pivotFormats
+        cs = ChartSpace(chart=container)
+        cs.style = self.style
+        cs.roundedCorners = self.roundedCorners
+        cs.pivotSource = self.pivotSource
+        cs.spPr = self.graphical_properties
+        return cs.to_tree()
+
+
+    @property
+    def _axes(self):
+        x = getattr(self, "x_axis", None)
+        y = getattr(self, "y_axis", None)
+        z = getattr(self, "z_axis", None)
+        return OrderedDict([(axis.axId, axis) for axis in (x, y, z) if axis])
+
+
+    def set_categories(self, labels):
+        """
+        Set the categories / x-axis values
+        """
+        if not isinstance(labels, Reference):
+            labels = Reference(range_string=labels)
+        for s in self.ser:
+            s.cat = AxDataSource(numRef=NumRef(f=labels))
+
+
+    def add_data(self, data, from_rows=False, titles_from_data=False):
+        """
+        Add a range of data in a single pass.
+        The default is to treat each column as a data series.
+        """
+        if not isinstance(data, Reference):
+            data = Reference(range_string=data)
+
+        if from_rows:
+            values = data.rows
+
+        else:
+            values = data.cols
+
+        for ref in values:
+            series = SeriesFactory(ref, title_from_data=titles_from_data)
+            self.series.append(series)
+
+
+    def append(self, value):
+        """Append a data series to the chart"""
+        l = self.series[:]
+        l.append(value)
+        self.series = l
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py
new file mode 100644
index 00000000..d3d98085
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py
@@ -0,0 +1,106 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Set,
+    Bool,
+    Integer,
+    Sequence,
+    Alias,
+)
+
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedMinMax,
+    NestedSet,
+    NestedBool,
+)
+
+from ._chart import ChartBase
+from .descriptors import NestedGapAmount
+from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines
+from .label import DataLabelList
+from .series import Series
+
+
+class _AreaChartBase(ChartBase):
+
+    grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked']))
+    varyColors = NestedBool(nested=True, allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+    dropLines = Typed(expected_type=ChartLines, allow_none=True)
+
+    _series_type = "area"
+
+    __elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines')
+
+    def __init__(self,
+                 grouping="standard",
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 dropLines=None,
+                ):
+        self.grouping = grouping
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        self.dropLines = dropLines
+        super().__init__()
+
+
+class AreaChart(_AreaChartBase):
+
+    tagname = "areaChart"
+
+    grouping = _AreaChartBase.grouping
+    varyColors = _AreaChartBase.varyColors
+    ser = _AreaChartBase.ser
+    dLbls = _AreaChartBase.dLbls
+    dropLines = _AreaChartBase.dropLines
+
+    # chart properties actually used by containing classes
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _AreaChartBase.__elements__ + ('axId',)
+
+    def __init__(self,
+                 axId=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        super().__init__(**kw)
+
+
+class AreaChart3D(AreaChart):
+
+    tagname = "area3DChart"
+
+    grouping = _AreaChartBase.grouping
+    varyColors = _AreaChartBase.varyColors
+    ser = _AreaChartBase.ser
+    dLbls = _AreaChartBase.dLbls
+    dropLines = _AreaChartBase.dropLines
+
+    gapDepth = NestedGapAmount()
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+    z_axis = Typed(expected_type=SeriesAxis, allow_none=True)
+
+    __elements__ = AreaChart.__elements__ + ('gapDepth', )
+
+    def __init__(self, gapDepth=None, **kw):
+        self.gapDepth = gapDepth
+        super(AreaChart3D, self).__init__(**kw)
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        self.z_axis = SeriesAxis()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/axis.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/axis.py
new file mode 100644
index 00000000..7e99416c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/axis.py
@@ -0,0 +1,401 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Float,
+    NoneSet,
+    Bool,
+    Integer,
+    MinMax,
+    NoneSet,
+    Set,
+    String,
+    Alias,
+)
+
+from openpyxl.descriptors.excel import (
+    ExtensionList,
+    Percentage,
+    _explicit_none,
+)
+from openpyxl.descriptors.nested import (
+    NestedValue,
+    NestedSet,
+    NestedBool,
+    NestedNoneSet,
+    NestedFloat,
+    NestedInteger,
+    NestedMinMax,
+)
+from openpyxl.xml.constants import CHART_NS
+
+from .descriptors import NumberFormatDescriptor
+from .layout import Layout
+from .text import Text, RichText
+from .shapes import GraphicalProperties
+from .title import Title, TitleDescriptor
+
+
+class ChartLines(Serialisable):
+
+    tagname = "chartLines"
+
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+
+    def __init__(self, spPr=None):
+        self.spPr = spPr
+
+
+class Scaling(Serialisable):
+
+    tagname = "scaling"
+
+    logBase = NestedFloat(allow_none=True)
+    orientation = NestedSet(values=(['maxMin', 'minMax']))
+    max = NestedFloat(allow_none=True)
+    min = NestedFloat(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('logBase', 'orientation', 'max', 'min',)
+
+    def __init__(self,
+                 logBase=None,
+                 orientation="minMax",
+                 max=None,
+                 min=None,
+                 extLst=None,
+                ):
+        self.logBase = logBase
+        self.orientation = orientation
+        self.max = max
+        self.min = min
+
+
+class _BaseAxis(Serialisable):
+
+    axId = NestedInteger(expected_type=int)
+    scaling = Typed(expected_type=Scaling)
+    delete = NestedBool(allow_none=True)
+    axPos = NestedSet(values=(['b', 'l', 'r', 't']))
+    majorGridlines = Typed(expected_type=ChartLines, allow_none=True)
+    minorGridlines = Typed(expected_type=ChartLines, allow_none=True)
+    title = TitleDescriptor()
+    numFmt = NumberFormatDescriptor()
+    number_format = Alias("numFmt")
+    majorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none)
+    minorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none)
+    tickLblPos = NestedNoneSet(values=(['high', 'low', 'nextTo']))
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textProperties = Alias('txPr')
+    crossAx = NestedInteger(expected_type=int) # references other axis
+    crosses = NestedNoneSet(values=(['autoZero', 'max', 'min']))
+    crossesAt = NestedFloat(allow_none=True)
+
+    # crosses & crossesAt are mutually exclusive
+
+    __elements__ = ('axId', 'scaling', 'delete', 'axPos', 'majorGridlines',
+                    'minorGridlines', 'title', 'numFmt', 'majorTickMark', 'minorTickMark',
+                    'tickLblPos', 'spPr', 'txPr', 'crossAx', 'crosses', 'crossesAt')
+
+    def __init__(self,
+                 axId=None,
+                 scaling=None,
+                 delete=None,
+                 axPos='l',
+                 majorGridlines=None,
+                 minorGridlines=None,
+                 title=None,
+                 numFmt=None,
+                 majorTickMark=None,
+                 minorTickMark=None,
+                 tickLblPos=None,
+                 spPr=None,
+                 txPr= None,
+                 crossAx=None,
+                 crosses=None,
+                 crossesAt=None,
+                ):
+        self.axId = axId
+        if scaling is None:
+            scaling = Scaling()
+        self.scaling = scaling
+        self.delete = delete
+        self.axPos = axPos
+        self.majorGridlines = majorGridlines
+        self.minorGridlines = minorGridlines
+        self.title = title
+        self.numFmt = numFmt
+        self.majorTickMark = majorTickMark
+        self.minorTickMark = minorTickMark
+        self.tickLblPos = tickLblPos
+        self.spPr = spPr
+        self.txPr = txPr
+        self.crossAx = crossAx
+        self.crosses = crosses
+        self.crossesAt = crossesAt
+
+
+class DisplayUnitsLabel(Serialisable):
+
+    tagname = "dispUnitsLbl"
+
+    layout = Typed(expected_type=Layout, allow_none=True)
+    tx = Typed(expected_type=Text, allow_none=True)
+    text = Alias("tx")
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textPropertes = Alias("txPr")
+
+    __elements__ = ('layout', 'tx', 'spPr', 'txPr')
+
+    def __init__(self,
+                 layout=None,
+                 tx=None,
+                 spPr=None,
+                 txPr=None,
+                ):
+        self.layout = layout
+        self.tx = tx
+        self.spPr = spPr
+        self.txPr = txPr
+
+
+class DisplayUnitsLabelList(Serialisable):
+
+    tagname = "dispUnits"
+
+    custUnit = NestedFloat(allow_none=True)
+    builtInUnit = NestedNoneSet(values=(['hundreds', 'thousands',
+                                         'tenThousands', 'hundredThousands', 'millions', 'tenMillions',
+                                         'hundredMillions', 'billions', 'trillions']))
+    dispUnitsLbl = Typed(expected_type=DisplayUnitsLabel, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('custUnit', 'builtInUnit', 'dispUnitsLbl',)
+
+    def __init__(self,
+                 custUnit=None,
+                 builtInUnit=None,
+                 dispUnitsLbl=None,
+                 extLst=None,
+                ):
+        self.custUnit = custUnit
+        self.builtInUnit = builtInUnit
+        self.dispUnitsLbl = dispUnitsLbl
+
+
+class NumericAxis(_BaseAxis):
+
+    tagname = "valAx"
+
+    axId = _BaseAxis.axId
+    scaling = _BaseAxis.scaling
+    delete = _BaseAxis.delete
+    axPos = _BaseAxis.axPos
+    majorGridlines = _BaseAxis.majorGridlines
+    minorGridlines = _BaseAxis.minorGridlines
+    title = _BaseAxis.title
+    numFmt = _BaseAxis.numFmt
+    majorTickMark = _BaseAxis.majorTickMark
+    minorTickMark = _BaseAxis.minorTickMark
+    tickLblPos = _BaseAxis.tickLblPos
+    spPr = _BaseAxis.spPr
+    txPr = _BaseAxis.txPr
+    crossAx = _BaseAxis.crossAx
+    crosses = _BaseAxis.crosses
+    crossesAt = _BaseAxis.crossesAt
+
+    crossBetween = NestedNoneSet(values=(['between', 'midCat']))
+    majorUnit = NestedFloat(allow_none=True)
+    minorUnit = NestedFloat(allow_none=True)
+    dispUnits = Typed(expected_type=DisplayUnitsLabelList, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _BaseAxis.__elements__ + ('crossBetween', 'majorUnit',
+                                             'minorUnit', 'dispUnits',)
+
+
+    def __init__(self,
+                 crossBetween=None,
+                 majorUnit=None,
+                 minorUnit=None,
+                 dispUnits=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.crossBetween = crossBetween
+        self.majorUnit = majorUnit
+        self.minorUnit = minorUnit
+        self.dispUnits = dispUnits
+        kw.setdefault('majorGridlines', ChartLines())
+        kw.setdefault('axId', 100)
+        kw.setdefault('crossAx', 10)
+        super().__init__(**kw)
+
+
+    @classmethod
+    def from_tree(cls, node):
+        """
+        Special case value axes with no gridlines
+        """
+        self = super().from_tree(node)
+        gridlines = node.find("{%s}majorGridlines" % CHART_NS)
+        if gridlines is None:
+            self.majorGridlines = None
+        return self
+
+
+
+class TextAxis(_BaseAxis):
+
+    tagname = "catAx"
+
+    axId = _BaseAxis.axId
+    scaling = _BaseAxis.scaling
+    delete = _BaseAxis.delete
+    axPos = _BaseAxis.axPos
+    majorGridlines = _BaseAxis.majorGridlines
+    minorGridlines = _BaseAxis.minorGridlines
+    title = _BaseAxis.title
+    numFmt = _BaseAxis.numFmt
+    majorTickMark = _BaseAxis.majorTickMark
+    minorTickMark = _BaseAxis.minorTickMark
+    tickLblPos = _BaseAxis.tickLblPos
+    spPr = _BaseAxis.spPr
+    txPr = _BaseAxis.txPr
+    crossAx = _BaseAxis.crossAx
+    crosses = _BaseAxis.crosses
+    crossesAt = _BaseAxis.crossesAt
+
+    auto = NestedBool(allow_none=True)
+    lblAlgn = NestedNoneSet(values=(['ctr', 'l', 'r']))
+    lblOffset = NestedMinMax(min=0, max=1000)
+    tickLblSkip = NestedInteger(allow_none=True)
+    tickMarkSkip = NestedInteger(allow_none=True)
+    noMultiLvlLbl = NestedBool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _BaseAxis.__elements__ + ('auto', 'lblAlgn', 'lblOffset',
+                                             'tickLblSkip', 'tickMarkSkip', 'noMultiLvlLbl')
+
+    def __init__(self,
+                 auto=None,
+                 lblAlgn=None,
+                 lblOffset=100,
+                 tickLblSkip=None,
+                 tickMarkSkip=None,
+                 noMultiLvlLbl=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.auto = auto
+        self.lblAlgn = lblAlgn
+        self.lblOffset = lblOffset
+        self.tickLblSkip = tickLblSkip
+        self.tickMarkSkip = tickMarkSkip
+        self.noMultiLvlLbl = noMultiLvlLbl
+        kw.setdefault('axId', 10)
+        kw.setdefault('crossAx', 100)
+        super().__init__(**kw)
+
+
+class DateAxis(TextAxis):
+
+    tagname = "dateAx"
+
+    axId = _BaseAxis.axId
+    scaling = _BaseAxis.scaling
+    delete = _BaseAxis.delete
+    axPos = _BaseAxis.axPos
+    majorGridlines = _BaseAxis.majorGridlines
+    minorGridlines = _BaseAxis.minorGridlines
+    title = _BaseAxis.title
+    numFmt = _BaseAxis.numFmt
+    majorTickMark = _BaseAxis.majorTickMark
+    minorTickMark = _BaseAxis.minorTickMark
+    tickLblPos = _BaseAxis.tickLblPos
+    spPr = _BaseAxis.spPr
+    txPr = _BaseAxis.txPr
+    crossAx = _BaseAxis.crossAx
+    crosses = _BaseAxis.crosses
+    crossesAt = _BaseAxis.crossesAt
+
+    auto = NestedBool(allow_none=True)
+    lblOffset = NestedInteger(allow_none=True)
+    baseTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
+    majorUnit = NestedFloat(allow_none=True)
+    majorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
+    minorUnit = NestedFloat(allow_none=True)
+    minorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _BaseAxis.__elements__ + ('auto', 'lblOffset',
+                                             'baseTimeUnit', 'majorUnit', 'majorTimeUnit', 'minorUnit',
+                                             'minorTimeUnit')
+
+    def __init__(self,
+                 auto=None,
+                 lblOffset=None,
+                 baseTimeUnit=None,
+                 majorUnit=None,
+                 majorTimeUnit=None,
+                 minorUnit=None,
+                 minorTimeUnit=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.auto = auto
+        self.lblOffset = lblOffset
+        self.baseTimeUnit = baseTimeUnit
+        self.majorUnit = majorUnit
+        self.majorTimeUnit = majorTimeUnit
+        self.minorUnit = minorUnit
+        self.minorTimeUnit = minorTimeUnit
+        kw.setdefault('axId', 500)
+        kw.setdefault('lblOffset', lblOffset)
+        super().__init__(**kw)
+
+
+class SeriesAxis(_BaseAxis):
+
+    tagname = "serAx"
+
+    axId = _BaseAxis.axId
+    scaling = _BaseAxis.scaling
+    delete = _BaseAxis.delete
+    axPos = _BaseAxis.axPos
+    majorGridlines = _BaseAxis.majorGridlines
+    minorGridlines = _BaseAxis.minorGridlines
+    title = _BaseAxis.title
+    numFmt = _BaseAxis.numFmt
+    majorTickMark = _BaseAxis.majorTickMark
+    minorTickMark = _BaseAxis.minorTickMark
+    tickLblPos = _BaseAxis.tickLblPos
+    spPr = _BaseAxis.spPr
+    txPr = _BaseAxis.txPr
+    crossAx = _BaseAxis.crossAx
+    crosses = _BaseAxis.crosses
+    crossesAt = _BaseAxis.crossesAt
+
+    tickLblSkip = NestedInteger(allow_none=True)
+    tickMarkSkip = NestedInteger(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _BaseAxis.__elements__ + ('tickLblSkip', 'tickMarkSkip')
+
+    def __init__(self,
+                 tickLblSkip=None,
+                 tickMarkSkip=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.tickLblSkip = tickLblSkip
+        self.tickMarkSkip = tickMarkSkip
+        kw.setdefault('axId', 1000)
+        kw.setdefault('crossAx', 10)
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py
new file mode 100644
index 00000000..fa08e076
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py
@@ -0,0 +1,144 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    Integer,
+    Sequence,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedSet,
+    NestedBool,
+    NestedInteger,
+    NestedMinMax,
+)
+
+from .descriptors import (
+    NestedGapAmount,
+    NestedOverlap,
+)
+from ._chart import ChartBase
+from ._3d import _3DBase
+from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines
+from .shapes import GraphicalProperties
+from .series import Series
+from .legend import Legend
+from .label import DataLabelList
+
+
+class _BarChartBase(ChartBase):
+
+    barDir = NestedSet(values=(['bar', 'col']))
+    type = Alias("barDir")
+    grouping = NestedSet(values=(['percentStacked', 'clustered', 'standard',
+                                  'stacked']))
+    varyColors = NestedBool(nested=True, allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+
+    __elements__ = ('barDir', 'grouping', 'varyColors', 'ser', 'dLbls')
+
+    _series_type = "bar"
+
+    def __init__(self,
+                 barDir="col",
+                 grouping="clustered",
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 **kw
+                ):
+        self.barDir = barDir
+        self.grouping = grouping
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        super().__init__(**kw)
+
+
+class BarChart(_BarChartBase):
+
+    tagname = "barChart"
+
+    barDir = _BarChartBase.barDir
+    grouping = _BarChartBase.grouping
+    varyColors = _BarChartBase.varyColors
+    ser = _BarChartBase.ser
+    dLbls = _BarChartBase.dLbls
+
+    gapWidth = NestedGapAmount()
+    overlap = NestedOverlap()
+    serLines = Typed(expected_type=ChartLines, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    # chart properties actually used by containing classes
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    __elements__ = _BarChartBase.__elements__ + ('gapWidth', 'overlap', 'serLines', 'axId')
+
+    def __init__(self,
+                 gapWidth=150,
+                 overlap=None,
+                 serLines=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.gapWidth = gapWidth
+        self.overlap = overlap
+        self.serLines = serLines
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        self.legend = Legend()
+        super().__init__(**kw)
+
+
+class BarChart3D(_BarChartBase, _3DBase):
+
+    tagname = "bar3DChart"
+
+    barDir = _BarChartBase.barDir
+    grouping = _BarChartBase.grouping
+    varyColors = _BarChartBase.varyColors
+    ser = _BarChartBase.ser
+    dLbls = _BarChartBase.dLbls
+
+    view3D = _3DBase.view3D
+    floor = _3DBase.floor
+    sideWall = _3DBase.sideWall
+    backWall = _3DBase.backWall
+
+    gapWidth = NestedGapAmount()
+    gapDepth = NestedGapAmount()
+    shape = NestedNoneSet(values=(['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax']))
+    serLines = Typed(expected_type=ChartLines, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+    z_axis = Typed(expected_type=SeriesAxis, allow_none=True)
+
+    __elements__ = _BarChartBase.__elements__ + ('gapWidth', 'gapDepth', 'shape', 'serLines', 'axId')
+
+    def __init__(self,
+                 gapWidth=150,
+                 gapDepth=150,
+                 shape=None,
+                 serLines=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.gapWidth = gapWidth
+        self.gapDepth = gapDepth
+        self.shape = shape
+        self.serLines = serLines
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        self.z_axis = SeriesAxis()
+
+        super(BarChart3D, self).__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/bubble_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/bubble_chart.py
new file mode 100644
index 00000000..3fca043a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/bubble_chart.py
@@ -0,0 +1,67 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Set,
+    MinMax,
+    Bool,
+    Integer,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedMinMax,
+    NestedBool,
+)
+
+from ._chart import ChartBase
+from .axis import TextAxis, NumericAxis
+from .series import XYSeries
+from .label import DataLabelList
+
+
+class BubbleChart(ChartBase):
+
+    tagname = "bubbleChart"
+
+    varyColors = NestedBool(allow_none=True)
+    ser = Sequence(expected_type=XYSeries, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+    bubble3D = NestedBool(allow_none=True)
+    bubbleScale = NestedMinMax(min=0, max=300, allow_none=True)
+    showNegBubbles = NestedBool(allow_none=True)
+    sizeRepresents = NestedNoneSet(values=(['area', 'w']))
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=NumericAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    _series_type = "bubble"
+
+    __elements__ = ('varyColors', 'ser', 'dLbls', 'bubble3D', 'bubbleScale',
+                    'showNegBubbles', 'sizeRepresents', 'axId')
+
+    def __init__(self,
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 bubble3D=None,
+                 bubbleScale=None,
+                 showNegBubbles=None,
+                 sizeRepresents=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        self.bubble3D = bubble3D
+        self.bubbleScale = bubbleScale
+        self.showNegBubbles = showNegBubbles
+        self.sizeRepresents = sizeRepresents
+        self.x_axis = NumericAxis(axId=10, crossAx=20)
+        self.y_axis = NumericAxis(axId=20, crossAx=10)
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py
new file mode 100644
index 00000000..cba213c2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py
@@ -0,0 +1,195 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Enclosing chart object. The various chart types are actually child objects.
+Will probably need to call this indirectly
+"""
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.excel import (
+    ExtensionList,
+    Relation
+)
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedNoneSet,
+    NestedString,
+    NestedMinMax,
+)
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.xml.constants import CHART_NS
+
+from openpyxl.drawing.colors import ColorMapping
+from .text import RichText
+from .shapes import GraphicalProperties
+from .legend import Legend
+from ._3d import _3DBase
+from .plotarea import PlotArea
+from .title import Title
+from .pivot import (
+    PivotFormat,
+    PivotSource,
+)
+from .print_settings import PrintSettings
+
+
+class ChartContainer(Serialisable):
+
+    tagname = "chart"
+
+    title = Typed(expected_type=Title, allow_none=True)
+    autoTitleDeleted = NestedBool(allow_none=True)
+    pivotFmts = NestedSequence(expected_type=PivotFormat)
+    view3D = _3DBase.view3D
+    floor = _3DBase.floor
+    sideWall = _3DBase.sideWall
+    backWall = _3DBase.backWall
+    plotArea = Typed(expected_type=PlotArea, )
+    legend = Typed(expected_type=Legend, allow_none=True)
+    plotVisOnly = NestedBool()
+    dispBlanksAs = NestedNoneSet(values=(['span', 'gap', 'zero']))
+    showDLblsOverMax = NestedBool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('title', 'autoTitleDeleted', 'pivotFmts', 'view3D',
+                    'floor', 'sideWall', 'backWall', 'plotArea', 'legend', 'plotVisOnly',
+                    'dispBlanksAs', 'showDLblsOverMax')
+
+    def __init__(self,
+                 title=None,
+                 autoTitleDeleted=None,
+                 pivotFmts=(),
+                 view3D=None,
+                 floor=None,
+                 sideWall=None,
+                 backWall=None,
+                 plotArea=None,
+                 legend=None,
+                 plotVisOnly=True,
+                 dispBlanksAs="gap",
+                 showDLblsOverMax=None,
+                 extLst=None,
+                ):
+        self.title = title
+        self.autoTitleDeleted = autoTitleDeleted
+        self.pivotFmts = pivotFmts
+        self.view3D = view3D
+        self.floor = floor
+        self.sideWall = sideWall
+        self.backWall = backWall
+        if plotArea is None:
+            plotArea = PlotArea()
+        self.plotArea = plotArea
+        self.legend = legend
+        self.plotVisOnly = plotVisOnly
+        self.dispBlanksAs = dispBlanksAs
+        self.showDLblsOverMax = showDLblsOverMax
+
+
+class Protection(Serialisable):
+
+    tagname = "protection"
+
+    chartObject = NestedBool(allow_none=True)
+    data = NestedBool(allow_none=True)
+    formatting = NestedBool(allow_none=True)
+    selection = NestedBool(allow_none=True)
+    userInterface = NestedBool(allow_none=True)
+
+    __elements__ = ("chartObject", "data", "formatting", "selection", "userInterface")
+
+    def __init__(self,
+                 chartObject=None,
+                 data=None,
+                 formatting=None,
+                 selection=None,
+                 userInterface=None,
+                ):
+        self.chartObject = chartObject
+        self.data = data
+        self.formatting = formatting
+        self.selection = selection
+        self.userInterface = userInterface
+
+
+class ExternalData(Serialisable):
+
+    tagname = "externalData"
+
+    autoUpdate = NestedBool(allow_none=True)
+    id = String() # Needs namespace
+
+    def __init__(self,
+                 autoUpdate=None,
+                 id=None
+                ):
+        self.autoUpdate = autoUpdate
+        self.id = id
+
+
+class ChartSpace(Serialisable):
+
+    tagname = "chartSpace"
+
+    date1904 = NestedBool(allow_none=True)
+    lang = NestedString(allow_none=True)
+    roundedCorners = NestedBool(allow_none=True)
+    style = NestedMinMax(allow_none=True, min=1, max=48)
+    clrMapOvr = Typed(expected_type=ColorMapping, allow_none=True)
+    pivotSource = Typed(expected_type=PivotSource, allow_none=True)
+    protection = Typed(expected_type=Protection, allow_none=True)
+    chart = Typed(expected_type=ChartContainer)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphical_properties = Alias("spPr")
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textProperties = Alias("txPr")
+    externalData = Typed(expected_type=ExternalData, allow_none=True)
+    printSettings = Typed(expected_type=PrintSettings, allow_none=True)
+    userShapes = Relation()
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('date1904', 'lang', 'roundedCorners', 'style',
+                    'clrMapOvr', 'pivotSource', 'protection', 'chart', 'spPr', 'txPr',
+                    'externalData', 'printSettings', 'userShapes')
+
+    def __init__(self,
+                 date1904=None,
+                 lang=None,
+                 roundedCorners=None,
+                 style=None,
+                 clrMapOvr=None,
+                 pivotSource=None,
+                 protection=None,
+                 chart=None,
+                 spPr=None,
+                 txPr=None,
+                 externalData=None,
+                 printSettings=None,
+                 userShapes=None,
+                 extLst=None,
+                ):
+        self.date1904 = date1904
+        self.lang = lang
+        self.roundedCorners = roundedCorners
+        self.style = style
+        self.clrMapOvr = clrMapOvr
+        self.pivotSource = pivotSource
+        self.protection = protection
+        self.chart = chart
+        self.spPr = spPr
+        self.txPr = txPr
+        self.externalData = externalData
+        self.printSettings = printSettings
+        self.userShapes = userShapes
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        tree = super().to_tree()
+        tree.set("xmlns", CHART_NS)
+        return tree
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py
new file mode 100644
index 00000000..c38eafb2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py
@@ -0,0 +1,246 @@
+"""
+Collection of utility primitives for charts.
+"""
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Bool,
+    Typed,
+    Alias,
+    String,
+    Integer,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedString,
+    NestedText,
+    NestedInteger,
+)
+
+
+class NumFmt(Serialisable):
+
+    formatCode = String()
+    sourceLinked = Bool()
+
+    def __init__(self,
+                 formatCode=None,
+                 sourceLinked=False
+                ):
+        self.formatCode = formatCode
+        self.sourceLinked = sourceLinked
+
+
+class NumberValueDescriptor(NestedText):
+    """
+    Data should be numerical but isn't always :-/
+    """
+
+    allow_none = True
+
+    def __set__(self, instance, value):
+        if value == "#N/A":
+            self.expected_type = str
+        else:
+            self.expected_type = float
+        super().__set__(instance, value)
+
+
+class NumVal(Serialisable):
+
+    idx = Integer()
+    formatCode = NestedText(allow_none=True, expected_type=str)
+    v = NumberValueDescriptor()
+
+    def __init__(self,
+                 idx=None,
+                 formatCode=None,
+                 v=None,
+                ):
+        self.idx = idx
+        self.formatCode = formatCode
+        self.v = v
+
+
+class NumData(Serialisable):
+
+    formatCode = NestedText(expected_type=str, allow_none=True)
+    ptCount = NestedInteger(allow_none=True)
+    pt = Sequence(expected_type=NumVal)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('formatCode', 'ptCount', 'pt')
+
+    def __init__(self,
+                 formatCode=None,
+                 ptCount=None,
+                 pt=(),
+                 extLst=None,
+                ):
+        self.formatCode = formatCode
+        self.ptCount = ptCount
+        self.pt = pt
+
+
+class NumRef(Serialisable):
+
+    f = NestedText(expected_type=str)
+    ref = Alias('f')
+    numCache = Typed(expected_type=NumData, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('f', 'numCache')
+
+    def __init__(self,
+                 f=None,
+                 numCache=None,
+                 extLst=None,
+                ):
+        self.f = f
+        self.numCache = numCache
+
+
+class StrVal(Serialisable):
+
+    tagname = "strVal"
+
+    idx = Integer()
+    v = NestedText(expected_type=str)
+
+    def __init__(self,
+                 idx=0,
+                 v=None,
+                ):
+        self.idx = idx
+        self.v = v
+
+
+class StrData(Serialisable):
+
+    tagname = "strData"
+
+    ptCount = NestedInteger(allow_none=True)
+    pt = Sequence(expected_type=StrVal)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('ptCount', 'pt')
+
+    def __init__(self,
+                 ptCount=None,
+                 pt=(),
+                 extLst=None,
+                ):
+        self.ptCount = ptCount
+        self.pt = pt
+
+
+class StrRef(Serialisable):
+
+    tagname = "strRef"
+
+    f = NestedText(expected_type=str, allow_none=True)
+    strCache = Typed(expected_type=StrData, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('f', 'strCache')
+
+    def __init__(self,
+                 f=None,
+                 strCache=None,
+                 extLst=None,
+                ):
+        self.f = f
+        self.strCache = strCache
+
+
+class NumDataSource(Serialisable):
+
+    numRef = Typed(expected_type=NumRef, allow_none=True)
+    numLit = Typed(expected_type=NumData, allow_none=True)
+
+
+    def __init__(self,
+                 numRef=None,
+                 numLit=None,
+                 ):
+        self.numRef = numRef
+        self.numLit = numLit
+
+
+class Level(Serialisable):
+
+    tagname = "lvl"
+
+    pt = Sequence(expected_type=StrVal)
+
+    __elements__ = ('pt',)
+
+    def __init__(self,
+                 pt=(),
+                ):
+        self.pt = pt
+
+
+class MultiLevelStrData(Serialisable):
+
+    tagname = "multiLvlStrData"
+
+    ptCount = Integer(allow_none=True)
+    lvl = Sequence(expected_type=Level)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('ptCount', 'lvl',)
+
+    def __init__(self,
+                 ptCount=None,
+                 lvl=(),
+                 extLst=None,
+                ):
+        self.ptCount = ptCount
+        self.lvl = lvl
+
+
+class MultiLevelStrRef(Serialisable):
+
+    tagname = "multiLvlStrRef"
+
+    f = NestedText(expected_type=str)
+    multiLvlStrCache = Typed(expected_type=MultiLevelStrData, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('multiLvlStrCache', 'f')
+
+    def __init__(self,
+                 f=None,
+                 multiLvlStrCache=None,
+                 extLst=None,
+                ):
+        self.f = f
+        self.multiLvlStrCache = multiLvlStrCache
+
+
+class AxDataSource(Serialisable):
+
+    tagname = "cat"
+
+    numRef = Typed(expected_type=NumRef, allow_none=True)
+    numLit = Typed(expected_type=NumData, allow_none=True)
+    strRef = Typed(expected_type=StrRef, allow_none=True)
+    strLit = Typed(expected_type=StrData, allow_none=True)
+    multiLvlStrRef = Typed(expected_type=MultiLevelStrRef, allow_none=True)
+
+    def __init__(self,
+                 numRef=None,
+                 numLit=None,
+                 strRef=None,
+                 strLit=None,
+                 multiLvlStrRef=None,
+                 ):
+        if not any([numLit, numRef, strRef, strLit, multiLvlStrRef]):
+            raise TypeError("A data source must be provided")
+        self.numRef = numRef
+        self.numLit = numLit
+        self.strRef = strRef
+        self.strLit = strLit
+        self.multiLvlStrRef = multiLvlStrRef
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/descriptors.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/descriptors.py
new file mode 100644
index 00000000..6bc94348
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/descriptors.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+
+from openpyxl.descriptors.nested import (
+    NestedMinMax
+    )
+
+from openpyxl.descriptors import Typed
+
+from .data_source import NumFmt
+
+"""
+Utility descriptors for the chart module.
+For convenience but also clarity.
+"""
+
+class NestedGapAmount(NestedMinMax):
+
+    allow_none = True
+    min = 0
+    max = 500
+
+
+class NestedOverlap(NestedMinMax):
+
+    allow_none = True
+    min = -100
+    max = 100
+
+
+class NumberFormatDescriptor(Typed):
+    """
+    Allow direct assignment of format code
+    """
+
+    expected_type = NumFmt
+    allow_none = True
+
+    def __set__(self, instance, value):
+        if isinstance(value, str):
+            value = NumFmt(value)
+        super().__set__(instance, value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/error_bar.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/error_bar.py
new file mode 100644
index 00000000..6ae24451
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/error_bar.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Float,
+    Set,
+    Alias
+)
+
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedSet,
+    NestedBool,
+    NestedFloat,
+)
+
+from .data_source import NumDataSource
+from .shapes import GraphicalProperties
+
+
+class ErrorBars(Serialisable):
+
+    tagname = "errBars"
+
+    errDir = NestedNoneSet(values=(['x', 'y']))
+    direction = Alias("errDir")
+    errBarType = NestedSet(values=(['both', 'minus', 'plus']))
+    style = Alias("errBarType")
+    errValType = NestedSet(values=(['cust', 'fixedVal', 'percentage', 'stdDev', 'stdErr']))
+    size = Alias("errValType")
+    noEndCap = NestedBool(nested=True, allow_none=True)
+    plus = Typed(expected_type=NumDataSource, allow_none=True)
+    minus = Typed(expected_type=NumDataSource, allow_none=True)
+    val = NestedFloat(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('errDir','errBarType', 'errValType', 'noEndCap','minus', 'plus', 'val', 'spPr')
+
+
+    def __init__(self,
+                 errDir=None,
+                 errBarType="both",
+                 errValType="fixedVal",
+                 noEndCap=None,
+                 plus=None,
+                 minus=None,
+                 val=None,
+                 spPr=None,
+                 extLst=None,
+                ):
+        self.errDir = errDir
+        self.errBarType = errBarType
+        self.errValType = errValType
+        self.noEndCap = noEndCap
+        self.plus = plus
+        self.minus = minus
+        self.val = val
+        self.spPr = spPr
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/label.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/label.py
new file mode 100644
index 00000000..d6eacb16
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/label.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    Alias,
+    Typed
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedBool,
+    NestedString,
+    NestedInteger,
+    )
+
+from .shapes import GraphicalProperties
+from .text import RichText
+
+
+class _DataLabelBase(Serialisable):
+
+    numFmt = NestedString(allow_none=True, attribute="formatCode")
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textProperties = Alias('txPr')
+    dLblPos = NestedNoneSet(values=['bestFit', 'b', 'ctr', 'inBase', 'inEnd',
+                                    'l', 'outEnd', 'r', 't'])
+    position = Alias('dLblPos')
+    showLegendKey = NestedBool(allow_none=True)
+    showVal = NestedBool(allow_none=True)
+    showCatName = NestedBool(allow_none=True)
+    showSerName = NestedBool(allow_none=True)
+    showPercent = NestedBool(allow_none=True)
+    showBubbleSize = NestedBool(allow_none=True)
+    showLeaderLines = NestedBool(allow_none=True)
+    separator = NestedString(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ("numFmt", "spPr", "txPr", "dLblPos", "showLegendKey",
+                    "showVal", "showCatName", "showSerName", "showPercent", "showBubbleSize",
+                    "showLeaderLines", "separator")
+
+    def __init__(self,
+                 numFmt=None,
+                 spPr=None,
+                 txPr=None,
+                 dLblPos=None,
+                 showLegendKey=None,
+                 showVal=None,
+                 showCatName=None,
+                 showSerName=None,
+                 showPercent=None,
+                 showBubbleSize=None,
+                 showLeaderLines=None,
+                 separator=None,
+                 extLst=None,
+                 ):
+        self.numFmt = numFmt
+        self.spPr = spPr
+        self.txPr = txPr
+        self.dLblPos = dLblPos
+        self.showLegendKey = showLegendKey
+        self.showVal = showVal
+        self.showCatName = showCatName
+        self.showSerName = showSerName
+        self.showPercent = showPercent
+        self.showBubbleSize = showBubbleSize
+        self.showLeaderLines = showLeaderLines
+        self.separator = separator
+
+
+class DataLabel(_DataLabelBase):
+
+    tagname = "dLbl"
+
+    idx = NestedInteger()
+
+    numFmt = _DataLabelBase.numFmt
+    spPr = _DataLabelBase.spPr
+    txPr = _DataLabelBase.txPr
+    dLblPos = _DataLabelBase.dLblPos
+    showLegendKey = _DataLabelBase.showLegendKey
+    showVal = _DataLabelBase.showVal
+    showCatName = _DataLabelBase.showCatName
+    showSerName = _DataLabelBase.showSerName
+    showPercent = _DataLabelBase.showPercent
+    showBubbleSize = _DataLabelBase.showBubbleSize
+    showLeaderLines = _DataLabelBase.showLeaderLines
+    separator = _DataLabelBase.separator
+    extLst = _DataLabelBase.extLst
+
+    __elements__ = ("idx",)  + _DataLabelBase.__elements__
+
+    def __init__(self, idx=0, **kw ):
+        self.idx = idx
+        super().__init__(**kw)
+
+
+class DataLabelList(_DataLabelBase):
+
+    tagname = "dLbls"
+
+    dLbl = Sequence(expected_type=DataLabel, allow_none=True)
+
+    delete = NestedBool(allow_none=True)
+    numFmt = _DataLabelBase.numFmt
+    spPr = _DataLabelBase.spPr
+    txPr = _DataLabelBase.txPr
+    dLblPos = _DataLabelBase.dLblPos
+    showLegendKey = _DataLabelBase.showLegendKey
+    showVal = _DataLabelBase.showVal
+    showCatName = _DataLabelBase.showCatName
+    showSerName = _DataLabelBase.showSerName
+    showPercent = _DataLabelBase.showPercent
+    showBubbleSize = _DataLabelBase.showBubbleSize
+    showLeaderLines = _DataLabelBase.showLeaderLines
+    separator = _DataLabelBase.separator
+    extLst = _DataLabelBase.extLst
+
+    __elements__ = ("delete", "dLbl",) + _DataLabelBase.__elements__
+
+    def __init__(self, dLbl=(), delete=None,  **kw):
+        self.dLbl = dLbl
+        self.delete = delete
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/layout.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/layout.py
new file mode 100644
index 00000000..f2f65530
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/layout.py
@@ -0,0 +1,74 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    NoneSet,
+    Float,
+    Typed,
+    Alias,
+)
+
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedSet,
+    NestedMinMax,
+)
+
+class ManualLayout(Serialisable):
+
+    tagname = "manualLayout"
+
+    layoutTarget = NestedNoneSet(values=(['inner', 'outer']))
+    xMode = NestedNoneSet(values=(['edge', 'factor']))
+    yMode = NestedNoneSet(values=(['edge', 'factor']))
+    wMode = NestedSet(values=(['edge', 'factor']))
+    hMode = NestedSet(values=(['edge', 'factor']))
+    x = NestedMinMax(min=-1, max=1, allow_none=True)
+    y = NestedMinMax(min=-1, max=1, allow_none=True)
+    w = NestedMinMax(min=0, max=1, allow_none=True)
+    width = Alias('w')
+    h = NestedMinMax(min=0, max=1,  allow_none=True)
+    height = Alias('h')
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('layoutTarget', 'xMode', 'yMode', 'wMode', 'hMode', 'x',
+                    'y', 'w', 'h')
+
+    def __init__(self,
+                 layoutTarget=None,
+                 xMode=None,
+                 yMode=None,
+                 wMode="factor",
+                 hMode="factor",
+                 x=None,
+                 y=None,
+                 w=None,
+                 h=None,
+                 extLst=None,
+                ):
+        self.layoutTarget = layoutTarget
+        self.xMode = xMode
+        self.yMode = yMode
+        self.wMode = wMode
+        self.hMode = hMode
+        self.x = x
+        self.y = y
+        self.w = w
+        self.h = h
+
+
+class Layout(Serialisable):
+
+    tagname = "layout"
+
+    manualLayout = Typed(expected_type=ManualLayout, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('manualLayout',)
+
+    def __init__(self,
+                 manualLayout=None,
+                 extLst=None,
+                ):
+        self.manualLayout = manualLayout
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/legend.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/legend.py
new file mode 100644
index 00000000..1f7c802b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/legend.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedSet,
+    NestedInteger
+)
+
+from .layout import Layout
+from .shapes import GraphicalProperties
+from .text import RichText
+
+
+class LegendEntry(Serialisable):
+
+    tagname = "legendEntry"
+
+    idx = NestedInteger()
+    delete = NestedBool()
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('idx', 'delete', 'txPr')
+
+    def __init__(self,
+                 idx=0,
+                 delete=False,
+                 txPr=None,
+                 extLst=None,
+                ):
+        self.idx = idx
+        self.delete = delete
+        self.txPr = txPr
+
+
+class Legend(Serialisable):
+
+    tagname = "legend"
+
+    legendPos = NestedSet(values=(['b', 'tr', 'l', 'r', 't']))
+    position = Alias('legendPos')
+    legendEntry = Sequence(expected_type=LegendEntry)
+    layout = Typed(expected_type=Layout, allow_none=True)
+    overlay = NestedBool(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textProperties = Alias('txPr')
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('legendPos', 'legendEntry', 'layout', 'overlay', 'spPr', 'txPr',)
+
+    def __init__(self,
+                 legendPos="r",
+                 legendEntry=(),
+                 layout=None,
+                 overlay=None,
+                 spPr=None,
+                 txPr=None,
+                 extLst=None,
+                ):
+        self.legendPos = legendPos
+        self.legendEntry = legendEntry
+        self.layout = layout
+        self.overlay = overlay
+        self.spPr = spPr
+        self.txPr = txPr
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py
new file mode 100644
index 00000000..0aa3ad5b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py
@@ -0,0 +1,129 @@
+#Autogenerated schema
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    Alias,
+    )
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedSet,
+    NestedBool,
+)
+
+from ._chart import ChartBase
+from .updown_bars import UpDownBars
+from .descriptors import NestedGapAmount
+from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines, _BaseAxis
+from .label import DataLabelList
+from .series import Series
+
+
+class _LineChartBase(ChartBase):
+
+    grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked']))
+    varyColors = NestedBool(allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+    dropLines = Typed(expected_type=ChartLines, allow_none=True)
+
+    _series_type = "line"
+
+    __elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines')
+
+    def __init__(self,
+                 grouping="standard",
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 dropLines=None,
+                 **kw
+                ):
+        self.grouping = grouping
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        self.dropLines = dropLines
+        super().__init__(**kw)
+
+
+class LineChart(_LineChartBase):
+
+    tagname = "lineChart"
+
+    grouping = _LineChartBase.grouping
+    varyColors = _LineChartBase.varyColors
+    ser = _LineChartBase.ser
+    dLbls = _LineChartBase.dLbls
+    dropLines =_LineChartBase.dropLines
+
+    hiLowLines = Typed(expected_type=ChartLines, allow_none=True)
+    upDownBars = Typed(expected_type=UpDownBars, allow_none=True)
+    marker = NestedBool(allow_none=True)
+    smooth = NestedBool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=_BaseAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    __elements__ = _LineChartBase.__elements__ + ('hiLowLines', 'upDownBars', 'marker', 'smooth', 'axId')
+
+    def __init__(self,
+                 hiLowLines=None,
+                 upDownBars=None,
+                 marker=None,
+                 smooth=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.hiLowLines = hiLowLines
+        self.upDownBars = upDownBars
+        self.marker = marker
+        self.smooth = smooth
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+
+        super().__init__(**kw)
+
+
+class LineChart3D(_LineChartBase):
+
+    tagname = "line3DChart"
+
+    grouping = _LineChartBase.grouping
+    varyColors = _LineChartBase.varyColors
+    ser = _LineChartBase.ser
+    dLbls = _LineChartBase.dLbls
+    dropLines =_LineChartBase.dropLines
+
+    gapDepth = NestedGapAmount()
+    hiLowLines = Typed(expected_type=ChartLines, allow_none=True)
+    upDownBars = Typed(expected_type=UpDownBars, allow_none=True)
+    marker = NestedBool(allow_none=True)
+    smooth = NestedBool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+    z_axis = Typed(expected_type=SeriesAxis)
+
+    __elements__ = _LineChartBase.__elements__ + ('gapDepth', 'hiLowLines',
+                                                  'upDownBars', 'marker', 'smooth', 'axId')
+
+    def __init__(self,
+                 gapDepth=None,
+                 hiLowLines=None,
+                 upDownBars=None,
+                 marker=None,
+                 smooth=None,
+                 **kw
+                ):
+        self.gapDepth = gapDepth
+        self.hiLowLines = hiLowLines
+        self.upDownBars = upDownBars
+        self.marker = marker
+        self.smooth = smooth
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        self.z_axis = SeriesAxis()
+        super(LineChart3D, self).__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/marker.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/marker.py
new file mode 100644
index 00000000..61e2641d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/marker.py
@@ -0,0 +1,90 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Alias,
+)
+
+from openpyxl.descriptors.excel import(
+    ExtensionList,
+    _explicit_none,
+)
+
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedInteger,
+    NestedMinMax,
+    NestedNoneSet,
+)
+
+from .layout import Layout
+from .picture import PictureOptions
+from .shapes import *
+from .text import *
+from .error_bar import *
+
+
+class Marker(Serialisable):
+
+    tagname = "marker"
+
+    symbol = NestedNoneSet(values=(['circle', 'dash', 'diamond', 'dot', 'picture',
+                              'plus', 'square', 'star', 'triangle', 'x', 'auto']),
+                           to_tree=_explicit_none)
+    size = NestedMinMax(min=2, max=72, allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('symbol', 'size', 'spPr')
+
+    def __init__(self,
+                 symbol=None,
+                 size=None,
+                 spPr=None,
+                 extLst=None,
+                ):
+        self.symbol = symbol
+        self.size = size
+        if spPr is None:
+            spPr = GraphicalProperties()
+        self.spPr = spPr
+
+
+class DataPoint(Serialisable):
+
+    tagname = "dPt"
+
+    idx = NestedInteger()
+    invertIfNegative = NestedBool(allow_none=True)
+    marker = Typed(expected_type=Marker, allow_none=True)
+    bubble3D = NestedBool(allow_none=True)
+    explosion = NestedInteger(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('idx', 'invertIfNegative', 'marker', 'bubble3D',
+                    'explosion', 'spPr', 'pictureOptions')
+
+    def __init__(self,
+                 idx=None,
+                 invertIfNegative=None,
+                 marker=None,
+                 bubble3D=None,
+                 explosion=None,
+                 spPr=None,
+                 pictureOptions=None,
+                 extLst=None,
+                ):
+        self.idx = idx
+        self.invertIfNegative = invertIfNegative
+        self.marker = marker
+        self.bubble3D = bubble3D
+        self.explosion = explosion
+        if spPr is None:
+            spPr = GraphicalProperties()
+        self.spPr = spPr
+        self.pictureOptions = pictureOptions
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/picture.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/picture.py
new file mode 100644
index 00000000..8c917d8c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/picture.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedFloat,
+    NestedMinMax,
+    NestedNoneSet,
+)
+
+class PictureOptions(Serialisable):
+
+    tagname = "pictureOptions"
+
+    applyToFront = NestedBool(allow_none=True, nested=True)
+    applyToSides = NestedBool(allow_none=True, nested=True)
+    applyToEnd = NestedBool(allow_none=True, nested=True)
+    pictureFormat = NestedNoneSet(values=(['stretch', 'stack', 'stackScale']), nested=True)
+    pictureStackUnit = NestedFloat(allow_none=True, nested=True)
+
+    __elements__ = ('applyToFront', 'applyToSides', 'applyToEnd', 'pictureFormat', 'pictureStackUnit')
+
+    def __init__(self,
+                 applyToFront=None,
+                 applyToSides=None,
+                 applyToEnd=None,
+                 pictureFormat=None,
+                 pictureStackUnit=None,
+                ):
+        self.applyToFront = applyToFront
+        self.applyToSides = applyToSides
+        self.applyToEnd = applyToEnd
+        self.pictureFormat = pictureFormat
+        self.pictureStackUnit = pictureStackUnit
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py
new file mode 100644
index 00000000..6bb67e1e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py
@@ -0,0 +1,177 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    MinMax,
+    Integer,
+    NoneSet,
+    Float,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList, Percentage
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedMinMax,
+    NestedInteger,
+    NestedFloat,
+    NestedNoneSet,
+    NestedSet,
+)
+from openpyxl.descriptors.sequence import ValueSequence
+
+from ._chart import ChartBase
+from .axis import ChartLines
+from .descriptors import NestedGapAmount
+from .series import Series
+from .label import DataLabelList
+
+
+class _PieChartBase(ChartBase):
+
+    varyColors = NestedBool(allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+
+    _series_type = "pie"
+
+    __elements__ = ('varyColors', 'ser', 'dLbls')
+
+    def __init__(self,
+                 varyColors=True,
+                 ser=(),
+                 dLbls=None,
+                ):
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        super().__init__()
+
+
+
+class PieChart(_PieChartBase):
+
+    tagname = "pieChart"
+
+    varyColors = _PieChartBase.varyColors
+    ser = _PieChartBase.ser
+    dLbls = _PieChartBase.dLbls
+
+    firstSliceAng = NestedMinMax(min=0, max=360)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _PieChartBase.__elements__ + ('firstSliceAng', )
+
+    def __init__(self,
+                 firstSliceAng=0,
+                 extLst=None,
+                 **kw
+                ):
+        self.firstSliceAng = firstSliceAng
+        super().__init__(**kw)
+
+
+class PieChart3D(_PieChartBase):
+
+    tagname = "pie3DChart"
+
+    varyColors = _PieChartBase.varyColors
+    ser = _PieChartBase.ser
+    dLbls = _PieChartBase.dLbls
+
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _PieChartBase.__elements__
+
+
+class DoughnutChart(_PieChartBase):
+
+    tagname = "doughnutChart"
+
+    varyColors = _PieChartBase.varyColors
+    ser = _PieChartBase.ser
+    dLbls = _PieChartBase.dLbls
+
+    firstSliceAng = NestedMinMax(min=0, max=360)
+    holeSize = NestedMinMax(min=1, max=90, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _PieChartBase.__elements__ + ('firstSliceAng', 'holeSize')
+
+    def __init__(self,
+                 firstSliceAng=0,
+                 holeSize=10,
+                 extLst=None,
+                 **kw
+                ):
+        self.firstSliceAng = firstSliceAng
+        self.holeSize = holeSize
+        super().__init__(**kw)
+
+
+class CustomSplit(Serialisable):
+
+    tagname = "custSplit"
+
+    secondPiePt = ValueSequence(expected_type=int)
+
+    __elements__ = ('secondPiePt',)
+
+    def __init__(self,
+                 secondPiePt=(),
+                ):
+        self.secondPiePt = secondPiePt
+
+
+class ProjectedPieChart(_PieChartBase):
+
+    """
+    From the spec 21.2.2.126
+
+    This element contains the pie of pie or bar of pie series on this
+    chart. Only the first series shall be displayed. The splitType element
+    shall determine whether the splitPos and custSplit elements apply.
+    """
+
+    tagname = "ofPieChart"
+
+    varyColors = _PieChartBase.varyColors
+    ser = _PieChartBase.ser
+    dLbls = _PieChartBase.dLbls
+
+    ofPieType = NestedSet(values=(['pie', 'bar']))
+    type = Alias('ofPieType')
+    gapWidth = NestedGapAmount()
+    splitType = NestedNoneSet(values=(['auto', 'cust', 'percent', 'pos', 'val']))
+    splitPos = NestedFloat(allow_none=True)
+    custSplit = Typed(expected_type=CustomSplit, allow_none=True)
+    secondPieSize = NestedMinMax(min=5, max=200, allow_none=True)
+    serLines = Typed(expected_type=ChartLines, allow_none=True)
+    join_lines = Alias('serLines')
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = _PieChartBase.__elements__ + ('ofPieType', 'gapWidth',
+                                                 'splitType', 'splitPos', 'custSplit', 'secondPieSize', 'serLines')
+
+    def __init__(self,
+                 ofPieType="pie",
+                 gapWidth=None,
+                 splitType="auto",
+                 splitPos=None,
+                 custSplit=None,
+                 secondPieSize=75,
+                 serLines=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.ofPieType = ofPieType
+        self.gapWidth = gapWidth
+        self.splitType = splitType
+        self.splitPos = splitPos
+        self.custSplit = custSplit
+        self.secondPieSize = secondPieSize
+        if serLines is None:
+            self.serLines = ChartLines()
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py
new file mode 100644
index 00000000..937fd294
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py
@@ -0,0 +1,65 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+)
+from openpyxl.descriptors.nested import NestedInteger, NestedText
+from openpyxl.descriptors.excel import ExtensionList
+
+from .label import DataLabel
+from .marker import Marker
+from .shapes import GraphicalProperties
+from .text import RichText
+
+
+class PivotSource(Serialisable):
+
+    tagname = "pivotSource"
+
+    name = NestedText(expected_type=str)
+    fmtId = NestedInteger(expected_type=int)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('name', 'fmtId')
+
+    def __init__(self,
+                 name=None,
+                 fmtId=None,
+                 extLst=None,
+                ):
+        self.name = name
+        self.fmtId = fmtId
+
+
+class PivotFormat(Serialisable):
+
+    tagname = "pivotFmt"
+
+    idx = NestedInteger(nested=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    TextBody = Alias("txPr")
+    marker = Typed(expected_type=Marker, allow_none=True)
+    dLbl = Typed(expected_type=DataLabel, allow_none=True)
+    DataLabel = Alias("dLbl")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('idx', 'spPr', 'txPr', 'marker', 'dLbl')
+
+    def __init__(self,
+                 idx=0,
+                 spPr=None,
+                 txPr=None,
+                 marker=None,
+                 dLbl=None,
+                 extLst=None,
+                ):
+        self.idx = idx
+        self.spPr = spPr
+        self.txPr = txPr
+        self.marker = marker
+        self.dLbl = dLbl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py
new file mode 100644
index 00000000..268bfbc4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py
@@ -0,0 +1,162 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Alias,
+)
+from openpyxl.descriptors.excel import (
+    ExtensionList,
+)
+from openpyxl.descriptors.sequence import (
+    MultiSequence,
+    MultiSequencePart,
+)
+from openpyxl.descriptors.nested import (
+    NestedBool,
+)
+
+from ._3d import _3DBase
+from .area_chart import AreaChart, AreaChart3D
+from .bar_chart import BarChart, BarChart3D
+from .bubble_chart import BubbleChart
+from .line_chart import LineChart, LineChart3D
+from .pie_chart import PieChart, PieChart3D, ProjectedPieChart, DoughnutChart
+from .radar_chart import RadarChart
+from .scatter_chart import ScatterChart
+from .stock_chart import StockChart
+from .surface_chart import SurfaceChart, SurfaceChart3D
+from .layout import Layout
+from .shapes import GraphicalProperties
+from .text import RichText
+
+from .axis import (
+    NumericAxis,
+    TextAxis,
+    SeriesAxis,
+    DateAxis,
+)
+
+
+class DataTable(Serialisable):
+
+    tagname = "dTable"
+
+    showHorzBorder = NestedBool(allow_none=True)
+    showVertBorder = NestedBool(allow_none=True)
+    showOutline = NestedBool(allow_none=True)
+    showKeys = NestedBool(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('showHorzBorder', 'showVertBorder', 'showOutline',
+                    'showKeys', 'spPr', 'txPr')
+
+    def __init__(self,
+                 showHorzBorder=None,
+                 showVertBorder=None,
+                 showOutline=None,
+                 showKeys=None,
+                 spPr=None,
+                 txPr=None,
+                 extLst=None,
+                ):
+        self.showHorzBorder = showHorzBorder
+        self.showVertBorder = showVertBorder
+        self.showOutline = showOutline
+        self.showKeys = showKeys
+        self.spPr = spPr
+        self.txPr = txPr
+
+
+class PlotArea(Serialisable):
+
+    tagname = "plotArea"
+
+    layout = Typed(expected_type=Layout, allow_none=True)
+    dTable = Typed(expected_type=DataTable, allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    # at least one chart
+    _charts = MultiSequence()
+    areaChart = MultiSequencePart(expected_type=AreaChart, store="_charts")
+    area3DChart = MultiSequencePart(expected_type=AreaChart3D, store="_charts")
+    lineChart = MultiSequencePart(expected_type=LineChart, store="_charts")
+    line3DChart = MultiSequencePart(expected_type=LineChart3D, store="_charts")
+    stockChart = MultiSequencePart(expected_type=StockChart, store="_charts")
+    radarChart = MultiSequencePart(expected_type=RadarChart, store="_charts")
+    scatterChart = MultiSequencePart(expected_type=ScatterChart, store="_charts")
+    pieChart = MultiSequencePart(expected_type=PieChart, store="_charts")
+    pie3DChart = MultiSequencePart(expected_type=PieChart3D, store="_charts")
+    doughnutChart = MultiSequencePart(expected_type=DoughnutChart, store="_charts")
+    barChart = MultiSequencePart(expected_type=BarChart, store="_charts")
+    bar3DChart = MultiSequencePart(expected_type=BarChart3D, store="_charts")
+    ofPieChart = MultiSequencePart(expected_type=ProjectedPieChart, store="_charts")
+    surfaceChart = MultiSequencePart(expected_type=SurfaceChart, store="_charts")
+    surface3DChart = MultiSequencePart(expected_type=SurfaceChart3D, store="_charts")
+    bubbleChart = MultiSequencePart(expected_type=BubbleChart, store="_charts")
+
+    # axes
+    _axes = MultiSequence()
+    valAx = MultiSequencePart(expected_type=NumericAxis, store="_axes")
+    catAx = MultiSequencePart(expected_type=TextAxis, store="_axes")
+    dateAx = MultiSequencePart(expected_type=DateAxis, store="_axes")
+    serAx = MultiSequencePart(expected_type=SeriesAxis, store="_axes")
+
+    __elements__ = ('layout', '_charts', '_axes', 'dTable', 'spPr')
+
+    def __init__(self,
+                 layout=None,
+                 dTable=None,
+                 spPr=None,
+                 _charts=(),
+                 _axes=(),
+                 extLst=None,
+                ):
+        self.layout = layout
+        self.dTable = dTable
+        self.spPr = spPr
+        self._charts = _charts
+        self._axes = _axes
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        axIds = {ax.axId for ax in self._axes}
+        for chart in self._charts:
+            for id, axis in chart._axes.items():
+                if id not in axIds:
+                    setattr(self, axis.tagname, axis)
+                    axIds.add(id)
+
+        return super().to_tree(tagname)
+
+
+    @classmethod
+    def from_tree(cls, node):
+        self = super().from_tree(node)
+        axes = dict((axis.axId, axis) for axis in self._axes)
+        for chart in self._charts:
+            if isinstance(chart, (ScatterChart, BubbleChart)):
+                x, y = (axes[axId] for axId in chart.axId)
+                chart.x_axis = x
+                chart.y_axis = y
+                continue
+
+            for axId in chart.axId:
+                axis = axes.get(axId)
+                if axis is None and isinstance(chart, _3DBase):
+                    # Series Axis can be optional
+                    chart.z_axis = None
+                    continue
+                if axis.tagname in ("catAx", "dateAx"):
+                    chart.x_axis = axis
+                elif axis.tagname == "valAx":
+                    chart.y_axis = axis
+                elif axis.tagname == "serAx":
+                    chart.z_axis = axis
+
+        return self
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/print_settings.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/print_settings.py
new file mode 100644
index 00000000..65137310
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/print_settings.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Float,
+    Typed,
+    Alias,
+)
+
+from openpyxl.worksheet.page import PrintPageSetup
+from openpyxl.worksheet.header_footer import HeaderFooter
+
+
+class PageMargins(Serialisable):
+    """
+    Identical to openpyxl.worksheet.page.Pagemargins but element names are different :-/
+    """
+    tagname = "pageMargins"
+
+    l = Float()
+    left = Alias('l')
+    r = Float()
+    right = Alias('r')
+    t = Float()
+    top = Alias('t')
+    b = Float()
+    bottom = Alias('b')
+    header = Float()
+    footer = Float()
+
+    def __init__(self, l=0.75, r=0.75, t=1, b=1, header=0.5, footer=0.5):
+        self.l = l
+        self.r = r
+        self.t = t
+        self.b = b
+        self.header = header
+        self.footer = footer
+
+
+class PrintSettings(Serialisable):
+
+    tagname = "printSettings"
+
+    headerFooter = Typed(expected_type=HeaderFooter, allow_none=True)
+    pageMargins = Typed(expected_type=PageMargins, allow_none=True)
+    pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True)
+
+    __elements__ = ("headerFooter", "pageMargins", "pageMargins")
+
+    def __init__(self,
+                 headerFooter=None,
+                 pageMargins=None,
+                 pageSetup=None,
+                ):
+        self.headerFooter = headerFooter
+        self.pageMargins = pageMargins
+        self.pageSetup = pageSetup
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/radar_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/radar_chart.py
new file mode 100644
index 00000000..fa3aa0da
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/radar_chart.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    Typed,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedInteger,
+    NestedSet
+)
+
+from ._chart import ChartBase
+from .axis import TextAxis, NumericAxis
+from .series import Series
+from .label import DataLabelList
+
+
+class RadarChart(ChartBase):
+
+    tagname = "radarChart"
+
+    radarStyle = NestedSet(values=(['standard', 'marker', 'filled']))
+    type = Alias("radarStyle")
+    varyColors = NestedBool(nested=True, allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    _series_type = "radar"
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    __elements__ = ('radarStyle', 'varyColors', 'ser', 'dLbls', 'axId')
+
+    def __init__(self,
+                 radarStyle="standard",
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.radarStyle = radarStyle
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/reader.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/reader.py
new file mode 100644
index 00000000..0ef719f9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/reader.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Read a chart
+"""
+
+def read_chart(chartspace):
+    cs = chartspace
+    plot = cs.chart.plotArea
+
+    chart = plot._charts[0]
+    chart._charts = plot._charts
+
+    chart.title = cs.chart.title
+    chart.display_blanks = cs.chart.dispBlanksAs
+    chart.visible_cells_only = cs.chart.plotVisOnly
+    chart.layout = plot.layout
+    chart.legend = cs.chart.legend
+
+    # 3d attributes
+    chart.floor = cs.chart.floor
+    chart.sideWall = cs.chart.sideWall
+    chart.backWall = cs.chart.backWall
+    chart.pivotSource = cs.pivotSource
+    chart.pivotFormats = cs.chart.pivotFmts
+    chart.idx_base = min((s.idx for s in chart.series), default=0)
+    chart._reindex()
+
+    # Border, fill, etc.
+    chart.graphical_properties = cs.graphical_properties
+
+    return chart
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/reference.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/reference.py
new file mode 100644
index 00000000..dc102791
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/reference.py
@@ -0,0 +1,124 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from itertools import chain
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    MinMax,
+    Typed,
+    String,
+    Strict,
+)
+from openpyxl.worksheet.worksheet import Worksheet
+from openpyxl.utils import (
+    get_column_letter,
+    range_to_tuple,
+    quote_sheetname
+)
+
+
+class DummyWorksheet:
+
+
+    def __init__(self, title):
+        self.title = title
+
+
+class Reference(Strict):
+
+    """
+    Normalise cell range references
+    """
+
+    min_row = MinMax(min=1, max=1000000, expected_type=int)
+    max_row = MinMax(min=1, max=1000000, expected_type=int)
+    min_col = MinMax(min=1, max=16384, expected_type=int)
+    max_col = MinMax(min=1, max=16384, expected_type=int)
+    range_string = String(allow_none=True)
+
+    def __init__(self,
+                 worksheet=None,
+                 min_col=None,
+                 min_row=None,
+                 max_col=None,
+                 max_row=None,
+                 range_string=None
+                 ):
+        if range_string is not None:
+            sheetname, boundaries = range_to_tuple(range_string)
+            min_col, min_row, max_col, max_row = boundaries
+            worksheet = DummyWorksheet(sheetname)
+
+        self.worksheet = worksheet
+        self.min_col = min_col
+        self.min_row = min_row
+        if max_col is None:
+            max_col = min_col
+        self.max_col = max_col
+        if max_row is None:
+            max_row = min_row
+        self.max_row = max_row
+
+
+    def __repr__(self):
+        return str(self)
+
+
+    def __str__(self):
+        fmt = u"{0}!${1}${2}:${3}${4}"
+        if (self.min_col == self.max_col
+            and self.min_row == self.max_row):
+            fmt = u"{0}!${1}${2}"
+        return fmt.format(self.sheetname,
+                          get_column_letter(self.min_col), self.min_row,
+                          get_column_letter(self.max_col), self.max_row
+                          )
+
+
+    __str__ = __str__
+
+
+
+    def __len__(self):
+        if self.min_row == self.max_row:
+            return 1 + self.max_col - self.min_col
+        return 1 + self.max_row - self.min_row
+
+
+    def __eq__(self, other):
+        return str(self) == str(other)
+
+
+    @property
+    def rows(self):
+        """
+        Return all rows in the range
+        """
+        for row in range(self.min_row, self.max_row+1):
+            yield Reference(self.worksheet, self.min_col, row, self.max_col, row)
+
+
+    @property
+    def cols(self):
+        """
+        Return all columns in the range
+        """
+        for col in range(self.min_col, self.max_col+1):
+            yield Reference(self.worksheet, col, self.min_row, col, self.max_row)
+
+
+    def pop(self):
+        """
+        Return and remove the first cell
+        """
+        cell = "{0}{1}".format(get_column_letter(self.min_col), self.min_row)
+        if self.min_row == self.max_row:
+            self.min_col += 1
+        else:
+            self.min_row += 1
+        return cell
+
+
+    @property
+    def sheetname(self):
+        return quote_sheetname(self.worksheet.title)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/scatter_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/scatter_chart.py
new file mode 100644
index 00000000..2699239e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/scatter_chart.py
@@ -0,0 +1,53 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    Alias
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedBool,
+)
+
+from ._chart import ChartBase
+from .axis import NumericAxis, TextAxis
+from .series import XYSeries
+from .label import DataLabelList
+
+
+class ScatterChart(ChartBase):
+
+    tagname = "scatterChart"
+
+    scatterStyle = NestedNoneSet(values=(['line', 'lineMarker', 'marker', 'smooth', 'smoothMarker']))
+    varyColors = NestedBool(allow_none=True)
+    ser = Sequence(expected_type=XYSeries, allow_none=True)
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias("dLbls")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=(NumericAxis, TextAxis))
+    y_axis = Typed(expected_type=NumericAxis)
+
+    _series_type = "scatter"
+
+    __elements__ = ('scatterStyle', 'varyColors', 'ser', 'dLbls', 'axId',)
+
+    def __init__(self,
+                 scatterStyle=None,
+                 varyColors=None,
+                 ser=(),
+                 dLbls=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.scatterStyle = scatterStyle
+        self.varyColors = varyColors
+        self.ser = ser
+        self.dLbls = dLbls
+        self.x_axis = NumericAxis(axId=10, crossAx=20)
+        self.y_axis = NumericAxis(axId=20, crossAx=10)
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/series.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/series.py
new file mode 100644
index 00000000..f1403a6c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/series.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Integer,
+    Bool,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedInteger,
+    NestedBool,
+    NestedNoneSet,
+    NestedText,
+)
+
+from .shapes import GraphicalProperties
+from .data_source import (
+    AxDataSource,
+    NumDataSource,
+    NumRef,
+    StrRef,
+)
+from .error_bar import ErrorBars
+from .label import DataLabelList
+from .marker import DataPoint, PictureOptions, Marker
+from .trendline import Trendline
+
+attribute_mapping = {
+    'area': ('idx', 'order', 'tx', 'spPr', 'pictureOptions', 'dPt', 'dLbls', 'errBars',
+             'trendline', 'cat', 'val',),
+    'bar':('idx', 'order','tx', 'spPr', 'invertIfNegative', 'pictureOptions', 'dPt',
+           'dLbls', 'trendline', 'errBars', 'cat', 'val', 'shape'),
+    'bubble':('idx','order', 'tx', 'spPr', 'invertIfNegative', 'dPt', 'dLbls',
+              'trendline', 'errBars', 'xVal', 'yVal', 'bubbleSize', 'bubble3D'),
+    'line':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'trendline',
+            'errBars', 'cat', 'val', 'smooth'),
+    'pie':('idx', 'order', 'tx', 'spPr', 'explosion', 'dPt', 'dLbls', 'cat', 'val'),
+    'radar':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'cat', 'val'),
+    'scatter':('idx', 'order', 'tx', 'spPr', 'marker', 'dPt', 'dLbls', 'trendline',
+               'errBars', 'xVal', 'yVal', 'smooth'),
+    'surface':('idx', 'order', 'tx', 'spPr', 'cat', 'val'),
+                     }
+
+
+class SeriesLabel(Serialisable):
+
+    tagname = "tx"
+
+    strRef = Typed(expected_type=StrRef, allow_none=True)
+    v = NestedText(expected_type=str, allow_none=True)
+    value = Alias('v')
+
+    __elements__ = ('strRef', 'v')
+
+    def __init__(self,
+                 strRef=None,
+                 v=None):
+        self.strRef = strRef
+        self.v = v
+
+
+class Series(Serialisable):
+
+    """
+    Generic series object. Should not be instantiated directly.
+    User the chart.Series factory instead.
+    """
+
+    tagname = "ser"
+
+    idx = NestedInteger()
+    order = NestedInteger()
+    tx = Typed(expected_type=SeriesLabel, allow_none=True)
+    title = Alias('tx')
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+
+    # area chart
+    pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
+    dPt = Sequence(expected_type=DataPoint, allow_none=True)
+    data_points = Alias("dPt")
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    labels = Alias("dLbls")
+    trendline = Typed(expected_type=Trendline, allow_none=True)
+    errBars = Typed(expected_type=ErrorBars, allow_none=True)
+    cat = Typed(expected_type=AxDataSource, allow_none=True)
+    identifiers = Alias("cat")
+    val = Typed(expected_type=NumDataSource, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    #bar chart
+    invertIfNegative = NestedBool(allow_none=True)
+    shape = NestedNoneSet(values=(['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax']))
+
+    #bubble chart
+    xVal = Typed(expected_type=AxDataSource, allow_none=True)
+    yVal = Typed(expected_type=NumDataSource, allow_none=True)
+    bubbleSize = Typed(expected_type=NumDataSource, allow_none=True)
+    zVal = Alias("bubbleSize")
+    bubble3D = NestedBool(allow_none=True)
+
+    #line chart
+    marker = Typed(expected_type=Marker, allow_none=True)
+    smooth = NestedBool(allow_none=True)
+
+    #pie chart
+    explosion = NestedInteger(allow_none=True)
+
+    __elements__ = ()
+
+
+    def __init__(self,
+                 idx=0,
+                 order=0,
+                 tx=None,
+                 spPr=None,
+                 pictureOptions=None,
+                 dPt=(),
+                 dLbls=None,
+                 trendline=None,
+                 errBars=None,
+                 cat=None,
+                 val=None,
+                 invertIfNegative=None,
+                 shape=None,
+                 xVal=None,
+                 yVal=None,
+                 bubbleSize=None,
+                 bubble3D=None,
+                 marker=None,
+                 smooth=None,
+                 explosion=None,
+                 extLst=None,
+                ):
+        self.idx = idx
+        self.order = order
+        self.tx = tx
+        if spPr is None:
+            spPr = GraphicalProperties()
+        self.spPr = spPr
+        self.pictureOptions = pictureOptions
+        self.dPt = dPt
+        self.dLbls = dLbls
+        self.trendline = trendline
+        self.errBars = errBars
+        self.cat = cat
+        self.val = val
+        self.invertIfNegative = invertIfNegative
+        self.shape = shape
+        self.xVal = xVal
+        self.yVal = yVal
+        self.bubbleSize = bubbleSize
+        self.bubble3D = bubble3D
+        if marker is None:
+            marker = Marker()
+        self.marker = marker
+        self.smooth = smooth
+        self.explosion = explosion
+
+
+    def to_tree(self, tagname=None, idx=None):
+        """The index can need rebasing"""
+        if idx is not None:
+            if self.order == self.idx:
+                self.order = idx # rebase the order if the index has been rebased
+            self.idx = idx
+        return super().to_tree(tagname)
+
+
+class XYSeries(Series):
+
+    """Dedicated series for charts that have x and y series"""
+
+    idx = Series.idx
+    order = Series.order
+    tx = Series.tx
+    spPr = Series.spPr
+
+    dPt = Series.dPt
+    dLbls = Series.dLbls
+    trendline = Series.trendline
+    errBars = Series.errBars
+    xVal = Series.xVal
+    yVal = Series.yVal
+
+    invertIfNegative = Series.invertIfNegative
+
+    bubbleSize = Series.bubbleSize
+    bubble3D = Series.bubble3D
+
+    marker = Series.marker
+    smooth = Series.smooth
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/series_factory.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/series_factory.py
new file mode 100644
index 00000000..90b368d9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/series_factory.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .data_source import NumDataSource, NumRef, AxDataSource
+from .reference import Reference
+from .series import Series, XYSeries, SeriesLabel, StrRef
+from  openpyxl.utils import rows_from_range, quote_sheetname
+
+
+def SeriesFactory(values, xvalues=None, zvalues=None, title=None, title_from_data=False):
+    """
+    Convenience Factory for creating chart data series.
+    """
+
+    if not isinstance(values, Reference):
+        values = Reference(range_string=values)
+
+    if title_from_data:
+        cell = values.pop()
+        title = u"{0}!{1}".format(values.sheetname, cell)
+        title = SeriesLabel(strRef=StrRef(title))
+    elif title is not None:
+        title = SeriesLabel(v=title)
+
+    source = NumDataSource(numRef=NumRef(f=values))
+    if xvalues is not None:
+        if not isinstance(xvalues, Reference):
+            xvalues = Reference(range_string=xvalues)
+        series = XYSeries()
+        series.yVal = source
+        series.xVal = AxDataSource(numRef=NumRef(f=xvalues))
+        if zvalues is not None:
+            if not isinstance(zvalues, Reference):
+                zvalues = Reference(range_string=zvalues)
+            series.zVal = NumDataSource(NumRef(f=zvalues))
+    else:
+        series = Series()
+        series.val = source
+
+    if title is not None:
+        series.title = title
+    return series
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/shapes.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/shapes.py
new file mode 100644
index 00000000..7736c1ad
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/shapes.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Alias
+)
+from openpyxl.descriptors.nested import (
+    EmptyTag
+)
+from openpyxl.drawing.colors import ColorChoiceDescriptor
+from openpyxl.drawing.fill import *
+from openpyxl.drawing.line import LineProperties
+from openpyxl.drawing.geometry import (
+    Shape3D,
+    Scene3D,
+    Transform2D,
+    CustomGeometry2D,
+    PresetGeometry2D,
+)
+
+
+class GraphicalProperties(Serialisable):
+
+    """
+    Somewhat vaguely 21.2.2.197 says this:
+
+    This element specifies the formatting for the parent chart element. The
+    custGeom, prstGeom, scene3d, and xfrm elements are not supported. The
+    bwMode attribute is not supported.
+
+    This doesn't leave much. And the element is used in different places.
+    """
+
+    tagname = "spPr"
+
+    bwMode = NoneSet(values=(['clr', 'auto', 'gray', 'ltGray', 'invGray',
+                          'grayWhite', 'blackGray', 'blackWhite', 'black', 'white', 'hidden']
+                         )
+                 )
+
+    xfrm = Typed(expected_type=Transform2D, allow_none=True)
+    transform = Alias('xfrm')
+    custGeom = Typed(expected_type=CustomGeometry2D, allow_none=True) # either or
+    prstGeom = Typed(expected_type=PresetGeometry2D, allow_none=True)
+
+    # fills one of
+    noFill = EmptyTag(namespace=DRAWING_NS)
+    solidFill = ColorChoiceDescriptor()
+    gradFill = Typed(expected_type=GradientFillProperties, allow_none=True)
+    pattFill = Typed(expected_type=PatternFillProperties, allow_none=True)
+
+    ln = Typed(expected_type=LineProperties, allow_none=True)
+    line = Alias('ln')
+    scene3d = Typed(expected_type=Scene3D, allow_none=True)
+    sp3d = Typed(expected_type=Shape3D, allow_none=True)
+    shape3D = Alias('sp3d')
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ('xfrm', 'prstGeom', 'noFill', 'solidFill', 'gradFill', 'pattFill',
+                    'ln', 'scene3d', 'sp3d')
+
+    def __init__(self,
+                 bwMode=None,
+                 xfrm=None,
+                 noFill=None,
+                 solidFill=None,
+                 gradFill=None,
+                 pattFill=None,
+                 ln=None,
+                 scene3d=None,
+                 custGeom=None,
+                 prstGeom=None,
+                 sp3d=None,
+                 extLst=None,
+                ):
+        self.bwMode = bwMode
+        self.xfrm = xfrm
+        self.noFill = noFill
+        self.solidFill = solidFill
+        self.gradFill = gradFill
+        self.pattFill = pattFill
+        if ln is None:
+            ln = LineProperties()
+        self.ln = ln
+        self.custGeom = custGeom
+        self.prstGeom = prstGeom
+        self.scene3d = scene3d
+        self.sp3d = sp3d
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/stock_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/stock_chart.py
new file mode 100644
index 00000000..119c7901
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/stock_chart.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList
+
+from ._chart import ChartBase
+from .axis import TextAxis, NumericAxis, ChartLines
+from .updown_bars import UpDownBars
+from .label import DataLabelList
+from .series import Series
+
+
+class StockChart(ChartBase):
+
+    tagname = "stockChart"
+
+    ser = Sequence(expected_type=Series) #min 3, max4
+    dLbls = Typed(expected_type=DataLabelList, allow_none=True)
+    dataLabels = Alias('dLbls')
+    dropLines = Typed(expected_type=ChartLines, allow_none=True)
+    hiLowLines = Typed(expected_type=ChartLines, allow_none=True)
+    upDownBars = Typed(expected_type=UpDownBars, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+
+    _series_type = "line"
+
+    __elements__ = ('ser', 'dLbls', 'dropLines', 'hiLowLines', 'upDownBars',
+                    'axId')
+
+    def __init__(self,
+                 ser=(),
+                 dLbls=None,
+                 dropLines=None,
+                 hiLowLines=None,
+                 upDownBars=None,
+                 extLst=None,
+                 **kw
+                ):
+        self.ser = ser
+        self.dLbls = dLbls
+        self.dropLines = dropLines
+        self.hiLowLines = hiLowLines
+        self.upDownBars = upDownBars
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        super().__init__(**kw)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/surface_chart.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/surface_chart.py
new file mode 100644
index 00000000..5f388e14
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/surface_chart.py
@@ -0,0 +1,119 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Bool,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedInteger,
+    NestedBool,
+)
+
+from ._chart import ChartBase
+from ._3d import _3DBase
+from .axis import TextAxis, NumericAxis, SeriesAxis
+from .shapes import GraphicalProperties
+from .series import Series
+
+
+class BandFormat(Serialisable):
+
+    tagname = "bandFmt"
+
+    idx = NestedInteger()
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+
+    __elements__ = ('idx', 'spPr')
+
+    def __init__(self,
+                 idx=0,
+                 spPr=None,
+                ):
+        self.idx = idx
+        self.spPr = spPr
+
+
+class BandFormatList(Serialisable):
+
+    tagname = "bandFmts"
+
+    bandFmt = Sequence(expected_type=BandFormat, allow_none=True)
+
+    __elements__ = ('bandFmt',)
+
+    def __init__(self,
+                 bandFmt=(),
+                ):
+        self.bandFmt = bandFmt
+
+
+class _SurfaceChartBase(ChartBase):
+
+    wireframe = NestedBool(allow_none=True)
+    ser = Sequence(expected_type=Series, allow_none=True)
+    bandFmts = Typed(expected_type=BandFormatList, allow_none=True)
+
+    _series_type = "surface"
+
+    __elements__ = ('wireframe', 'ser', 'bandFmts')
+
+    def __init__(self,
+                 wireframe=None,
+                 ser=(),
+                 bandFmts=None,
+                 **kw
+                ):
+        self.wireframe = wireframe
+        self.ser = ser
+        self.bandFmts = bandFmts
+        super().__init__(**kw)
+
+
+class SurfaceChart3D(_SurfaceChartBase, _3DBase):
+
+    tagname = "surface3DChart"
+
+    wireframe = _SurfaceChartBase.wireframe
+    ser = _SurfaceChartBase.ser
+    bandFmts = _SurfaceChartBase.bandFmts
+
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    x_axis = Typed(expected_type=TextAxis)
+    y_axis = Typed(expected_type=NumericAxis)
+    z_axis = Typed(expected_type=SeriesAxis)
+
+    __elements__ = _SurfaceChartBase.__elements__ + ('axId',)
+
+    def __init__(self, **kw):
+        self.x_axis = TextAxis()
+        self.y_axis = NumericAxis()
+        self.z_axis = SeriesAxis()
+        super(SurfaceChart3D, self).__init__(**kw)
+
+
+class SurfaceChart(SurfaceChart3D):
+
+    tagname = "surfaceChart"
+
+    wireframe = _SurfaceChartBase.wireframe
+    ser = _SurfaceChartBase.ser
+    bandFmts = _SurfaceChartBase.bandFmts
+
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = SurfaceChart3D.__elements__
+
+    def __init__(self, **kw):
+        super().__init__(**kw)
+        self.y_axis.delete = True
+        self.view3D.x_rotation = 90
+        self.view3D.y_rotation = 0
+        self.view3D.perspective = False
+        self.view3D.right_angle_axes = False
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/text.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/text.py
new file mode 100644
index 00000000..bd034c24
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/text.py
@@ -0,0 +1,78 @@
+# Copyright (c) 2010-2024 openpyxl
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Alias,
+    Sequence,
+)
+
+
+from openpyxl.drawing.text import (
+    RichTextProperties,
+    ListStyle,
+    Paragraph,
+)
+
+from .data_source import StrRef
+
+
+class RichText(Serialisable):
+
+    """
+    From the specification: 21.2.2.216
+
+    This element specifies text formatting. The lstStyle element is not supported.
+    """
+
+    tagname = "rich"
+
+    bodyPr = Typed(expected_type=RichTextProperties)
+    properties = Alias("bodyPr")
+    lstStyle = Typed(expected_type=ListStyle, allow_none=True)
+    p = Sequence(expected_type=Paragraph)
+    paragraphs = Alias('p')
+
+    __elements__ = ("bodyPr", "lstStyle", "p")
+
+    def __init__(self,
+                 bodyPr=None,
+                 lstStyle=None,
+                 p=None,
+                ):
+        if bodyPr is None:
+            bodyPr = RichTextProperties()
+        self.bodyPr = bodyPr
+        self.lstStyle = lstStyle
+        if p is None:
+            p = [Paragraph()]
+        self.p = p
+
+
+class Text(Serialisable):
+
+    """
+    The value can be either a cell reference or a text element
+    If both are present then the reference will be used.
+    """
+
+    tagname = "tx"
+
+    strRef = Typed(expected_type=StrRef, allow_none=True)
+    rich = Typed(expected_type=RichText, allow_none=True)
+
+    __elements__ = ("strRef", "rich")
+
+    def __init__(self,
+                 strRef=None,
+                 rich=None
+                 ):
+        self.strRef = strRef
+        if rich is None:
+            rich = RichText()
+        self.rich = rich
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        if self.strRef and self.rich:
+            self.rich = None # can only have one
+        return super().to_tree(tagname, idx, namespace)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/title.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/title.py
new file mode 100644
index 00000000..10f79d7a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/title.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Alias,
+)
+
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import NestedBool
+
+from .text import Text, RichText
+from .layout import Layout
+from .shapes import GraphicalProperties
+
+from openpyxl.drawing.text import (
+    Paragraph,
+    RegularTextRun,
+    LineBreak,
+    ParagraphProperties,
+    CharacterProperties,
+)
+
+
+class Title(Serialisable):
+    tagname = "title"
+
+    tx = Typed(expected_type=Text, allow_none=True)
+    text = Alias('tx')
+    layout = Typed(expected_type=Layout, allow_none=True)
+    overlay = NestedBool(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    body = Alias('txPr')
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('tx', 'layout', 'overlay', 'spPr', 'txPr')
+
+    def __init__(self,
+                 tx=None,
+                 layout=None,
+                 overlay=None,
+                 spPr=None,
+                 txPr=None,
+                 extLst=None,
+                ):
+        if tx is None:
+            tx = Text()
+        self.tx = tx
+        self.layout = layout
+        self.overlay = overlay
+        self.spPr = spPr
+        self.txPr = txPr
+
+
+
+def title_maker(text):
+    title = Title()
+    paraprops = ParagraphProperties()
+    paraprops.defRPr = CharacterProperties()
+    paras = [Paragraph(r=[RegularTextRun(t=s)], pPr=paraprops) for s in text.split("\n")]
+
+    title.tx.rich.paragraphs = paras
+    return title
+
+
+class TitleDescriptor(Typed):
+
+    expected_type = Title
+    allow_none = True
+
+    def __set__(self, instance, value):
+        if isinstance(value, str):
+            value = title_maker(value)
+        super().__set__(instance, value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/trendline.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/trendline.py
new file mode 100644
index 00000000..bf6d2366
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/trendline.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Alias
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedBool,
+    NestedInteger,
+    NestedFloat,
+    NestedSet
+)
+
+from .data_source import NumFmt
+from .shapes import GraphicalProperties
+from .text import RichText, Text
+from .layout import Layout
+
+
+class TrendlineLabel(Serialisable):
+
+    tagname = "trendlineLbl"
+
+    layout = Typed(expected_type=Layout, allow_none=True)
+    tx = Typed(expected_type=Text, allow_none=True)
+    numFmt = Typed(expected_type=NumFmt, allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias("spPr")
+    txPr = Typed(expected_type=RichText, allow_none=True)
+    textProperties = Alias("txPr")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('layout', 'tx', 'numFmt', 'spPr', 'txPr')
+
+    def __init__(self,
+                 layout=None,
+                 tx=None,
+                 numFmt=None,
+                 spPr=None,
+                 txPr=None,
+                 extLst=None,
+                ):
+        self.layout = layout
+        self.tx = tx
+        self.numFmt = numFmt
+        self.spPr = spPr
+        self.txPr = txPr
+
+
+class Trendline(Serialisable):
+
+    tagname = "trendline"
+
+    name = String(allow_none=True)
+    spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
+    graphicalProperties = Alias('spPr')
+    trendlineType = NestedSet(values=(['exp', 'linear', 'log', 'movingAvg', 'poly', 'power']))
+    order = NestedInteger(allow_none=True)
+    period = NestedInteger(allow_none=True)
+    forward = NestedFloat(allow_none=True)
+    backward = NestedFloat(allow_none=True)
+    intercept = NestedFloat(allow_none=True)
+    dispRSqr = NestedBool(allow_none=True)
+    dispEq = NestedBool(allow_none=True)
+    trendlineLbl = Typed(expected_type=TrendlineLabel, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('spPr', 'trendlineType', 'order', 'period', 'forward',
+                    'backward', 'intercept', 'dispRSqr', 'dispEq', 'trendlineLbl')
+
+    def __init__(self,
+                 name=None,
+                 spPr=None,
+                 trendlineType='linear',
+                 order=None,
+                 period=None,
+                 forward=None,
+                 backward=None,
+                 intercept=None,
+                 dispRSqr=None,
+                 dispEq=None,
+                 trendlineLbl=None,
+                 extLst=None,
+                ):
+        self.name = name
+        self.spPr = spPr
+        self.trendlineType = trendlineType
+        self.order = order
+        self.period = period
+        self.forward = forward
+        self.backward = backward
+        self.intercept = intercept
+        self.dispRSqr = dispRSqr
+        self.dispEq = dispEq
+        self.trendlineLbl = trendlineLbl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chart/updown_bars.py b/.venv/lib/python3.12/site-packages/openpyxl/chart/updown_bars.py
new file mode 100644
index 00000000..6de7ab8a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chart/updown_bars.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import Typed
+from openpyxl.descriptors.excel import ExtensionList
+
+from .shapes import GraphicalProperties
+from .axis import ChartLines
+from .descriptors import NestedGapAmount
+
+
+class UpDownBars(Serialisable):
+
+    tagname = "upbars"
+
+    gapWidth = NestedGapAmount()
+    upBars = Typed(expected_type=ChartLines, allow_none=True)
+    downBars = Typed(expected_type=ChartLines, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('gapWidth', 'upBars', 'downBars')
+
+    def __init__(self,
+                 gapWidth=150,
+                 upBars=None,
+                 downBars=None,
+                 extLst=None,
+                ):
+        self.gapWidth = gapWidth
+        self.upBars = upBars
+        self.downBars = downBars
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/__init__.py
new file mode 100644
index 00000000..17266761
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .chartsheet import Chartsheet
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/chartsheet.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/chartsheet.py
new file mode 100644
index 00000000..21adbb43
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/chartsheet.py
@@ -0,0 +1,107 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors import Typed, Set, Alias
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.drawing.spreadsheet_drawing import (
+    AbsoluteAnchor,
+    SpreadsheetDrawing,
+)
+from openpyxl.worksheet.page import (
+    PageMargins,
+    PrintPageSetup
+)
+from openpyxl.worksheet.drawing import Drawing
+from openpyxl.worksheet.header_footer import HeaderFooter
+from openpyxl.workbook.child import _WorkbookChild
+from openpyxl.xml.constants import SHEET_MAIN_NS, REL_NS
+
+from .relation import DrawingHF, SheetBackgroundPicture
+from .properties import ChartsheetProperties
+from .protection import ChartsheetProtection
+from .views import ChartsheetViewList
+from .custom import CustomChartsheetViews
+from .publish import WebPublishItems
+
+
+class Chartsheet(_WorkbookChild, Serialisable):
+
+    tagname = "chartsheet"
+    _default_title = "Chart"
+    _rel_type = "chartsheet"
+    _path = "/xl/chartsheets/sheet{0}.xml"
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
+
+    sheetPr = Typed(expected_type=ChartsheetProperties, allow_none=True)
+    sheetViews = Typed(expected_type=ChartsheetViewList)
+    sheetProtection = Typed(expected_type=ChartsheetProtection, allow_none=True)
+    customSheetViews = Typed(expected_type=CustomChartsheetViews, allow_none=True)
+    pageMargins = Typed(expected_type=PageMargins, allow_none=True)
+    pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True)
+    drawing = Typed(expected_type=Drawing, allow_none=True)
+    drawingHF = Typed(expected_type=DrawingHF, allow_none=True)
+    picture = Typed(expected_type=SheetBackgroundPicture, allow_none=True)
+    webPublishItems = Typed(expected_type=WebPublishItems, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    sheet_state = Set(values=('visible', 'hidden', 'veryHidden'))
+    headerFooter = Typed(expected_type=HeaderFooter)
+    HeaderFooter = Alias('headerFooter')
+
+    __elements__ = (
+        'sheetPr', 'sheetViews', 'sheetProtection', 'customSheetViews',
+        'pageMargins', 'pageSetup', 'headerFooter', 'drawing', 'drawingHF',
+        'picture', 'webPublishItems')
+
+    __attrs__ = ()
+
+    def __init__(self,
+                 sheetPr=None,
+                 sheetViews=None,
+                 sheetProtection=None,
+                 customSheetViews=None,
+                 pageMargins=None,
+                 pageSetup=None,
+                 headerFooter=None,
+                 drawing=None,
+                 drawingHF=None,
+                 picture=None,
+                 webPublishItems=None,
+                 extLst=None,
+                 parent=None,
+                 title="",
+                 sheet_state='visible',
+                 ):
+        super().__init__(parent, title)
+        self._charts = []
+        self.sheetPr = sheetPr
+        if sheetViews is None:
+            sheetViews = ChartsheetViewList()
+        self.sheetViews = sheetViews
+        self.sheetProtection = sheetProtection
+        self.customSheetViews = customSheetViews
+        self.pageMargins = pageMargins
+        self.pageSetup = pageSetup
+        if headerFooter is not None:
+            self.headerFooter = headerFooter
+        self.drawing = Drawing("rId1")
+        self.drawingHF = drawingHF
+        self.picture = picture
+        self.webPublishItems = webPublishItems
+        self.sheet_state = sheet_state
+
+
+    def add_chart(self, chart):
+        chart.anchor = AbsoluteAnchor()
+        self._charts.append(chart)
+
+
+    def to_tree(self):
+        self._drawing = SpreadsheetDrawing()
+        self._drawing.charts = self._charts
+        tree = super().to_tree()
+        if not self.headerFooter:
+            el = tree.find('headerFooter')
+            tree.remove(el)
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/custom.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/custom.py
new file mode 100644
index 00000000..01fcd254
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/custom.py
@@ -0,0 +1,61 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.worksheet.header_footer import HeaderFooter
+
+from openpyxl.descriptors import (
+    Bool,
+    Integer,
+    Set,
+    Typed,
+    Sequence
+)
+from openpyxl.descriptors.excel import Guid
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.worksheet.page import (
+    PageMargins,
+    PrintPageSetup
+)
+
+
+class CustomChartsheetView(Serialisable):
+    tagname = "customSheetView"
+
+    guid = Guid()
+    scale = Integer()
+    state = Set(values=(['visible', 'hidden', 'veryHidden']))
+    zoomToFit = Bool(allow_none=True)
+    pageMargins = Typed(expected_type=PageMargins, allow_none=True)
+    pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True)
+    headerFooter = Typed(expected_type=HeaderFooter, allow_none=True)
+
+    __elements__ = ('pageMargins', 'pageSetup', 'headerFooter')
+
+    def __init__(self,
+                 guid=None,
+                 scale=None,
+                 state='visible',
+                 zoomToFit=None,
+                 pageMargins=None,
+                 pageSetup=None,
+                 headerFooter=None,
+                 ):
+        self.guid = guid
+        self.scale = scale
+        self.state = state
+        self.zoomToFit = zoomToFit
+        self.pageMargins = pageMargins
+        self.pageSetup = pageSetup
+        self.headerFooter = headerFooter
+
+
+class CustomChartsheetViews(Serialisable):
+    tagname = "customSheetViews"
+
+    customSheetView = Sequence(expected_type=CustomChartsheetView, allow_none=True)
+
+    __elements__ = ('customSheetView',)
+
+    def __init__(self,
+                 customSheetView=None,
+                 ):
+        self.customSheetView = customSheetView
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/properties.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/properties.py
new file mode 100644
index 00000000..bff6b3b3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/properties.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Bool,
+    String,
+    Typed
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.styles import Color
+
+
+class ChartsheetProperties(Serialisable):
+    tagname = "sheetPr"
+
+    published = Bool(allow_none=True)
+    codeName = String(allow_none=True)
+    tabColor = Typed(expected_type=Color, allow_none=True)
+
+    __elements__ = ('tabColor',)
+
+    def __init__(self,
+                 published=None,
+                 codeName=None,
+                 tabColor=None,
+                 ):
+        self.published = published
+        self.codeName = codeName
+        self.tabColor = tabColor
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/protection.py
new file mode 100644
index 00000000..f76a306b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/protection.py
@@ -0,0 +1,41 @@
+import hashlib
+
+from openpyxl.descriptors import (Bool, Integer, String)
+from openpyxl.descriptors.excel import Base64Binary
+from openpyxl.descriptors.serialisable import Serialisable
+
+from openpyxl.worksheet.protection import (
+    hash_password,
+    _Protected
+)
+
+
+class ChartsheetProtection(Serialisable, _Protected):
+    tagname = "sheetProtection"
+
+    algorithmName = String(allow_none=True)
+    hashValue = Base64Binary(allow_none=True)
+    saltValue = Base64Binary(allow_none=True)
+    spinCount = Integer(allow_none=True)
+    content = Bool(allow_none=True)
+    objects = Bool(allow_none=True)
+
+    __attrs__ = ("content", "objects", "password", "hashValue", "spinCount", "saltValue", "algorithmName")
+
+    def __init__(self,
+                 content=None,
+                 objects=None,
+                 hashValue=None,
+                 spinCount=None,
+                 saltValue=None,
+                 algorithmName=None,
+                 password=None,
+                 ):
+        self.content = content
+        self.objects = objects
+        self.hashValue = hashValue
+        self.spinCount = spinCount
+        self.saltValue = saltValue
+        self.algorithmName = algorithmName
+        if password is not None:
+            self.password = password
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/publish.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/publish.py
new file mode 100644
index 00000000..4f5714e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/publish.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Bool,
+    Integer,
+    String,
+    Set,
+    Sequence
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+class WebPublishItem(Serialisable):
+    tagname = "webPublishItem"
+
+    id = Integer()
+    divId = String()
+    sourceType = Set(values=(['sheet', 'printArea', 'autoFilter', 'range', 'chart', 'pivotTable', 'query', 'label']))
+    sourceRef = String()
+    sourceObject = String(allow_none=True)
+    destinationFile = String()
+    title = String(allow_none=True)
+    autoRepublish = Bool(allow_none=True)
+
+    def __init__(self,
+                 id=None,
+                 divId=None,
+                 sourceType=None,
+                 sourceRef=None,
+                 sourceObject=None,
+                 destinationFile=None,
+                 title=None,
+                 autoRepublish=None,
+                 ):
+        self.id = id
+        self.divId = divId
+        self.sourceType = sourceType
+        self.sourceRef = sourceRef
+        self.sourceObject = sourceObject
+        self.destinationFile = destinationFile
+        self.title = title
+        self.autoRepublish = autoRepublish
+
+
+class WebPublishItems(Serialisable):
+    tagname = "WebPublishItems"
+
+    count = Integer(allow_none=True)
+    webPublishItem = Sequence(expected_type=WebPublishItem, )
+
+    __elements__ = ('webPublishItem',)
+
+    def __init__(self,
+                 count=None,
+                 webPublishItem=None,
+                 ):
+        self.count = len(webPublishItem)
+        self.webPublishItem = webPublishItem
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/relation.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/relation.py
new file mode 100644
index 00000000..47f5f3d9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/relation.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Integer,
+    Alias
+)
+from openpyxl.descriptors.excel import Relation
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+class SheetBackgroundPicture(Serialisable):
+    tagname = "picture"
+    id = Relation()
+
+    def __init__(self, id):
+        self.id = id
+
+
+class DrawingHF(Serialisable):
+    id = Relation()
+    lho = Integer(allow_none=True)
+    leftHeaderOddPages = Alias('lho')
+    lhe = Integer(allow_none=True)
+    leftHeaderEvenPages = Alias('lhe')
+    lhf = Integer(allow_none=True)
+    leftHeaderFirstPage = Alias('lhf')
+    cho = Integer(allow_none=True)
+    centerHeaderOddPages = Alias('cho')
+    che = Integer(allow_none=True)
+    centerHeaderEvenPages = Alias('che')
+    chf = Integer(allow_none=True)
+    centerHeaderFirstPage = Alias('chf')
+    rho = Integer(allow_none=True)
+    rightHeaderOddPages = Alias('rho')
+    rhe = Integer(allow_none=True)
+    rightHeaderEvenPages = Alias('rhe')
+    rhf = Integer(allow_none=True)
+    rightHeaderFirstPage = Alias('rhf')
+    lfo = Integer(allow_none=True)
+    leftFooterOddPages = Alias('lfo')
+    lfe = Integer(allow_none=True)
+    leftFooterEvenPages = Alias('lfe')
+    lff = Integer(allow_none=True)
+    leftFooterFirstPage = Alias('lff')
+    cfo = Integer(allow_none=True)
+    centerFooterOddPages = Alias('cfo')
+    cfe = Integer(allow_none=True)
+    centerFooterEvenPages = Alias('cfe')
+    cff = Integer(allow_none=True)
+    centerFooterFirstPage = Alias('cff')
+    rfo = Integer(allow_none=True)
+    rightFooterOddPages = Alias('rfo')
+    rfe = Integer(allow_none=True)
+    rightFooterEvenPages = Alias('rfe')
+    rff = Integer(allow_none=True)
+    rightFooterFirstPage = Alias('rff')
+
+    def __init__(self,
+                 id=None,
+                 lho=None,
+                 lhe=None,
+                 lhf=None,
+                 cho=None,
+                 che=None,
+                 chf=None,
+                 rho=None,
+                 rhe=None,
+                 rhf=None,
+                 lfo=None,
+                 lfe=None,
+                 lff=None,
+                 cfo=None,
+                 cfe=None,
+                 cff=None,
+                 rfo=None,
+                 rfe=None,
+                 rff=None,
+                 ):
+        self.id = id
+        self.lho = lho
+        self.lhe = lhe
+        self.lhf = lhf
+        self.cho = cho
+        self.che = che
+        self.chf = chf
+        self.rho = rho
+        self.rhe = rhe
+        self.rhf = rhf
+        self.lfo = lfo
+        self.lfe = lfe
+        self.lff = lff
+        self.cfo = cfo
+        self.cfe = cfe
+        self.cff = cff
+        self.rfo = rfo
+        self.rfe = rfe
+        self.rff = rff
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/views.py b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/views.py
new file mode 100644
index 00000000..59289222
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/chartsheet/views.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Bool,
+    Integer,
+    Typed,
+    Sequence
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+class ChartsheetView(Serialisable):
+    tagname = "sheetView"
+
+    tabSelected = Bool(allow_none=True)
+    zoomScale = Integer(allow_none=True)
+    workbookViewId = Integer()
+    zoomToFit = Bool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 tabSelected=None,
+                 zoomScale=None,
+                 workbookViewId=0,
+                 zoomToFit=True,
+                 extLst=None,
+                 ):
+        self.tabSelected = tabSelected
+        self.zoomScale = zoomScale
+        self.workbookViewId = workbookViewId
+        self.zoomToFit = zoomToFit
+
+
+class ChartsheetViewList(Serialisable):
+    tagname = "sheetViews"
+
+    sheetView = Sequence(expected_type=ChartsheetView, )
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('sheetView',)
+
+    def __init__(self,
+                 sheetView=None,
+                 extLst=None,
+                 ):
+        if sheetView is None:
+            sheetView = [ChartsheetView()]
+        self.sheetView = sheetView
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/comments/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/comments/__init__.py
new file mode 100644
index 00000000..288bdf1d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/comments/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from .comments import Comment
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/comments/author.py b/.venv/lib/python3.12/site-packages/openpyxl/comments/author.py
new file mode 100644
index 00000000..9155fa5a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/comments/author.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    Alias
+)
+
+
+class AuthorList(Serialisable):
+
+    tagname = "authors"
+
+    author = Sequence(expected_type=str)
+    authors = Alias("author")
+
+    def __init__(self,
+                 author=(),
+                ):
+        self.author = author
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/comments/comment_sheet.py b/.venv/lib/python3.12/site-packages/openpyxl/comments/comment_sheet.py
new file mode 100644
index 00000000..67dccc55
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/comments/comment_sheet.py
@@ -0,0 +1,211 @@
+# Copyright (c) 2010-2024 openpyxl
+
+## Incomplete!
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Set,
+    String,
+    Bool,
+)
+from openpyxl.descriptors.excel import Guid, ExtensionList
+from openpyxl.descriptors.sequence import NestedSequence
+
+from openpyxl.utils.indexed_list import IndexedList
+from openpyxl.xml.constants import SHEET_MAIN_NS
+
+from openpyxl.cell.text import Text
+from .author import AuthorList
+from .comments import Comment
+from .shape_writer import ShapeWriter
+
+
+class Properties(Serialisable):
+
+    locked = Bool(allow_none=True)
+    defaultSize = Bool(allow_none=True)
+    _print = Bool(allow_none=True)
+    disabled = Bool(allow_none=True)
+    uiObject = Bool(allow_none=True)
+    autoFill = Bool(allow_none=True)
+    autoLine = Bool(allow_none=True)
+    altText = String(allow_none=True)
+    textHAlign = Set(values=(['left', 'center', 'right', 'justify', 'distributed']))
+    textVAlign = Set(values=(['top', 'center', 'bottom', 'justify', 'distributed']))
+    lockText = Bool(allow_none=True)
+    justLastX = Bool(allow_none=True)
+    autoScale = Bool(allow_none=True)
+    rowHidden = Bool(allow_none=True)
+    colHidden = Bool(allow_none=True)
+    # anchor = Typed(expected_type=ObjectAnchor, )
+
+    __elements__ = ('anchor',)
+
+    def __init__(self,
+                 locked=None,
+                 defaultSize=None,
+                 _print=None,
+                 disabled=None,
+                 uiObject=None,
+                 autoFill=None,
+                 autoLine=None,
+                 altText=None,
+                 textHAlign=None,
+                 textVAlign=None,
+                 lockText=None,
+                 justLastX=None,
+                 autoScale=None,
+                 rowHidden=None,
+                 colHidden=None,
+                 anchor=None,
+                ):
+        self.locked = locked
+        self.defaultSize = defaultSize
+        self._print = _print
+        self.disabled = disabled
+        self.uiObject = uiObject
+        self.autoFill = autoFill
+        self.autoLine = autoLine
+        self.altText = altText
+        self.textHAlign = textHAlign
+        self.textVAlign = textVAlign
+        self.lockText = lockText
+        self.justLastX = justLastX
+        self.autoScale = autoScale
+        self.rowHidden = rowHidden
+        self.colHidden = colHidden
+        self.anchor = anchor
+
+
+class CommentRecord(Serialisable):
+
+    tagname = "comment"
+
+    ref = String()
+    authorId = Integer()
+    guid = Guid(allow_none=True)
+    shapeId = Integer(allow_none=True)
+    text = Typed(expected_type=Text)
+    commentPr = Typed(expected_type=Properties, allow_none=True)
+    author = String(allow_none=True)
+
+    __elements__ = ('text', 'commentPr')
+    __attrs__ = ('ref', 'authorId', 'guid', 'shapeId')
+
+    def __init__(self,
+                 ref="",
+                 authorId=0,
+                 guid=None,
+                 shapeId=0,
+                 text=None,
+                 commentPr=None,
+                 author=None,
+                 height=79,
+                 width=144
+                ):
+        self.ref = ref
+        self.authorId = authorId
+        self.guid = guid
+        self.shapeId = shapeId
+        if text is None:
+            text = Text()
+        self.text = text
+        self.commentPr = commentPr
+        self.author = author
+        self.height = height
+        self.width = width
+
+
+    @classmethod
+    def from_cell(cls, cell):
+        """
+        Class method to convert cell comment
+        """
+        comment = cell._comment
+        ref = cell.coordinate
+        self = cls(ref=ref, author=comment.author)
+        self.text.t = comment.content
+        self.height = comment.height
+        self.width = comment.width
+        return self
+
+
+    @property
+    def content(self):
+        """
+        Remove all inline formatting and stuff
+        """
+        return self.text.content
+
+
+class CommentSheet(Serialisable):
+
+    tagname = "comments"
+
+    authors = Typed(expected_type=AuthorList)
+    commentList = NestedSequence(expected_type=CommentRecord, count=0)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    _id = None
+    _path = "/xl/comments/comment{0}.xml"
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
+    _rel_type = "comments"
+    _rel_id = None
+
+    __elements__ = ('authors', 'commentList')
+
+    def __init__(self,
+                 authors=None,
+                 commentList=None,
+                 extLst=None,
+                ):
+        self.authors = authors
+        self.commentList = commentList
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+    @property
+    def comments(self):
+        """
+        Return a dictionary of comments keyed by coord
+        """
+        authors = self.authors.author
+
+        for c in self.commentList:
+            yield c.ref, Comment(c.content, authors[c.authorId], c.height, c.width)
+
+
+    @classmethod
+    def from_comments(cls, comments):
+        """
+        Create a comment sheet from a list of comments for a particular worksheet
+        """
+        authors = IndexedList()
+
+        # dedupe authors and get indexes
+        for comment in comments:
+            comment.authorId = authors.add(comment.author)
+
+        return cls(authors=AuthorList(authors), commentList=comments)
+
+
+    def write_shapes(self, vml=None):
+        """
+        Create the VML for comments
+        """
+        sw = ShapeWriter(self.comments)
+        return sw.write(vml)
+
+
+    @property
+    def path(self):
+        """
+        Return path within the archive
+        """
+        return self._path.format(self._id)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/comments/comments.py b/.venv/lib/python3.12/site-packages/openpyxl/comments/comments.py
new file mode 100644
index 00000000..192bbc46
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/comments/comments.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+class Comment:
+
+    _parent = None
+
+    def __init__(self, text, author, height=79, width=144):
+        self.content = text
+        self.author = author
+        self.height = height
+        self.width = width
+
+
+    @property
+    def parent(self):
+        return self._parent
+
+
+    def __eq__(self, other):
+        return (
+            self.content == other.content
+            and self.author == other.author
+        )
+
+    def __repr__(self):
+        return "Comment: {0} by {1}".format(self.content, self.author)
+
+
+    def __copy__(self):
+        """Create a detached copy of this comment."""
+        clone = self.__class__(self.content, self.author, self.height, self.width)
+        return clone
+
+
+    def bind(self, cell):
+        """
+        Bind comment to a particular cell
+        """
+        if cell is not None and self._parent is not None and self._parent != cell:
+            fmt = "Comment already assigned to {0} in worksheet {1}. Cannot assign a comment to more than one cell"
+            raise AttributeError(fmt.format(cell.coordinate, cell.parent.title))
+        self._parent = cell
+
+
+    def unbind(self):
+        """
+        Unbind a comment from a cell
+        """
+        self._parent = None
+
+
+    @property
+    def text(self):
+        """
+        Any comment text stripped of all formatting.
+        """
+        return self.content
+
+    @text.setter
+    def text(self, value):
+        self.content = value
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/comments/shape_writer.py b/.venv/lib/python3.12/site-packages/openpyxl/comments/shape_writer.py
new file mode 100644
index 00000000..cebfbc3d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/comments/shape_writer.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.xml.functions import (
+    Element,
+    SubElement,
+    tostring,
+)
+
+from openpyxl.utils import coordinate_to_tuple
+
+vmlns = "urn:schemas-microsoft-com:vml"
+officens = "urn:schemas-microsoft-com:office:office"
+excelns = "urn:schemas-microsoft-com:office:excel"
+
+
+class ShapeWriter:
+    """
+    Create VML for comments
+    """
+
+    vml = None
+    vml_path = None
+
+
+    def __init__(self, comments):
+        self.comments = comments
+
+
+    def add_comment_shapetype(self, root):
+        shape_layout = SubElement(root, "{%s}shapelayout" % officens,
+                                  {"{%s}ext" % vmlns: "edit"})
+        SubElement(shape_layout,
+                   "{%s}idmap" % officens,
+                   {"{%s}ext" % vmlns: "edit", "data": "1"})
+        shape_type = SubElement(root,
+                                "{%s}shapetype" % vmlns,
+                                {"id": "_x0000_t202",
+                                 "coordsize": "21600,21600",
+                                 "{%s}spt" % officens: "202",
+                                 "path": "m,l,21600r21600,l21600,xe"})
+        SubElement(shape_type, "{%s}stroke" % vmlns, {"joinstyle": "miter"})
+        SubElement(shape_type,
+                   "{%s}path" % vmlns,
+                   {"gradientshapeok": "t",
+                    "{%s}connecttype" % officens: "rect"})
+
+
+    def add_comment_shape(self, root, idx, coord, height, width):
+        row, col = coordinate_to_tuple(coord)
+        row -= 1
+        col -= 1
+        shape = _shape_factory(row, col, height, width)
+
+        shape.set('id', "_x0000_s%04d" % idx)
+        root.append(shape)
+
+
+    def write(self, root):
+
+        if not hasattr(root, "findall"):
+            root = Element("xml")
+
+        # Remove any existing comment shapes
+        comments = root.findall("{%s}shape[@type='#_x0000_t202']" % vmlns)
+        for c in comments:
+            root.remove(c)
+
+        # check whether comments shape type already exists
+        shape_types = root.find("{%s}shapetype[@id='_x0000_t202']" % vmlns)
+        if shape_types is None:
+            self.add_comment_shapetype(root)
+
+        for idx, (coord, comment) in enumerate(self.comments, 1026):
+            self.add_comment_shape(root, idx, coord, comment.height, comment.width)
+
+        return tostring(root)
+
+
+def _shape_factory(row, column, height, width):
+    style = ("position:absolute; "
+             "margin-left:59.25pt;"
+             "margin-top:1.5pt;"
+             "width:{width}px;"
+             "height:{height}px;"
+             "z-index:1;"
+             "visibility:hidden").format(height=height,
+                                         width=width)
+    attrs = {
+        "type": "#_x0000_t202",
+        "style": style,
+        "fillcolor": "#ffffe1",
+        "{%s}insetmode" % officens: "auto"
+    }
+    shape = Element("{%s}shape" % vmlns, attrs)
+
+    SubElement(shape, "{%s}fill" % vmlns,
+               {"color2": "#ffffe1"})
+    SubElement(shape, "{%s}shadow" % vmlns,
+               {"color": "black", "obscured": "t"})
+    SubElement(shape, "{%s}path" % vmlns,
+               {"{%s}connecttype" % officens: "none"})
+    textbox = SubElement(shape, "{%s}textbox" % vmlns,
+                         {"style": "mso-direction-alt:auto"})
+    SubElement(textbox, "div", {"style": "text-align:left"})
+    client_data = SubElement(shape, "{%s}ClientData" % excelns,
+                             {"ObjectType": "Note"})
+    SubElement(client_data, "{%s}MoveWithCells" % excelns)
+    SubElement(client_data, "{%s}SizeWithCells" % excelns)
+    SubElement(client_data, "{%s}AutoFill" % excelns).text = "False"
+    SubElement(client_data, "{%s}Row" % excelns).text = str(row)
+    SubElement(client_data, "{%s}Column" % excelns).text = str(column)
+    return shape
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/__init__.py
new file mode 100644
index 00000000..dac09096
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/__init__.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .numbers import NUMERIC_TYPES
+from .strings import safe_string
+
+import warnings
+from functools import wraps
+import inspect
+
+
+class DummyCode:
+
+    pass
+
+
+# from https://github.com/tantale/deprecated/blob/master/deprecated/__init__.py
+# with an enhancement to update docstrings of deprecated functions
+string_types = (type(b''), type(u''))
+def deprecated(reason):
+
+    if isinstance(reason, string_types):
+
+        def decorator(func1):
+
+            if inspect.isclass(func1):
+                fmt1 = "Call to deprecated class {name} ({reason})."
+            else:
+                fmt1 = "Call to deprecated function {name} ({reason})."
+
+            @wraps(func1)
+            def new_func1(*args, **kwargs):
+                #warnings.simplefilter('default', DeprecationWarning)
+                warnings.warn(
+                    fmt1.format(name=func1.__name__, reason=reason),
+                    category=DeprecationWarning,
+                    stacklevel=2
+                )
+                return func1(*args, **kwargs)
+
+            # Enhance docstring with a deprecation note
+            deprecationNote = "\n\n.. note::\n    Deprecated: " + reason
+            if new_func1.__doc__:
+                new_func1.__doc__ += deprecationNote
+            else:
+                new_func1.__doc__ = deprecationNote
+            return new_func1
+
+        return decorator
+
+    elif inspect.isclass(reason) or inspect.isfunction(reason):
+        raise TypeError("Reason for deprecation must be supplied")
+
+    else:
+        raise TypeError(repr(type(reason)))
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/abc.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/abc.py
new file mode 100644
index 00000000..36a47f3f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/abc.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+try:
+    from abc import ABC
+except ImportError:
+    from abc import ABCMeta
+    ABC = ABCMeta('ABC', (object, ), {})
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/numbers.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/numbers.py
new file mode 100644
index 00000000..7d583451
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/numbers.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from decimal import Decimal
+
+NUMERIC_TYPES = (int, float, Decimal)
+
+
+try:
+    import numpy
+    NUMPY = True
+except ImportError:
+    NUMPY = False
+
+
+if NUMPY:
+    NUMERIC_TYPES = NUMERIC_TYPES + (numpy.short,
+                                     numpy.ushort,
+                                     numpy.intc,
+                                     numpy.uintc,
+                                     numpy.int_,
+                                     numpy.uint,
+                                     numpy.longlong,
+                                     numpy.ulonglong,
+                                     numpy.half,
+                                     numpy.float16,
+                                     numpy.single,
+                                     numpy.double,
+                                     numpy.longdouble,
+                                     numpy.int8,
+                                     numpy.int16,
+                                     numpy.int32,
+                                     numpy.int64,
+                                     numpy.uint8,
+                                     numpy.uint16,
+                                     numpy.uint32,
+                                     numpy.uint64,
+                                     numpy.intp,
+                                     numpy.uintp,
+                                     numpy.float32,
+                                     numpy.float64,
+                                     numpy.bool_,
+                                     numpy.floating,
+                                     numpy.integer)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/product.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/product.py
new file mode 100644
index 00000000..68fdae9f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/product.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+math.prod equivalent for < Python 3.8
+"""
+
+import functools
+import operator
+
+def product(sequence):
+    return functools.reduce(operator.mul, sequence)
+
+
+try:
+    from math import prod
+except ImportError:
+    prod = product
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/singleton.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/singleton.py
new file mode 100644
index 00000000..1fe6a908
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/singleton.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import weakref
+
+
+class Singleton(type):
+    """
+    Singleton metaclass
+    Based on Python Cookbook 3rd Edition Recipe 9.13
+    Only one instance of a class can exist. Does not work with __slots__
+    """
+
+    def __init__(self, *args, **kw):
+        super().__init__(*args, **kw)
+        self.__instance = None
+
+    def __call__(self, *args, **kw):
+        if self.__instance is None:
+            self.__instance = super().__call__(*args, **kw)
+        return self.__instance
+
+
+class Cached(type):
+    """
+    Caching metaclass
+    Child classes will only create new instances of themselves if
+    one doesn't already exist. Does not work with __slots__
+    """
+
+    def __init__(self, *args, **kw):
+        super().__init__(*args, **kw)
+        self.__cache = weakref.WeakValueDictionary()
+
+    def __call__(self, *args):
+        if args in self.__cache:
+            return self.__cache[args]
+
+        obj = super().__call__(*args)
+        self.__cache[args] = obj
+        return obj
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/compat/strings.py b/.venv/lib/python3.12/site-packages/openpyxl/compat/strings.py
new file mode 100644
index 00000000..2cc9d60e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/compat/strings.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from datetime import datetime
+from math import isnan, isinf
+import sys
+
+VER = sys.version_info
+
+from .numbers import NUMERIC_TYPES
+
+
+def safe_string(value):
+    """Safely and consistently format numeric values"""
+    if isinstance(value, NUMERIC_TYPES):
+        if isnan(value) or isinf(value):
+            value = ""
+        else:
+            value = "%.16g" % value
+    elif value is None:
+        value = "none"
+    elif isinstance(value, datetime):
+        value = value.isoformat()
+    elif not isinstance(value, str):
+        value = str(value)
+    return value
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/__init__.py
new file mode 100644
index 00000000..df86a3c7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/__init__.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .base import *
+from .sequence import Sequence
+
+
+class MetaStrict(type):
+
+    def __new__(cls, clsname, bases, methods):
+        for k, v in methods.items():
+            if isinstance(v, Descriptor):
+                v.name = k
+        return type.__new__(cls, clsname, bases, methods)
+
+
+class Strict(metaclass=MetaStrict):
+
+    pass
+
+
+class MetaSerialisable(type):
+
+    def __new__(cls, clsname, bases, methods):
+        attrs = []
+        nested = []
+        elements = []
+        namespaced = []
+        for k, v in methods.items():
+            if isinstance(v, Descriptor):
+                ns= getattr(v, 'namespace', None)
+                if ns:
+                    namespaced.append((k, "{%s}%s" % (ns, k)))
+                if getattr(v, 'nested', False):
+                    nested.append(k)
+                    elements.append(k)
+                elif isinstance(v, Sequence):
+                    elements.append(k)
+                elif isinstance(v, Typed):
+                    if hasattr(v.expected_type, 'to_tree'):
+                        elements.append(k)
+                    elif isinstance(v.expected_type, tuple):
+                        if any((hasattr(el, "to_tree") for el in v.expected_type)):
+                            # don't bind elements as attrs
+                            continue
+                    else:
+                        attrs.append(k)
+                else:
+                    if not isinstance(v, Alias):
+                        attrs.append(k)
+
+        if methods.get('__attrs__') is None:
+            methods['__attrs__'] = tuple(attrs)
+        methods['__namespaced__'] = tuple(namespaced)
+        if methods.get('__nested__') is None:
+            methods['__nested__'] = tuple(sorted(nested))
+        if methods.get('__elements__') is None:
+            methods['__elements__'] = tuple(sorted(elements))
+        return MetaStrict.__new__(cls, clsname, bases, methods)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/base.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/base.py
new file mode 100644
index 00000000..f1e86ed3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/base.py
@@ -0,0 +1,272 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""
+Based on Python Cookbook 3rd Edition, 8.13
+http://chimera.labs.oreilly.com/books/1230000000393/ch08.html#_discussiuncion_130
+"""
+
+import datetime
+import re
+
+from openpyxl import DEBUG
+from openpyxl.utils.datetime import from_ISO8601
+
+from .namespace import namespaced
+
+class Descriptor:
+
+    def __init__(self, name=None, **kw):
+        self.name = name
+        for k, v in kw.items():
+            setattr(self, k, v)
+
+    def __set__(self, instance, value):
+        instance.__dict__[self.name] = value
+
+
+class Typed(Descriptor):
+    """Values must of a particular type"""
+
+    expected_type = type(None)
+    allow_none = False
+    nested = False
+
+    def __init__(self, *args, **kw):
+        super().__init__(*args, **kw)
+        self.__doc__ = f"Values must be of type {self.expected_type}"
+
+    def __set__(self, instance, value):
+        if not isinstance(value, self.expected_type):
+            if (not self.allow_none
+                or (self.allow_none and value is not None)):
+                msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but value is {type(value)}"
+                if DEBUG:
+                    msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but {value} is {type(value)}"
+                raise TypeError(msg)
+        super().__set__(instance, value)
+
+    def __repr__(self):
+        return  self.__doc__
+
+
+def _convert(expected_type, value):
+    """
+    Check value is of or can be converted to expected type.
+    """
+    if not isinstance(value, expected_type):
+        try:
+            value = expected_type(value)
+        except:
+            raise TypeError('expected ' + str(expected_type))
+    return value
+
+
+class Convertible(Typed):
+    """Values must be convertible to a particular type"""
+
+    def __set__(self, instance, value):
+        if ((self.allow_none and value is not None)
+            or not self.allow_none):
+            value = _convert(self.expected_type, value)
+        super().__set__(instance, value)
+
+
+class Max(Convertible):
+    """Values must be less than a `max` value"""
+
+    expected_type = float
+    allow_none = False
+
+    def __init__(self, **kw):
+        if 'max' not in kw and not hasattr(self, 'max'):
+            raise TypeError('missing max value')
+        super().__init__(**kw)
+
+    def __set__(self, instance, value):
+        if ((self.allow_none and value is not None)
+            or not self.allow_none):
+            value = _convert(self.expected_type, value)
+            if value > self.max:
+                raise ValueError('Max value is {0}'.format(self.max))
+        super().__set__(instance, value)
+
+
+class Min(Convertible):
+    """Values must be greater than a `min` value"""
+
+    expected_type = float
+    allow_none = False
+
+    def __init__(self, **kw):
+        if 'min' not in kw and not hasattr(self, 'min'):
+            raise TypeError('missing min value')
+        super().__init__(**kw)
+
+    def __set__(self, instance, value):
+        if ((self.allow_none and value is not None)
+            or not self.allow_none):
+            value = _convert(self.expected_type, value)
+            if value < self.min:
+                raise ValueError('Min value is {0}'.format(self.min))
+        super().__set__(instance, value)
+
+
+class MinMax(Min, Max):
+    """Values must be greater than `min` value and less than a `max` one"""
+    pass
+
+
+class Set(Descriptor):
+    """Value can only be from a set of know values"""
+
+    def __init__(self, name=None, **kw):
+        if not 'values' in kw:
+            raise TypeError("missing set of values")
+        kw['values'] = set(kw['values'])
+        super().__init__(name, **kw)
+        self.__doc__ = "Value must be one of {0}".format(self.values)
+
+    def __set__(self, instance, value):
+        if value not in self.values:
+            raise ValueError(self.__doc__)
+        super().__set__(instance, value)
+
+
+class NoneSet(Set):
+
+    """'none' will be treated as None"""
+
+    def __init__(self, name=None, **kw):
+        super().__init__(name, **kw)
+        self.values.add(None)
+
+    def __set__(self, instance, value):
+        if value == 'none':
+            value = None
+        super().__set__(instance, value)
+
+
+class Integer(Convertible):
+
+    expected_type = int
+
+
+class Float(Convertible):
+
+    expected_type = float
+
+
+class Bool(Convertible):
+
+    expected_type = bool
+
+    def __set__(self, instance, value):
+        if isinstance(value, str):
+            if value in ('false', 'f', '0'):
+                value = False
+        super().__set__(instance, value)
+
+
+class String(Typed):
+
+    expected_type = str
+
+
+class Text(String, Convertible):
+
+    pass
+
+
+class ASCII(Typed):
+
+    expected_type = bytes
+
+
+class Tuple(Typed):
+
+    expected_type = tuple
+
+
+class Length(Descriptor):
+
+    def __init__(self, name=None, **kw):
+        if "length" not in kw:
+            raise TypeError("value length must be supplied")
+        super().__init__(**kw)
+
+
+    def __set__(self, instance, value):
+        if len(value) != self.length:
+            raise ValueError("Value must be length {0}".format(self.length))
+        super().__set__(instance, value)
+
+
+class Default(Typed):
+    """
+    When called returns an instance of the expected type.
+    Additional default values can be passed in to the descriptor
+    """
+
+    def __init__(self, name=None, **kw):
+        if "defaults" not in kw:
+            kw['defaults'] = {}
+        super().__init__(**kw)
+
+    def __call__(self):
+        return self.expected_type()
+
+
+class Alias(Descriptor):
+    """
+    Aliases can be used when either the desired attribute name is not allowed
+    or confusing in Python (eg. "type") or a more descriptive name is desired
+    (eg. "underline" for "u")
+    """
+
+    def __init__(self, alias):
+        self.alias = alias
+
+    def __set__(self, instance, value):
+        setattr(instance, self.alias, value)
+
+    def __get__(self, instance, cls):
+        return getattr(instance, self.alias)
+
+
+class MatchPattern(Descriptor):
+    """Values must match a regex pattern """
+    allow_none = False
+
+    def __init__(self, name=None, **kw):
+        if 'pattern' not in kw and not hasattr(self, 'pattern'):
+            raise TypeError('missing pattern value')
+
+        super().__init__(name, **kw)
+        self.test_pattern = re.compile(self.pattern, re.VERBOSE)
+
+
+    def __set__(self, instance, value):
+
+        if value is None and not self.allow_none:
+            raise ValueError("Value must not be none")
+
+        if ((self.allow_none and value is not None)
+            or not self.allow_none):
+            if not self.test_pattern.match(value):
+                raise ValueError('Value does not match pattern {0}'.format(self.pattern))
+
+        super().__set__(instance, value)
+
+
+class DateTime(Typed):
+
+    expected_type = datetime.datetime
+
+    def __set__(self, instance, value):
+        if value is not None and isinstance(value, str):
+            try:
+                value = from_ISO8601(value)
+            except ValueError:
+                raise ValueError("Value must be ISO datetime format")
+        super().__set__(instance, value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/container.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/container.py
new file mode 100644
index 00000000..4b1839f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/container.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Utility list for top level containers that contain one type of element
+
+Provides the necessary API to read and write XML
+"""
+
+from openpyxl.xml.functions import Element
+
+
+class ElementList(list):
+
+
+    @property
+    def tagname(self):
+        raise NotImplementedError
+
+
+    @property
+    def expected_type(self):
+        raise NotImplementedError
+
+
+    @classmethod
+    def from_tree(cls, tree):
+        l = [cls.expected_type.from_tree(el) for el in tree]
+        return cls(l)
+
+
+    def to_tree(self):
+        container = Element(self.tagname)
+        for el in self:
+            container.append(el.to_tree())
+        return container
+
+
+    def append(self, value):
+        if not isinstance(value, self.expected_type):
+            raise TypeError(f"Value must of type {self.expected_type} {type(value)} provided")
+        super().append(value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/excel.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/excel.py
new file mode 100644
index 00000000..d8aa2028
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/excel.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Excel specific descriptors
+"""
+
+from openpyxl.xml.constants import REL_NS
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import Element
+
+from . import (
+    MatchPattern,
+    MinMax,
+    Integer,
+    String,
+    Sequence,
+)
+from .serialisable import Serialisable
+
+
+class HexBinary(MatchPattern):
+
+    pattern = "[0-9a-fA-F]+$"
+
+
+class UniversalMeasure(MatchPattern):
+
+    pattern = r"[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"
+
+
+class TextPoint(MinMax):
+    """
+    Size in hundredths of points.
+    In theory other units of measurement can be used but these are unbounded
+    """
+    expected_type = int
+
+    min = -400000
+    max = 400000
+
+
+Coordinate = Integer
+
+
+class Percentage(MinMax):
+
+    pattern = r"((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%" # strict
+    min = -1000000
+    max = 1000000
+
+    def __set__(self, instance, value):
+        if isinstance(value, str) and "%" in value:
+            value = value.replace("%", "")
+            value = int(float(value) * 1000)
+        super().__set__(instance, value)
+
+
+class Extension(Serialisable):
+
+    uri = String()
+
+    def __init__(self,
+                 uri=None,
+                ):
+        self.uri = uri
+
+
+class ExtensionList(Serialisable):
+
+    ext = Sequence(expected_type=Extension)
+
+    def __init__(self,
+                 ext=(),
+                ):
+        self.ext = ext
+
+
+class Relation(String):
+
+    namespace = REL_NS
+    allow_none = True
+
+
+class Base64Binary(MatchPattern):
+    # http://www.w3.org/TR/xmlschema11-2/#nt-Base64Binary
+    pattern = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$"
+
+
+class Guid(MatchPattern):
+    # https://msdn.microsoft.com/en-us/library/dd946381(v=office.12).aspx
+    pattern = r"{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}"
+
+
+class CellRange(MatchPattern):
+
+    pattern = r"^[$]?([A-Za-z]{1,3})[$]?(\d+)(:[$]?([A-Za-z]{1,3})[$]?(\d+)?)?$|^[A-Za-z]{1,3}:[A-Za-z]{1,3}$"
+    allow_none = True
+
+    def __set__(self, instance, value):
+
+        if value is not None:
+            value = value.upper()
+        super().__set__(instance, value)
+
+
+def _explicit_none(tagname, value, namespace=None):
+    """
+    Override serialisation because explicit none required
+    """
+    if namespace is not None:
+        tagname = "{%s}%s" % (namespace, tagname)
+    return Element(tagname, val=safe_string(value))
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/namespace.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/namespace.py
new file mode 100644
index 00000000..93cc9e41
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/namespace.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+def namespaced(obj, tagname, namespace=None):
+    """
+    Utility to create a namespaced tag for an object
+    """
+
+    namespace = getattr(obj, "namespace", None) or namespace
+    if namespace is not None:
+        tagname = "{%s}%s" % (namespace, tagname)
+    return tagname
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/nested.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/nested.py
new file mode 100644
index 00000000..bda63a2d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/nested.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Generic serialisable classes
+"""
+from .base import (
+    Convertible,
+    Bool,
+    Descriptor,
+    NoneSet,
+    MinMax,
+    Set,
+    Float,
+    Integer,
+    String,
+    )
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import Element, localname, whitespace
+
+
+class Nested(Descriptor):
+
+    nested = True
+    attribute = "val"
+
+    def __set__(self, instance, value):
+        if hasattr(value, "tag"):
+            tag = localname(value)
+            if tag != self.name:
+                raise ValueError("Tag does not match attribute")
+
+            value = self.from_tree(value)
+        super().__set__(instance, value)
+
+
+    def from_tree(self, node):
+        return node.get(self.attribute)
+
+
+    def to_tree(self, tagname=None, value=None, namespace=None):
+        namespace = getattr(self, "namespace", namespace)
+        if value is not None:
+            if namespace is not None:
+                tagname = "{%s}%s" % (namespace, tagname)
+            value = safe_string(value)
+            return Element(tagname, {self.attribute:value})
+
+
+class NestedValue(Nested, Convertible):
+    """
+    Nested tag storing the value on the 'val' attribute
+    """
+    pass
+
+
+class NestedText(NestedValue):
+    """
+    Represents any nested tag with the value as the contents of the tag
+    """
+
+
+    def from_tree(self, node):
+        return node.text
+
+
+    def to_tree(self, tagname=None, value=None, namespace=None):
+        namespace = getattr(self, "namespace", namespace)
+        if value is not None:
+            if namespace is not None:
+                tagname = "{%s}%s" % (namespace, tagname)
+            el = Element(tagname)
+            el.text = safe_string(value)
+            whitespace(el)
+            return el
+
+
+class NestedFloat(NestedValue, Float):
+
+    pass
+
+
+class NestedInteger(NestedValue, Integer):
+
+    pass
+
+
+class NestedString(NestedValue, String):
+
+    pass
+
+
+class NestedBool(NestedValue, Bool):
+
+
+    def from_tree(self, node):
+        return node.get("val", True)
+
+
+class NestedNoneSet(Nested, NoneSet):
+
+    pass
+
+
+class NestedSet(Nested, Set):
+
+    pass
+
+
+class NestedMinMax(Nested, MinMax):
+
+    pass
+
+
+class EmptyTag(Nested, Bool):
+
+    """
+    Boolean if a tag exists or not.
+    """
+
+    def from_tree(self, node):
+        return True
+
+
+    def to_tree(self, tagname=None, value=None, namespace=None):
+        if value:
+            namespace = getattr(self, "namespace", namespace)
+            if namespace is not None:
+                tagname = "{%s}%s" % (namespace, tagname)
+            return Element(tagname)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/sequence.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/sequence.py
new file mode 100644
index 00000000..d77116b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/sequence.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import Element
+from openpyxl.utils.indexed_list import IndexedList
+
+from .base import Descriptor, Alias, _convert
+from .namespace import namespaced
+
+
+class Sequence(Descriptor):
+    """
+    A sequence (list or tuple) that may only contain objects of the declared
+    type
+    """
+
+    expected_type = type(None)
+    seq_types = (list, tuple)
+    idx_base = 0
+    unique = False
+    container = list
+
+
+    def __set__(self, instance, seq):
+        if not isinstance(seq, self.seq_types):
+            raise TypeError("Value must be a sequence")
+        seq = self.container(_convert(self.expected_type, value) for value in seq)
+        if self.unique:
+            seq = IndexedList(seq)
+
+        super().__set__(instance, seq)
+
+
+    def to_tree(self, tagname, obj, namespace=None):
+        """
+        Convert the sequence represented by the descriptor to an XML element
+        """
+        for idx, v in enumerate(obj, self.idx_base):
+            if hasattr(v, "to_tree"):
+                el = v.to_tree(tagname, idx)
+            else:
+                tagname = namespaced(obj, tagname, namespace)
+                el = Element(tagname)
+                el.text = safe_string(v)
+            yield el
+
+
+class UniqueSequence(Sequence):
+    """
+    Use a set to keep values unique
+    """
+    seq_types = (list, tuple, set)
+    container = set
+
+
+class ValueSequence(Sequence):
+    """
+    A sequence of primitive types that are stored as a single attribute.
+    "val" is the default attribute
+    """
+
+    attribute = "val"
+
+
+    def to_tree(self, tagname, obj, namespace=None):
+        tagname = namespaced(self, tagname, namespace)
+        for v in obj:
+            yield Element(tagname, {self.attribute:safe_string(v)})
+
+
+    def from_tree(self, node):
+
+        return node.get(self.attribute)
+
+
+class NestedSequence(Sequence):
+    """
+    Wrap a sequence in an containing object
+    """
+
+    count = False
+
+    def to_tree(self, tagname, obj, namespace=None):
+        tagname = namespaced(self, tagname, namespace)
+        container = Element(tagname)
+        if self.count:
+            container.set('count', str(len(obj)))
+        for v in obj:
+            container.append(v.to_tree())
+        return container
+
+
+    def from_tree(self, node):
+        return [self.expected_type.from_tree(el) for el in node]
+
+
+class MultiSequence(Sequence):
+    """
+    Sequences can contain objects with different tags
+    """
+
+    def __set__(self, instance, seq):
+        if not isinstance(seq, (tuple, list)):
+            raise ValueError("Value must be a sequence")
+        seq = list(seq)
+        Descriptor.__set__(self, instance, seq)
+
+
+    def to_tree(self, tagname, obj, namespace=None):
+        """
+        Convert the sequence represented by the descriptor to an XML element
+        """
+        for v in obj:
+            el = v.to_tree(namespace=namespace)
+            yield el
+
+
+class MultiSequencePart(Alias):
+    """
+    Allow a multisequence to be built up from parts
+
+    Excluded from the instance __elements__ or __attrs__ as is effectively an Alias
+    """
+
+    def __init__(self, expected_type, store):
+        self.expected_type = expected_type
+        self.store = store
+
+
+    def __set__(self, instance, value):
+        value = _convert(self.expected_type, value)
+        instance.__dict__[self.store].append(value)
+
+
+    def __get__(self, instance, cls):
+        return self
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/serialisable.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/serialisable.py
new file mode 100644
index 00000000..1bc9ef0d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/serialisable.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from copy import copy
+from keyword import kwlist
+KEYWORDS = frozenset(kwlist)
+
+from . import Descriptor
+from . import MetaSerialisable
+from .sequence import (
+    Sequence,
+    NestedSequence,
+    MultiSequencePart,
+)
+from .namespace import namespaced
+
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import (
+    Element,
+    localname,
+)
+
+seq_types = (list, tuple)
+
+class Serialisable(metaclass=MetaSerialisable):
+    """
+    Objects can serialise to XML their attributes and child objects.
+    The following class attributes are created by the metaclass at runtime:
+    __attrs__ = attributes
+    __nested__ = single-valued child treated as an attribute
+    __elements__ = child elements
+    """
+
+    __attrs__ = None
+    __nested__ = None
+    __elements__ = None
+    __namespaced__ = None
+
+    idx_base = 0
+
+    @property
+    def tagname(self):
+        raise(NotImplementedError)
+
+    namespace = None
+
+    @classmethod
+    def from_tree(cls, node):
+        """
+        Create object from XML
+        """
+        # strip known namespaces from attributes
+        attrib = dict(node.attrib)
+        for key, ns in cls.__namespaced__:
+            if ns in attrib:
+                attrib[key] = attrib[ns]
+                del attrib[ns]
+
+        # strip attributes with unknown namespaces
+        for key in list(attrib):
+            if key.startswith('{'):
+                del attrib[key]
+            elif key in KEYWORDS:
+                attrib["_" + key] = attrib[key]
+                del attrib[key]
+            elif "-" in key:
+                n = key.replace("-", "_")
+                attrib[n] = attrib[key]
+                del attrib[key]
+
+        if node.text and "attr_text" in cls.__attrs__:
+            attrib["attr_text"] = node.text
+
+        for el in node:
+            tag = localname(el)
+            if tag in KEYWORDS:
+                tag = "_" + tag
+            desc = getattr(cls, tag, None)
+            if desc is None or isinstance(desc, property):
+                continue
+
+            if hasattr(desc, 'from_tree'):
+                #descriptor manages conversion
+                obj = desc.from_tree(el)
+            else:
+                if hasattr(desc.expected_type, "from_tree"):
+                    #complex type
+                    obj = desc.expected_type.from_tree(el)
+                else:
+                    #primitive
+                    obj = el.text
+
+            if isinstance(desc, NestedSequence):
+                attrib[tag] = obj
+            elif isinstance(desc, Sequence):
+                attrib.setdefault(tag, [])
+                attrib[tag].append(obj)
+            elif isinstance(desc, MultiSequencePart):
+                attrib.setdefault(desc.store, [])
+                attrib[desc.store].append(obj)
+            else:
+                attrib[tag] = obj
+
+        return cls(**attrib)
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+
+        if tagname is None:
+            tagname = self.tagname
+
+        # keywords have to be masked
+        if tagname.startswith("_"):
+            tagname = tagname[1:]
+
+        tagname = namespaced(self, tagname, namespace)
+        namespace = getattr(self, "namespace", namespace)
+
+        attrs = dict(self)
+        for key, ns in self.__namespaced__:
+            if key in attrs:
+                attrs[ns] = attrs[key]
+                del attrs[key]
+
+        el = Element(tagname, attrs)
+        if "attr_text" in self.__attrs__:
+            el.text = safe_string(getattr(self, "attr_text"))
+
+        for child_tag in self.__elements__:
+            desc = getattr(self.__class__, child_tag, None)
+            obj = getattr(self, child_tag)
+            if hasattr(desc, "namespace") and hasattr(obj, 'namespace'):
+                obj.namespace = desc.namespace
+
+            if isinstance(obj, seq_types):
+                if isinstance(desc, NestedSequence):
+                    # wrap sequence in container
+                    if not obj:
+                        continue
+                    nodes = [desc.to_tree(child_tag, obj, namespace)]
+                elif isinstance(desc, Sequence):
+                    # sequence
+                    desc.idx_base = self.idx_base
+                    nodes = (desc.to_tree(child_tag, obj, namespace))
+                else: # property
+                    nodes = (v.to_tree(child_tag, namespace) for v in obj)
+                for node in nodes:
+                    el.append(node)
+            else:
+                if child_tag in self.__nested__:
+                    node = desc.to_tree(child_tag, obj, namespace)
+                elif obj is None:
+                    continue
+                else:
+                    node = obj.to_tree(child_tag)
+                if node is not None:
+                    el.append(node)
+        return el
+
+
+    def __iter__(self):
+        for attr in self.__attrs__:
+            value = getattr(self, attr)
+            if attr.startswith("_"):
+                attr = attr[1:]
+            elif attr != "attr_text" and "_" in attr:
+                desc = getattr(self.__class__, attr)
+                if getattr(desc, "hyphenated", False):
+                    attr = attr.replace("_", "-")
+            if attr != "attr_text" and value is not None:
+                yield attr, safe_string(value)
+
+
+    def __eq__(self, other):
+        if not self.__class__ == other.__class__:
+            return False
+        elif not dict(self) == dict(other):
+            return False
+        for el in self.__elements__:
+            if getattr(self, el) != getattr(other, el):
+                return False
+        return True
+
+
+    def __ne__(self, other):
+        return not self == other
+
+
+    def __repr__(self):
+        s = u"<{0}.{1} object>\nParameters:".format(
+            self.__module__,
+            self.__class__.__name__
+        )
+        args = []
+        for k in self.__attrs__ + self.__elements__:
+            v = getattr(self, k)
+            if isinstance(v, Descriptor):
+                v = None
+            args.append(u"{0}={1}".format(k, repr(v)))
+        args = u", ".join(args)
+
+        return u"\n".join([s, args])
+
+
+    def __hash__(self):
+        fields = []
+        for attr in self.__attrs__ + self.__elements__:
+            val = getattr(self, attr)
+            if isinstance(val, list):
+                val = tuple(val)
+            fields.append(val)
+
+        return hash(tuple(fields))
+
+
+    def __add__(self, other):
+        if type(self) != type(other):
+            raise TypeError("Cannot combine instances of different types")
+        vals = {}
+        for attr in self.__attrs__:
+            vals[attr] = getattr(self, attr) or getattr(other, attr)
+        for el in self.__elements__:
+            a = getattr(self, el)
+            b = getattr(other, el)
+            if a and b:
+                vals[el] = a + b
+            else:
+                vals[el] = a or b
+        return self.__class__(**vals)
+
+
+    def __copy__(self):
+        # serialise to xml and back to avoid shallow copies
+        xml = self.to_tree(tagname="dummy")
+        cp = self.__class__.from_tree(xml)
+        # copy any non-persisted attributed
+        for k in self.__dict__:
+            if k not in self.__attrs__ + self.__elements__:
+                v = copy(getattr(self, k))
+                setattr(cp, k, v)
+        return cp
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/descriptors/slots.py b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/slots.py
new file mode 100644
index 00000000..cadc1ef3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/descriptors/slots.py
@@ -0,0 +1,18 @@
+# Metaclass for mixing slots and descriptors
+# From "Programming in Python 3" by Mark Summerfield Ch.8 p. 383
+
+class AutoSlotProperties(type):
+
+    def __new__(mcl, classname, bases, dictionary):
+        slots = list(dictionary.get("__slots__", []))
+        for getter_name in [key for key in dictionary if key.startswith("get_")]:
+            name = getter_name
+            slots.append("__" + name)
+            getter = dictionary.pop(getter_name)
+            setter = dictionary.get(setter_name, None)
+            if (setter is not None
+                and isinstance(setter, collections.Callable)):
+                del dictionary[setter_name]
+            dictionary[name] = property(getter. setter)
+            dictionary["__slots__"] = tuple(slots)
+            return super().__new__(mcl, classname, bases, dictionary)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/__init__.py
new file mode 100644
index 00000000..02f05876
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from .drawing import Drawing
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/colors.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/colors.py
new file mode 100644
index 00000000..19fa5e84
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/colors.py
@@ -0,0 +1,435 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    Integer,
+    Set,
+    MinMax,
+)
+from openpyxl.descriptors.excel import Percentage
+from openpyxl.descriptors.nested import (
+    NestedNoneSet,
+    NestedValue,
+    NestedInteger,
+    EmptyTag,
+)
+
+from openpyxl.styles.colors import RGB
+from openpyxl.xml.constants import DRAWING_NS
+
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+
+PRESET_COLORS = [
+        'aliceBlue', 'antiqueWhite', 'aqua', 'aquamarine',
+        'azure', 'beige', 'bisque', 'black', 'blanchedAlmond', 'blue',
+        'blueViolet', 'brown', 'burlyWood', 'cadetBlue', 'chartreuse',
+        'chocolate', 'coral', 'cornflowerBlue', 'cornsilk', 'crimson', 'cyan',
+        'darkBlue', 'darkCyan', 'darkGoldenrod', 'darkGray', 'darkGrey',
+        'darkGreen', 'darkKhaki', 'darkMagenta', 'darkOliveGreen', 'darkOrange',
+        'darkOrchid', 'darkRed', 'darkSalmon', 'darkSeaGreen', 'darkSlateBlue',
+        'darkSlateGray', 'darkSlateGrey', 'darkTurquoise', 'darkViolet',
+        'dkBlue', 'dkCyan', 'dkGoldenrod', 'dkGray', 'dkGrey', 'dkGreen',
+        'dkKhaki', 'dkMagenta', 'dkOliveGreen', 'dkOrange', 'dkOrchid', 'dkRed',
+        'dkSalmon', 'dkSeaGreen', 'dkSlateBlue', 'dkSlateGray', 'dkSlateGrey',
+        'dkTurquoise', 'dkViolet', 'deepPink', 'deepSkyBlue', 'dimGray',
+        'dimGrey', 'dodgerBlue', 'firebrick', 'floralWhite', 'forestGreen',
+        'fuchsia', 'gainsboro', 'ghostWhite', 'gold', 'goldenrod', 'gray',
+        'grey', 'green', 'greenYellow', 'honeydew', 'hotPink', 'indianRed',
+        'indigo', 'ivory', 'khaki', 'lavender', 'lavenderBlush', 'lawnGreen',
+        'lemonChiffon', 'lightBlue', 'lightCoral', 'lightCyan',
+        'lightGoldenrodYellow', 'lightGray', 'lightGrey', 'lightGreen',
+        'lightPink', 'lightSalmon', 'lightSeaGreen', 'lightSkyBlue',
+        'lightSlateGray', 'lightSlateGrey', 'lightSteelBlue', 'lightYellow',
+        'ltBlue', 'ltCoral', 'ltCyan', 'ltGoldenrodYellow', 'ltGray', 'ltGrey',
+        'ltGreen', 'ltPink', 'ltSalmon', 'ltSeaGreen', 'ltSkyBlue',
+        'ltSlateGray', 'ltSlateGrey', 'ltSteelBlue', 'ltYellow', 'lime',
+        'limeGreen', 'linen', 'magenta', 'maroon', 'medAquamarine', 'medBlue',
+        'medOrchid', 'medPurple', 'medSeaGreen', 'medSlateBlue',
+        'medSpringGreen', 'medTurquoise', 'medVioletRed', 'mediumAquamarine',
+        'mediumBlue', 'mediumOrchid', 'mediumPurple', 'mediumSeaGreen',
+        'mediumSlateBlue', 'mediumSpringGreen', 'mediumTurquoise',
+        'mediumVioletRed', 'midnightBlue', 'mintCream', 'mistyRose', 'moccasin',
+        'navajoWhite', 'navy', 'oldLace', 'olive', 'oliveDrab', 'orange',
+        'orangeRed', 'orchid', 'paleGoldenrod', 'paleGreen', 'paleTurquoise',
+        'paleVioletRed', 'papayaWhip', 'peachPuff', 'peru', 'pink', 'plum',
+        'powderBlue', 'purple', 'red', 'rosyBrown', 'royalBlue', 'saddleBrown',
+        'salmon', 'sandyBrown', 'seaGreen', 'seaShell', 'sienna', 'silver',
+        'skyBlue', 'slateBlue', 'slateGray', 'slateGrey', 'snow', 'springGreen',
+        'steelBlue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet',
+        'wheat', 'white', 'whiteSmoke', 'yellow', 'yellowGreen'
+    ]
+
+
+SCHEME_COLORS= ['bg1', 'tx1', 'bg2', 'tx2', 'accent1', 'accent2', 'accent3',
+                'accent4', 'accent5', 'accent6', 'hlink', 'folHlink', 'phClr', 'dk1', 'lt1',
+                'dk2', 'lt2'
+                ]
+
+
+class Transform(Serialisable):
+
+    pass
+
+
+class SystemColor(Serialisable):
+
+    tagname = "sysClr"
+    namespace = DRAWING_NS
+
+    # color transform options
+    tint = NestedInteger(allow_none=True)
+    shade = NestedInteger(allow_none=True)
+    comp = Typed(expected_type=Transform, allow_none=True)
+    inv = Typed(expected_type=Transform, allow_none=True)
+    gray = Typed(expected_type=Transform, allow_none=True)
+    alpha = NestedInteger(allow_none=True)
+    alphaOff = NestedInteger(allow_none=True)
+    alphaMod = NestedInteger(allow_none=True)
+    hue = NestedInteger(allow_none=True)
+    hueOff = NestedInteger(allow_none=True)
+    hueMod = NestedInteger(allow_none=True)
+    sat = NestedInteger(allow_none=True)
+    satOff = NestedInteger(allow_none=True)
+    satMod = NestedInteger(allow_none=True)
+    lum = NestedInteger(allow_none=True)
+    lumOff = NestedInteger(allow_none=True)
+    lumMod = NestedInteger(allow_none=True)
+    red = NestedInteger(allow_none=True)
+    redOff = NestedInteger(allow_none=True)
+    redMod = NestedInteger(allow_none=True)
+    green = NestedInteger(allow_none=True)
+    greenOff = NestedInteger(allow_none=True)
+    greenMod = NestedInteger(allow_none=True)
+    blue = NestedInteger(allow_none=True)
+    blueOff = NestedInteger(allow_none=True)
+    blueMod = NestedInteger(allow_none=True)
+    gamma = Typed(expected_type=Transform, allow_none=True)
+    invGamma = Typed(expected_type=Transform, allow_none=True)
+
+    val = Set(values=( ['scrollBar', 'background', 'activeCaption',
+                        'inactiveCaption', 'menu', 'window', 'windowFrame', 'menuText',
+                        'windowText', 'captionText', 'activeBorder', 'inactiveBorder',
+                        'appWorkspace', 'highlight', 'highlightText', 'btnFace', 'btnShadow',
+                        'grayText', 'btnText', 'inactiveCaptionText', 'btnHighlight',
+                        '3dDkShadow', '3dLight', 'infoText', 'infoBk', 'hotLight',
+                        'gradientActiveCaption', 'gradientInactiveCaption', 'menuHighlight',
+                        'menuBar'] )
+              )
+    lastClr = RGB(allow_none=True)
+
+    __elements__ = ('tint', 'shade', 'comp', 'inv', 'gray', "alpha",
+                    "alphaOff", "alphaMod", "hue", "hueOff", "hueMod", "hueOff", "sat",
+                    "satOff", "satMod", "lum", "lumOff", "lumMod", "red", "redOff", "redMod",
+                    "green", "greenOff", "greenMod", "blue", "blueOff", "blueMod", "gamma",
+                    "invGamma")
+
+    def __init__(self,
+                 val="windowText",
+                 lastClr=None,
+                 tint=None,
+                 shade=None,
+                 comp=None,
+                 inv=None,
+                 gray=None,
+                 alpha=None,
+                 alphaOff=None,
+                 alphaMod=None,
+                 hue=None,
+                 hueOff=None,
+                 hueMod=None,
+                 sat=None,
+                 satOff=None,
+                 satMod=None,
+                 lum=None,
+                 lumOff=None,
+                 lumMod=None,
+                 red=None,
+                 redOff=None,
+                 redMod=None,
+                 green=None,
+                 greenOff=None,
+                 greenMod=None,
+                 blue=None,
+                 blueOff=None,
+                 blueMod=None,
+                 gamma=None,
+                 invGamma=None
+                ):
+        self.val = val
+        self.lastClr = lastClr
+        self.tint = tint
+        self.shade = shade
+        self.comp = comp
+        self.inv = inv
+        self.gray = gray
+        self.alpha = alpha
+        self.alphaOff = alphaOff
+        self.alphaMod = alphaMod
+        self.hue = hue
+        self.hueOff = hueOff
+        self.hueMod = hueMod
+        self.sat = sat
+        self.satOff = satOff
+        self.satMod = satMod
+        self.lum = lum
+        self.lumOff = lumOff
+        self.lumMod = lumMod
+        self.red = red
+        self.redOff = redOff
+        self.redMod = redMod
+        self.green = green
+        self.greenOff = greenOff
+        self.greenMod = greenMod
+        self.blue = blue
+        self.blueOff = blueOff
+        self.blueMod = blueMod
+        self.gamma = gamma
+        self.invGamma = invGamma
+
+
+class HSLColor(Serialisable):
+
+    tagname = "hslClr"
+
+    hue = Integer()
+    sat = MinMax(min=0, max=100)
+    lum = MinMax(min=0, max=100)
+
+    #TODO add color transform options
+
+    def __init__(self,
+                 hue=None,
+                 sat=None,
+                 lum=None,
+                ):
+        self.hue = hue
+        self.sat = sat
+        self.lum = lum
+
+
+
+class RGBPercent(Serialisable):
+
+    tagname = "rgbClr"
+
+    r = MinMax(min=0, max=100)
+    g = MinMax(min=0, max=100)
+    b = MinMax(min=0, max=100)
+
+    #TODO add color transform options
+
+    def __init__(self,
+                 r=None,
+                 g=None,
+                 b=None,
+                ):
+        self.r = r
+        self.g = g
+        self.b = b
+
+
+class SchemeColor(Serialisable):
+
+    tagname = "schemeClr"
+    namespace = DRAWING_NS
+
+    tint = NestedInteger(allow_none=True)
+    shade = NestedInteger(allow_none=True)
+    comp = EmptyTag(allow_none=True)
+    inv = NestedInteger(allow_none=True)
+    gray = NestedInteger(allow_none=True)
+    alpha = NestedInteger(allow_none=True)
+    alphaOff = NestedInteger(allow_none=True)
+    alphaMod = NestedInteger(allow_none=True)
+    hue = NestedInteger(allow_none=True)
+    hueOff = NestedInteger(allow_none=True)
+    hueMod = NestedInteger(allow_none=True)
+    sat = NestedInteger(allow_none=True)
+    satOff = NestedInteger(allow_none=True)
+    satMod = NestedInteger(allow_none=True)
+    lum = NestedInteger(allow_none=True)
+    lumOff = NestedInteger(allow_none=True)
+    lumMod = NestedInteger(allow_none=True)
+    red = NestedInteger(allow_none=True)
+    redOff = NestedInteger(allow_none=True)
+    redMod = NestedInteger(allow_none=True)
+    green = NestedInteger(allow_none=True)
+    greenOff = NestedInteger(allow_none=True)
+    greenMod = NestedInteger(allow_none=True)
+    blue = NestedInteger(allow_none=True)
+    blueOff = NestedInteger(allow_none=True)
+    blueMod = NestedInteger(allow_none=True)
+    gamma = EmptyTag(allow_none=True)
+    invGamma = EmptyTag(allow_none=True)
+    val = Set(values=(['bg1', 'tx1', 'bg2', 'tx2', 'accent1', 'accent2',
+                       'accent3', 'accent4', 'accent5', 'accent6', 'hlink', 'folHlink', 'phClr',
+                       'dk1', 'lt1', 'dk2', 'lt2']))
+
+    __elements__ = ('tint', 'shade', 'comp', 'inv', 'gray', 'alpha',
+                    'alphaOff', 'alphaMod', 'hue', 'hueOff', 'hueMod', 'sat', 'satOff',
+                    'satMod', 'lum', 'lumMod', 'lumOff', 'red', 'redOff', 'redMod', 'green',
+                    'greenOff', 'greenMod', 'blue', 'blueOff', 'blueMod', 'gamma',
+                    'invGamma')
+
+    def __init__(self,
+                 tint=None,
+                 shade=None,
+                 comp=None,
+                 inv=None,
+                 gray=None,
+                 alpha=None,
+                 alphaOff=None,
+                 alphaMod=None,
+                 hue=None,
+                 hueOff=None,
+                 hueMod=None,
+                 sat=None,
+                 satOff=None,
+                 satMod=None,
+                 lum=None,
+                 lumOff=None,
+                 lumMod=None,
+                 red=None,
+                 redOff=None,
+                 redMod=None,
+                 green=None,
+                 greenOff=None,
+                 greenMod=None,
+                 blue=None,
+                 blueOff=None,
+                 blueMod=None,
+                 gamma=None,
+                 invGamma=None,
+                 val=None,
+                ):
+        self.tint = tint
+        self.shade = shade
+        self.comp = comp
+        self.inv = inv
+        self.gray = gray
+        self.alpha = alpha
+        self.alphaOff = alphaOff
+        self.alphaMod = alphaMod
+        self.hue = hue
+        self.hueOff = hueOff
+        self.hueMod = hueMod
+        self.sat = sat
+        self.satOff = satOff
+        self.satMod = satMod
+        self.lum = lum
+        self.lumOff = lumOff
+        self.lumMod = lumMod
+        self.red = red
+        self.redOff = redOff
+        self.redMod = redMod
+        self.green = green
+        self.greenOff = greenOff
+        self.greenMod = greenMod
+        self.blue = blue
+        self.blueOff = blueOff
+        self.blueMod = blueMod
+        self.gamma = gamma
+        self.invGamma = invGamma
+        self.val = val
+
+class ColorChoice(Serialisable):
+
+    tagname = "colorChoice"
+    namespace = DRAWING_NS
+
+    scrgbClr = Typed(expected_type=RGBPercent, allow_none=True)
+    RGBPercent = Alias('scrgbClr')
+    srgbClr = NestedValue(expected_type=str, allow_none=True) # needs pattern and can have transform
+    RGB = Alias('srgbClr')
+    hslClr = Typed(expected_type=HSLColor, allow_none=True)
+    sysClr = Typed(expected_type=SystemColor, allow_none=True)
+    schemeClr = Typed(expected_type=SchemeColor, allow_none=True)
+    prstClr = NestedNoneSet(values=PRESET_COLORS)
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 scrgbClr=None,
+                 srgbClr=None,
+                 hslClr=None,
+                 sysClr=None,
+                 schemeClr=None,
+                 prstClr=None,
+                ):
+        self.scrgbClr = scrgbClr
+        self.srgbClr = srgbClr
+        self.hslClr = hslClr
+        self.sysClr = sysClr
+        self.schemeClr = schemeClr
+        self.prstClr = prstClr
+
+_COLOR_SET = ('dk1', 'lt1', 'dk2', 'lt2', 'accent1', 'accent2', 'accent3',
+               'accent4', 'accent5', 'accent6', 'hlink', 'folHlink')
+
+
+class ColorMapping(Serialisable):
+
+    tagname = "clrMapOvr"
+
+    bg1 = Set(values=_COLOR_SET)
+    tx1 = Set(values=_COLOR_SET)
+    bg2 = Set(values=_COLOR_SET)
+    tx2 = Set(values=_COLOR_SET)
+    accent1 = Set(values=_COLOR_SET)
+    accent2 = Set(values=_COLOR_SET)
+    accent3 = Set(values=_COLOR_SET)
+    accent4 = Set(values=_COLOR_SET)
+    accent5 = Set(values=_COLOR_SET)
+    accent6 = Set(values=_COLOR_SET)
+    hlink = Set(values=_COLOR_SET)
+    folHlink = Set(values=_COLOR_SET)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 bg1="lt1",
+                 tx1="dk1",
+                 bg2="lt2",
+                 tx2="dk2",
+                 accent1="accent1",
+                 accent2="accent2",
+                 accent3="accent3",
+                 accent4="accent4",
+                 accent5="accent5",
+                 accent6="accent6",
+                 hlink="hlink",
+                 folHlink="folHlink",
+                 extLst=None,
+                ):
+        self.bg1 = bg1
+        self.tx1 = tx1
+        self.bg2 = bg2
+        self.tx2 = tx2
+        self.accent1 = accent1
+        self.accent2 = accent2
+        self.accent3 = accent3
+        self.accent4 = accent4
+        self.accent5 = accent5
+        self.accent6 = accent6
+        self.hlink = hlink
+        self.folHlink = folHlink
+        self.extLst = extLst
+
+
+class ColorChoiceDescriptor(Typed):
+    """
+    Objects can choose from 7 different kinds of color system.
+    Assume RGBHex if a string is passed in.
+    """
+
+    expected_type = ColorChoice
+    allow_none = True
+
+    def __set__(self, instance, value):
+        if isinstance(value, str):
+            value = ColorChoice(srgbClr=value)
+        else:
+            if hasattr(self, "namespace") and value is not None:
+                value.namespace = self.namespace
+        super().__set__(instance, value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/connector.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/connector.py
new file mode 100644
index 00000000..d25bcf71
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/connector.py
@@ -0,0 +1,144 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    Integer,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+from openpyxl.chart.shapes import GraphicalProperties
+from openpyxl.chart.text import RichText
+
+from .properties import (
+    NonVisualDrawingProps,
+    NonVisualDrawingShapeProps,
+)
+from .geometry import ShapeStyle
+
+class Connection(Serialisable):
+
+    id = Integer()
+    idx = Integer()
+
+    def __init__(self,
+                 id=None,
+                 idx=None,
+                ):
+        self.id = id
+        self.idx = idx
+
+
+class ConnectorLocking(Serialisable):
+
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 extLst=None,
+                ):
+        self.extLst = extLst
+
+
+class NonVisualConnectorProperties(Serialisable):
+
+    cxnSpLocks = Typed(expected_type=ConnectorLocking, allow_none=True)
+    stCxn = Typed(expected_type=Connection, allow_none=True)
+    endCxn = Typed(expected_type=Connection, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 cxnSpLocks=None,
+                 stCxn=None,
+                 endCxn=None,
+                 extLst=None,
+                ):
+        self.cxnSpLocks = cxnSpLocks
+        self.stCxn = stCxn
+        self.endCxn = endCxn
+        self.extLst = extLst
+
+
+class ConnectorNonVisual(Serialisable):
+
+    cNvPr = Typed(expected_type=NonVisualDrawingProps, )
+    cNvCxnSpPr = Typed(expected_type=NonVisualConnectorProperties, )
+
+    __elements__ = ("cNvPr", "cNvCxnSpPr",)
+
+    def __init__(self,
+                 cNvPr=None,
+                 cNvCxnSpPr=None,
+                ):
+        self.cNvPr = cNvPr
+        self.cNvCxnSpPr = cNvCxnSpPr
+
+
+class ConnectorShape(Serialisable):
+
+    tagname = "cxnSp"
+
+    nvCxnSpPr = Typed(expected_type=ConnectorNonVisual)
+    spPr = Typed(expected_type=GraphicalProperties)
+    style = Typed(expected_type=ShapeStyle, allow_none=True)
+    macro = String(allow_none=True)
+    fPublished = Bool(allow_none=True)
+
+    def __init__(self,
+                 nvCxnSpPr=None,
+                 spPr=None,
+                 style=None,
+                 macro=None,
+                 fPublished=None,
+                 ):
+        self.nvCxnSpPr = nvCxnSpPr
+        self.spPr = spPr
+        self.style = style
+        self.macro = macro
+        self.fPublished = fPublished
+
+
+class ShapeMeta(Serialisable):
+
+    tagname = "nvSpPr"
+
+    cNvPr = Typed(expected_type=NonVisualDrawingProps)
+    cNvSpPr = Typed(expected_type=NonVisualDrawingShapeProps)
+
+    def __init__(self, cNvPr=None, cNvSpPr=None):
+        self.cNvPr = cNvPr
+        self.cNvSpPr = cNvSpPr
+
+
+class Shape(Serialisable):
+
+    macro = String(allow_none=True)
+    textlink = String(allow_none=True)
+    fPublished = Bool(allow_none=True)
+    fLocksText = Bool(allow_none=True)
+    nvSpPr = Typed(expected_type=ShapeMeta, allow_none=True)
+    meta = Alias("nvSpPr")
+    spPr = Typed(expected_type=GraphicalProperties)
+    graphicalProperties = Alias("spPr")
+    style = Typed(expected_type=ShapeStyle, allow_none=True)
+    txBody = Typed(expected_type=RichText, allow_none=True)
+
+    def __init__(self,
+                 macro=None,
+                 textlink=None,
+                 fPublished=None,
+                 fLocksText=None,
+                 nvSpPr=None,
+                 spPr=None,
+                 style=None,
+                 txBody=None,
+                ):
+        self.macro = macro
+        self.textlink = textlink
+        self.fPublished = fPublished
+        self.fLocksText = fLocksText
+        self.nvSpPr = nvSpPr
+        self.spPr = spPr
+        self.style = style
+        self.txBody = txBody
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/drawing.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/drawing.py
new file mode 100644
index 00000000..45acdfe5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/drawing.py
@@ -0,0 +1,92 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+import math
+
+from openpyxl.utils.units import pixels_to_EMU
+
+
+class Drawing:
+    """ a drawing object - eg container for shapes or charts
+        we assume user specifies dimensions in pixels; units are
+        converted to EMU in the drawing part
+    """
+
+    count = 0
+
+    def __init__(self):
+
+        self.name = ''
+        self.description = ''
+        self.coordinates = ((1, 2), (16, 8))
+        self.left = 0
+        self.top = 0
+        self._width = 21 # default in px
+        self._height = 192 #default in px
+        self.resize_proportional = False
+        self.rotation = 0
+        self.anchortype = "absolute"
+        self.anchorcol = 0 # left cell
+        self.anchorrow = 0 # top row
+
+
+    @property
+    def width(self):
+        return self._width
+
+
+    @width.setter
+    def width(self, w):
+        if self.resize_proportional and w:
+            ratio = self._height / self._width
+            self._height = round(ratio * w)
+        self._width = w
+
+
+    @property
+    def height(self):
+        return self._height
+
+
+    @height.setter
+    def height(self, h):
+        if self.resize_proportional and h:
+            ratio = self._width / self._height
+            self._width = round(ratio * h)
+        self._height = h
+
+
+    def set_dimension(self, w=0, h=0):
+
+        xratio = w / self._width
+        yratio = h / self._height
+
+        if self.resize_proportional and w and h:
+            if (xratio * self._height) < h:
+                self._height = math.ceil(xratio * self._height)
+                self._width = w
+            else:
+                self._width = math.ceil(yratio * self._width)
+                self._height = h
+
+
+    @property
+    def anchor(self):
+        from .spreadsheet_drawing import (
+            OneCellAnchor,
+            TwoCellAnchor,
+            AbsoluteAnchor)
+        if self.anchortype == "absolute":
+            anchor = AbsoluteAnchor()
+            anchor.pos.x = pixels_to_EMU(self.left)
+            anchor.pos.y = pixels_to_EMU(self.top)
+
+        elif self.anchortype == "oneCell":
+            anchor = OneCellAnchor()
+            anchor._from.col = self.anchorcol
+            anchor._from.row = self.anchorrow
+
+        anchor.ext.width = pixels_to_EMU(self._width)
+        anchor.ext.height = pixels_to_EMU(self._height)
+
+        return anchor
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/effect.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/effect.py
new file mode 100644
index 00000000..9edae342
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/effect.py
@@ -0,0 +1,407 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Set,
+    Bool,
+    Integer,
+    Float,
+)
+
+from .colors import ColorChoice
+
+
+class TintEffect(Serialisable):
+
+    tagname = "tint"
+
+    hue = Integer()
+    amt = Integer()
+
+    def __init__(self,
+                 hue=0,
+                 amt=0,
+                ):
+        self.hue = hue
+        self.amt = amt
+
+
+class LuminanceEffect(Serialisable):
+
+    tagname = "lum"
+
+    bright = Integer() #Pct ?
+    contrast = Integer() #Pct#
+
+    def __init__(self,
+                 bright=0,
+                 contrast=0,
+                ):
+        self.bright = bright
+        self.contrast = contrast
+
+
+class HSLEffect(Serialisable):
+
+    hue = Integer()
+    sat = Integer()
+    lum = Integer()
+
+    def __init__(self,
+                 hue=None,
+                 sat=None,
+                 lum=None,
+                ):
+        self.hue = hue
+        self.sat = sat
+        self.lum = lum
+
+
+class GrayscaleEffect(Serialisable):
+
+    tagname = "grayscl"
+
+
+class FillOverlayEffect(Serialisable):
+
+    blend = Set(values=(['over', 'mult', 'screen', 'darken', 'lighten']))
+
+    def __init__(self,
+                 blend=None,
+                ):
+        self.blend = blend
+
+
+class DuotoneEffect(Serialisable):
+
+    pass
+
+class ColorReplaceEffect(Serialisable):
+
+    pass
+
+class Color(Serialisable):
+
+    pass
+
+class ColorChangeEffect(Serialisable):
+
+    useA = Bool(allow_none=True)
+    clrFrom = Typed(expected_type=Color, )
+    clrTo = Typed(expected_type=Color, )
+
+    def __init__(self,
+                 useA=None,
+                 clrFrom=None,
+                 clrTo=None,
+                ):
+        self.useA = useA
+        self.clrFrom = clrFrom
+        self.clrTo = clrTo
+
+
+class BlurEffect(Serialisable):
+
+    rad = Float()
+    grow = Bool(allow_none=True)
+
+    def __init__(self,
+                 rad=None,
+                 grow=None,
+                ):
+        self.rad = rad
+        self.grow = grow
+
+
+class BiLevelEffect(Serialisable):
+
+    thresh = Integer()
+
+    def __init__(self,
+                 thresh=None,
+                ):
+        self.thresh = thresh
+
+
+class AlphaReplaceEffect(Serialisable):
+
+    a = Integer()
+
+    def __init__(self,
+                 a=None,
+                ):
+        self.a = a
+
+
+class AlphaModulateFixedEffect(Serialisable):
+
+    amt = Integer()
+
+    def __init__(self,
+                 amt=None,
+                ):
+        self.amt = amt
+
+
+class EffectContainer(Serialisable):
+
+    type = Set(values=(['sib', 'tree']))
+    name = String(allow_none=True)
+
+    def __init__(self,
+                 type=None,
+                 name=None,
+                ):
+        self.type = type
+        self.name = name
+
+
+class AlphaModulateEffect(Serialisable):
+
+    cont = Typed(expected_type=EffectContainer, )
+
+    def __init__(self,
+                 cont=None,
+                ):
+        self.cont = cont
+
+
+class AlphaInverseEffect(Serialisable):
+
+    pass
+
+class AlphaFloorEffect(Serialisable):
+
+    pass
+
+class AlphaCeilingEffect(Serialisable):
+
+    pass
+
+class AlphaBiLevelEffect(Serialisable):
+
+    thresh = Integer()
+
+    def __init__(self,
+                 thresh=None,
+                ):
+        self.thresh = thresh
+
+
+class GlowEffect(ColorChoice):
+
+    rad = Float()
+    # uses element group EG_ColorChoice
+    scrgbClr = ColorChoice.scrgbClr
+    srgbClr = ColorChoice.srgbClr
+    hslClr = ColorChoice.hslClr
+    sysClr = ColorChoice.sysClr
+    schemeClr = ColorChoice.schemeClr
+    prstClr = ColorChoice.prstClr
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 rad=None,
+                 **kw
+                ):
+        self.rad = rad
+        super().__init__(**kw)
+
+
+class InnerShadowEffect(ColorChoice):
+
+    blurRad = Float()
+    dist = Float()
+    dir = Integer()
+    # uses element group EG_ColorChoice
+    scrgbClr = ColorChoice.scrgbClr
+    srgbClr = ColorChoice.srgbClr
+    hslClr = ColorChoice.hslClr
+    sysClr = ColorChoice.sysClr
+    schemeClr = ColorChoice.schemeClr
+    prstClr = ColorChoice.prstClr
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 blurRad=None,
+                 dist=None,
+                 dir=None,
+                 **kw
+                 ):
+        self.blurRad = blurRad
+        self.dist = dist
+        self.dir = dir
+        super().__init__(**kw)
+
+
+class OuterShadow(ColorChoice):
+
+    tagname = "outerShdw"
+
+    blurRad = Float(allow_none=True)
+    dist = Float(allow_none=True)
+    dir = Integer(allow_none=True)
+    sx = Integer(allow_none=True)
+    sy = Integer(allow_none=True)
+    kx = Integer(allow_none=True)
+    ky = Integer(allow_none=True)
+    algn = Set(values=['tl', 't', 'tr', 'l', 'ctr', 'r', 'bl', 'b', 'br'])
+    rotWithShape = Bool(allow_none=True)
+    # uses element group EG_ColorChoice
+    scrgbClr = ColorChoice.scrgbClr
+    srgbClr = ColorChoice.srgbClr
+    hslClr = ColorChoice.hslClr
+    sysClr = ColorChoice.sysClr
+    schemeClr = ColorChoice.schemeClr
+    prstClr = ColorChoice.prstClr
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 blurRad=None,
+                 dist=None,
+                 dir=None,
+                 sx=None,
+                 sy=None,
+                 kx=None,
+                 ky=None,
+                 algn=None,
+                 rotWithShape=None,
+                 **kw
+                ):
+        self.blurRad = blurRad
+        self.dist = dist
+        self.dir = dir
+        self.sx = sx
+        self.sy = sy
+        self.kx = kx
+        self.ky = ky
+        self.algn = algn
+        self.rotWithShape = rotWithShape
+        super().__init__(**kw)
+
+
+class PresetShadowEffect(ColorChoice):
+
+    prst = Set(values=(['shdw1', 'shdw2', 'shdw3', 'shdw4', 'shdw5', 'shdw6',
+                        'shdw7', 'shdw8', 'shdw9', 'shdw10', 'shdw11', 'shdw12', 'shdw13',
+                        'shdw14', 'shdw15', 'shdw16', 'shdw17', 'shdw18', 'shdw19', 'shdw20']))
+    dist = Float()
+    dir = Integer()
+    # uses element group EG_ColorChoice
+    scrgbClr = ColorChoice.scrgbClr
+    srgbClr = ColorChoice.srgbClr
+    hslClr = ColorChoice.hslClr
+    sysClr = ColorChoice.sysClr
+    schemeClr = ColorChoice.schemeClr
+    prstClr = ColorChoice.prstClr
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 prst=None,
+                 dist=None,
+                 dir=None,
+                 **kw
+                ):
+        self.prst = prst
+        self.dist = dist
+        self.dir = dir
+        super().__init__(**kw)
+
+
+class ReflectionEffect(Serialisable):
+
+    blurRad = Float()
+    stA = Integer()
+    stPos = Integer()
+    endA = Integer()
+    endPos = Integer()
+    dist = Float()
+    dir = Integer()
+    fadeDir = Integer()
+    sx = Integer()
+    sy = Integer()
+    kx = Integer()
+    ky = Integer()
+    algn = Set(values=(['tl', 't', 'tr', 'l', 'ctr', 'r', 'bl', 'b', 'br']))
+    rotWithShape = Bool(allow_none=True)
+
+    def __init__(self,
+                 blurRad=None,
+                 stA=None,
+                 stPos=None,
+                 endA=None,
+                 endPos=None,
+                 dist=None,
+                 dir=None,
+                 fadeDir=None,
+                 sx=None,
+                 sy=None,
+                 kx=None,
+                 ky=None,
+                 algn=None,
+                 rotWithShape=None,
+                ):
+        self.blurRad = blurRad
+        self.stA = stA
+        self.stPos = stPos
+        self.endA = endA
+        self.endPos = endPos
+        self.dist = dist
+        self.dir = dir
+        self.fadeDir = fadeDir
+        self.sx = sx
+        self.sy = sy
+        self.kx = kx
+        self.ky = ky
+        self.algn = algn
+        self.rotWithShape = rotWithShape
+
+
+class SoftEdgesEffect(Serialisable):
+
+    rad = Float()
+
+    def __init__(self,
+                 rad=None,
+                ):
+        self.rad = rad
+
+
+class EffectList(Serialisable):
+
+    blur = Typed(expected_type=BlurEffect, allow_none=True)
+    fillOverlay = Typed(expected_type=FillOverlayEffect, allow_none=True)
+    glow = Typed(expected_type=GlowEffect, allow_none=True)
+    innerShdw = Typed(expected_type=InnerShadowEffect, allow_none=True)
+    outerShdw = Typed(expected_type=OuterShadow, allow_none=True)
+    prstShdw = Typed(expected_type=PresetShadowEffect, allow_none=True)
+    reflection = Typed(expected_type=ReflectionEffect, allow_none=True)
+    softEdge = Typed(expected_type=SoftEdgesEffect, allow_none=True)
+
+    __elements__ = ('blur', 'fillOverlay', 'glow', 'innerShdw', 'outerShdw',
+                    'prstShdw', 'reflection', 'softEdge')
+
+    def __init__(self,
+                 blur=None,
+                 fillOverlay=None,
+                 glow=None,
+                 innerShdw=None,
+                 outerShdw=None,
+                 prstShdw=None,
+                 reflection=None,
+                 softEdge=None,
+                ):
+        self.blur = blur
+        self.fillOverlay = fillOverlay
+        self.glow = glow
+        self.innerShdw = innerShdw
+        self.outerShdw = outerShdw
+        self.prstShdw = prstShdw
+        self.reflection = reflection
+        self.softEdge = softEdge
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/fill.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/fill.py
new file mode 100644
index 00000000..580e0db2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/fill.py
@@ -0,0 +1,425 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Bool,
+    Integer,
+    Set,
+    NoneSet,
+    Typed,
+    MinMax,
+)
+from openpyxl.descriptors.excel import (
+    Relation,
+    Percentage,
+)
+from openpyxl.descriptors.nested import NestedNoneSet, NestedValue
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+from openpyxl.xml.constants import DRAWING_NS
+
+from .colors import (
+    ColorChoice,
+    HSLColor,
+    SystemColor,
+    SchemeColor,
+    PRESET_COLORS,
+    RGBPercent,
+)
+
+from .effect import (
+    AlphaBiLevelEffect,
+    AlphaCeilingEffect,
+    AlphaFloorEffect,
+    AlphaInverseEffect,
+    AlphaModulateEffect,
+    AlphaModulateFixedEffect,
+    AlphaReplaceEffect,
+    BiLevelEffect,
+    BlurEffect,
+    ColorChangeEffect,
+    ColorReplaceEffect,
+    DuotoneEffect,
+    FillOverlayEffect,
+    GrayscaleEffect,
+    HSLEffect,
+    LuminanceEffect,
+    TintEffect,
+)
+
+"""
+Fill elements from drawing main schema
+"""
+
+class PatternFillProperties(Serialisable):
+
+    tagname = "pattFill"
+    namespace = DRAWING_NS
+
+    prst = NoneSet(values=(['pct5', 'pct10', 'pct20', 'pct25', 'pct30',
+                            'pct40', 'pct50', 'pct60', 'pct70', 'pct75', 'pct80', 'pct90', 'horz',
+                            'vert', 'ltHorz', 'ltVert', 'dkHorz', 'dkVert', 'narHorz', 'narVert',
+                            'dashHorz', 'dashVert', 'cross', 'dnDiag', 'upDiag', 'ltDnDiag',
+                            'ltUpDiag', 'dkDnDiag', 'dkUpDiag', 'wdDnDiag', 'wdUpDiag', 'dashDnDiag',
+                            'dashUpDiag', 'diagCross', 'smCheck', 'lgCheck', 'smGrid', 'lgGrid',
+                            'dotGrid', 'smConfetti', 'lgConfetti', 'horzBrick', 'diagBrick',
+                            'solidDmnd', 'openDmnd', 'dotDmnd', 'plaid', 'sphere', 'weave', 'divot',
+                            'shingle', 'wave', 'trellis', 'zigZag']))
+    preset = Alias("prst")
+    fgClr = Typed(expected_type=ColorChoice, allow_none=True)
+    foreground = Alias("fgClr")
+    bgClr = Typed(expected_type=ColorChoice, allow_none=True)
+    background = Alias("bgClr")
+
+    __elements__ = ("fgClr", "bgClr")
+
+    def __init__(self,
+                 prst=None,
+                 fgClr=None,
+                 bgClr=None,
+                ):
+        self.prst = prst
+        self.fgClr = fgClr
+        self.bgClr = bgClr
+
+
+class RelativeRect(Serialisable):
+
+    tagname = "rect"
+    namespace = DRAWING_NS
+
+    l = Percentage(allow_none=True)
+    left = Alias('l')
+    t = Percentage(allow_none=True)
+    top = Alias('t')
+    r = Percentage(allow_none=True)
+    right = Alias('r')
+    b = Percentage(allow_none=True)
+    bottom = Alias('b')
+
+    def __init__(self,
+                 l=None,
+                 t=None,
+                 r=None,
+                 b=None,
+                ):
+        self.l = l
+        self.t = t
+        self.r = r
+        self.b = b
+
+
+class StretchInfoProperties(Serialisable):
+
+    tagname = "stretch"
+    namespace = DRAWING_NS
+
+    fillRect = Typed(expected_type=RelativeRect, allow_none=True)
+
+    def __init__(self,
+                 fillRect=RelativeRect(),
+                ):
+        self.fillRect = fillRect
+
+
+class GradientStop(Serialisable):
+
+    tagname = "gs"
+    namespace = DRAWING_NS
+
+    pos = MinMax(min=0, max=100000, allow_none=True)
+    # Color Choice Group
+    scrgbClr = Typed(expected_type=RGBPercent, allow_none=True)
+    RGBPercent = Alias('scrgbClr')
+    srgbClr = NestedValue(expected_type=str, allow_none=True) # needs pattern and can have transform
+    RGB = Alias('srgbClr')
+    hslClr = Typed(expected_type=HSLColor, allow_none=True)
+    sysClr = Typed(expected_type=SystemColor, allow_none=True)
+    schemeClr = Typed(expected_type=SchemeColor, allow_none=True)
+    prstClr = NestedNoneSet(values=PRESET_COLORS)
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 pos=None,
+                 scrgbClr=None,
+                 srgbClr=None,
+                 hslClr=None,
+                 sysClr=None,
+                 schemeClr=None,
+                 prstClr=None,
+                ):
+        if pos is None:
+            pos = 0
+        self.pos = pos
+
+        self.scrgbClr = scrgbClr
+        self.srgbClr = srgbClr
+        self.hslClr = hslClr
+        self.sysClr = sysClr
+        self.schemeClr = schemeClr
+        self.prstClr = prstClr
+
+
+class LinearShadeProperties(Serialisable):
+
+    tagname = "lin"
+    namespace = DRAWING_NS
+
+    ang = Integer()
+    scaled = Bool(allow_none=True)
+
+    def __init__(self,
+                 ang=None,
+                 scaled=None,
+                ):
+        self.ang = ang
+        self.scaled = scaled
+
+
+class PathShadeProperties(Serialisable):
+
+    tagname = "path"
+    namespace = DRAWING_NS
+
+    path = Set(values=(['shape', 'circle', 'rect']))
+    fillToRect = Typed(expected_type=RelativeRect, allow_none=True)
+
+    def __init__(self,
+                 path=None,
+                 fillToRect=None,
+                ):
+        self.path = path
+        self.fillToRect = fillToRect
+
+
+class GradientFillProperties(Serialisable):
+
+    tagname = "gradFill"
+    namespace = DRAWING_NS
+
+    flip = NoneSet(values=(['x', 'y', 'xy']))
+    rotWithShape = Bool(allow_none=True)
+
+    gsLst = NestedSequence(expected_type=GradientStop, count=False)
+    stop_list = Alias("gsLst")
+
+    lin = Typed(expected_type=LinearShadeProperties, allow_none=True)
+    linear = Alias("lin")
+    path = Typed(expected_type=PathShadeProperties, allow_none=True)
+
+    tileRect = Typed(expected_type=RelativeRect, allow_none=True)
+
+    __elements__ = ('gsLst', 'lin', 'path', 'tileRect')
+
+    def __init__(self,
+                 flip=None,
+                 rotWithShape=None,
+                 gsLst=(),
+                 lin=None,
+                 path=None,
+                 tileRect=None,
+                ):
+        self.flip = flip
+        self.rotWithShape = rotWithShape
+        self.gsLst = gsLst
+        self.lin = lin
+        self.path = path
+        self.tileRect = tileRect
+
+
+class SolidColorFillProperties(Serialisable):
+
+    tagname = "solidFill"
+
+    # uses element group EG_ColorChoice
+    scrgbClr = Typed(expected_type=RGBPercent, allow_none=True)
+    RGBPercent = Alias('scrgbClr')
+    srgbClr = NestedValue(expected_type=str, allow_none=True) # needs pattern and can have transform
+    RGB = Alias('srgbClr')
+    hslClr = Typed(expected_type=HSLColor, allow_none=True)
+    sysClr = Typed(expected_type=SystemColor, allow_none=True)
+    schemeClr = Typed(expected_type=SchemeColor, allow_none=True)
+    prstClr = NestedNoneSet(values=PRESET_COLORS)
+
+    __elements__ = ('scrgbClr', 'srgbClr', 'hslClr', 'sysClr', 'schemeClr', 'prstClr')
+
+    def __init__(self,
+                 scrgbClr=None,
+                 srgbClr=None,
+                 hslClr=None,
+                 sysClr=None,
+                 schemeClr=None,
+                 prstClr=None,
+                ):
+        self.scrgbClr = scrgbClr
+        self.srgbClr = srgbClr
+        self.hslClr = hslClr
+        self.sysClr = sysClr
+        self.schemeClr = schemeClr
+        self.prstClr = prstClr
+
+
+class Blip(Serialisable):
+
+    tagname = "blip"
+    namespace = DRAWING_NS
+
+    # Using attribute groupAG_Blob
+    cstate = NoneSet(values=(['email', 'screen', 'print', 'hqprint']))
+    embed = Relation() # rId
+    link = Relation() # hyperlink
+    noGrp = Bool(allow_none=True)
+    noSelect = Bool(allow_none=True)
+    noRot = Bool(allow_none=True)
+    noChangeAspect = Bool(allow_none=True)
+    noMove = Bool(allow_none=True)
+    noResize = Bool(allow_none=True)
+    noEditPoints = Bool(allow_none=True)
+    noAdjustHandles = Bool(allow_none=True)
+    noChangeArrowheads = Bool(allow_none=True)
+    noChangeShapeType = Bool(allow_none=True)
+    # some elements are choice
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+    alphaBiLevel = Typed(expected_type=AlphaBiLevelEffect, allow_none=True)
+    alphaCeiling = Typed(expected_type=AlphaCeilingEffect, allow_none=True)
+    alphaFloor = Typed(expected_type=AlphaFloorEffect, allow_none=True)
+    alphaInv = Typed(expected_type=AlphaInverseEffect, allow_none=True)
+    alphaMod = Typed(expected_type=AlphaModulateEffect, allow_none=True)
+    alphaModFix = Typed(expected_type=AlphaModulateFixedEffect, allow_none=True)
+    alphaRepl = Typed(expected_type=AlphaReplaceEffect, allow_none=True)
+    biLevel = Typed(expected_type=BiLevelEffect, allow_none=True)
+    blur = Typed(expected_type=BlurEffect, allow_none=True)
+    clrChange = Typed(expected_type=ColorChangeEffect, allow_none=True)
+    clrRepl = Typed(expected_type=ColorReplaceEffect, allow_none=True)
+    duotone = Typed(expected_type=DuotoneEffect, allow_none=True)
+    fillOverlay = Typed(expected_type=FillOverlayEffect, allow_none=True)
+    grayscl = Typed(expected_type=GrayscaleEffect, allow_none=True)
+    hsl = Typed(expected_type=HSLEffect, allow_none=True)
+    lum = Typed(expected_type=LuminanceEffect, allow_none=True)
+    tint = Typed(expected_type=TintEffect, allow_none=True)
+
+    __elements__ = ('alphaBiLevel', 'alphaCeiling', 'alphaFloor', 'alphaInv',
+                    'alphaMod', 'alphaModFix', 'alphaRepl', 'biLevel', 'blur', 'clrChange',
+                    'clrRepl', 'duotone', 'fillOverlay', 'grayscl', 'hsl', 'lum', 'tint')
+
+    def __init__(self,
+                 cstate=None,
+                 embed=None,
+                 link=None,
+                 noGrp=None,
+                 noSelect=None,
+                 noRot=None,
+                 noChangeAspect=None,
+                 noMove=None,
+                 noResize=None,
+                 noEditPoints=None,
+                 noAdjustHandles=None,
+                 noChangeArrowheads=None,
+                 noChangeShapeType=None,
+                 extLst=None,
+                 alphaBiLevel=None,
+                 alphaCeiling=None,
+                 alphaFloor=None,
+                 alphaInv=None,
+                 alphaMod=None,
+                 alphaModFix=None,
+                 alphaRepl=None,
+                 biLevel=None,
+                 blur=None,
+                 clrChange=None,
+                 clrRepl=None,
+                 duotone=None,
+                 fillOverlay=None,
+                 grayscl=None,
+                 hsl=None,
+                 lum=None,
+                 tint=None,
+                ):
+        self.cstate = cstate
+        self.embed = embed
+        self.link = link
+        self.noGrp = noGrp
+        self.noSelect = noSelect
+        self.noRot = noRot
+        self.noChangeAspect = noChangeAspect
+        self.noMove = noMove
+        self.noResize = noResize
+        self.noEditPoints = noEditPoints
+        self.noAdjustHandles = noAdjustHandles
+        self.noChangeArrowheads = noChangeArrowheads
+        self.noChangeShapeType = noChangeShapeType
+        self.extLst = extLst
+        self.alphaBiLevel = alphaBiLevel
+        self.alphaCeiling = alphaCeiling
+        self.alphaFloor = alphaFloor
+        self.alphaInv = alphaInv
+        self.alphaMod = alphaMod
+        self.alphaModFix = alphaModFix
+        self.alphaRepl = alphaRepl
+        self.biLevel = biLevel
+        self.blur = blur
+        self.clrChange = clrChange
+        self.clrRepl = clrRepl
+        self.duotone = duotone
+        self.fillOverlay = fillOverlay
+        self.grayscl = grayscl
+        self.hsl = hsl
+        self.lum = lum
+        self.tint = tint
+
+
+class TileInfoProperties(Serialisable):
+
+    tx = Integer(allow_none=True)
+    ty = Integer(allow_none=True)
+    sx = Integer(allow_none=True)
+    sy = Integer(allow_none=True)
+    flip = NoneSet(values=(['x', 'y', 'xy']))
+    algn = Set(values=(['tl', 't', 'tr', 'l', 'ctr', 'r', 'bl', 'b', 'br']))
+
+    def __init__(self,
+                 tx=None,
+                 ty=None,
+                 sx=None,
+                 sy=None,
+                 flip=None,
+                 algn=None,
+                ):
+        self.tx = tx
+        self.ty = ty
+        self.sx = sx
+        self.sy = sy
+        self.flip = flip
+        self.algn = algn
+
+
+class BlipFillProperties(Serialisable):
+
+    tagname = "blipFill"
+
+    dpi = Integer(allow_none=True)
+    rotWithShape = Bool(allow_none=True)
+
+    blip = Typed(expected_type=Blip, allow_none=True)
+    srcRect = Typed(expected_type=RelativeRect, allow_none=True)
+    tile = Typed(expected_type=TileInfoProperties, allow_none=True)
+    stretch = Typed(expected_type=StretchInfoProperties, allow_none=True)
+
+    __elements__ = ("blip", "srcRect", "tile", "stretch")
+
+    def __init__(self,
+                 dpi=None,
+                 rotWithShape=None,
+                 blip=None,
+                 tile=None,
+                 stretch=StretchInfoProperties(),
+                 srcRect=None,
+                ):
+        self.dpi = dpi
+        self.rotWithShape = rotWithShape
+        self.blip = blip
+        self.tile = tile
+        self.stretch = stretch
+        self.srcRect = srcRect
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/geometry.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/geometry.py
new file mode 100644
index 00000000..2cc7ca63
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/geometry.py
@@ -0,0 +1,584 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Float,
+    Integer,
+    Bool,
+    MinMax,
+    Set,
+    NoneSet,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.excel import Coordinate, Percentage
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+from .line import LineProperties
+
+from openpyxl.styles.colors import Color
+from openpyxl.xml.constants import DRAWING_NS
+
+
+class Point2D(Serialisable):
+
+    tagname = "off"
+    namespace = DRAWING_NS
+
+    x = Coordinate()
+    y = Coordinate()
+
+    def __init__(self,
+                 x=None,
+                 y=None,
+                ):
+        self.x = x
+        self.y = y
+
+
+class PositiveSize2D(Serialisable):
+
+    tagname = "ext"
+    namespace = DRAWING_NS
+
+    """
+    Dimensions in EMUs
+    """
+
+    cx = Integer()
+    width = Alias('cx')
+    cy = Integer()
+    height = Alias('cy')
+
+    def __init__(self,
+                 cx=None,
+                 cy=None,
+                ):
+        self.cx = cx
+        self.cy = cy
+
+
+class Transform2D(Serialisable):
+
+    tagname = "xfrm"
+    namespace = DRAWING_NS
+
+    rot = Integer(allow_none=True)
+    flipH = Bool(allow_none=True)
+    flipV = Bool(allow_none=True)
+    off = Typed(expected_type=Point2D, allow_none=True)
+    ext = Typed(expected_type=PositiveSize2D, allow_none=True)
+    chOff = Typed(expected_type=Point2D, allow_none=True)
+    chExt = Typed(expected_type=PositiveSize2D, allow_none=True)
+
+    __elements__ = ('off', 'ext', 'chOff', 'chExt')
+
+    def __init__(self,
+                 rot=None,
+                 flipH=None,
+                 flipV=None,
+                 off=None,
+                 ext=None,
+                 chOff=None,
+                 chExt=None,
+                ):
+        self.rot = rot
+        self.flipH = flipH
+        self.flipV = flipV
+        self.off = off
+        self.ext = ext
+        self.chOff = chOff
+        self.chExt = chExt
+
+
+class GroupTransform2D(Serialisable):
+
+    tagname = "xfrm"
+    namespace = DRAWING_NS
+
+    rot = Integer(allow_none=True)
+    flipH = Bool(allow_none=True)
+    flipV = Bool(allow_none=True)
+    off = Typed(expected_type=Point2D, allow_none=True)
+    ext = Typed(expected_type=PositiveSize2D, allow_none=True)
+    chOff = Typed(expected_type=Point2D, allow_none=True)
+    chExt = Typed(expected_type=PositiveSize2D, allow_none=True)
+
+    __elements__ = ("off", "ext", "chOff", "chExt")
+
+    def __init__(self,
+                 rot=0,
+                 flipH=None,
+                 flipV=None,
+                 off=None,
+                 ext=None,
+                 chOff=None,
+                 chExt=None,
+                ):
+        self.rot = rot
+        self.flipH = flipH
+        self.flipV = flipV
+        self.off = off
+        self.ext = ext
+        self.chOff = chOff
+        self.chExt = chExt
+
+
+class SphereCoords(Serialisable):
+
+    tagname = "sphereCoords" # usually
+
+    lat = Integer()
+    lon = Integer()
+    rev = Integer()
+
+    def __init__(self,
+                 lat=None,
+                 lon=None,
+                 rev=None,
+                ):
+        self.lat = lat
+        self.lon = lon
+        self.rev = rev
+
+
+class Camera(Serialisable):
+
+    tagname = "camera"
+
+    prst = Set(values=[
+        'legacyObliqueTopLeft', 'legacyObliqueTop', 'legacyObliqueTopRight', 'legacyObliqueLeft',
+         'legacyObliqueFront', 'legacyObliqueRight', 'legacyObliqueBottomLeft',
+         'legacyObliqueBottom', 'legacyObliqueBottomRight', 'legacyPerspectiveTopLeft',
+         'legacyPerspectiveTop', 'legacyPerspectiveTopRight', 'legacyPerspectiveLeft',
+         'legacyPerspectiveFront', 'legacyPerspectiveRight', 'legacyPerspectiveBottomLeft',
+         'legacyPerspectiveBottom', 'legacyPerspectiveBottomRight', 'orthographicFront',
+         'isometricTopUp', 'isometricTopDown', 'isometricBottomUp', 'isometricBottomDown',
+         'isometricLeftUp', 'isometricLeftDown', 'isometricRightUp', 'isometricRightDown',
+         'isometricOffAxis1Left', 'isometricOffAxis1Right', 'isometricOffAxis1Top',
+         'isometricOffAxis2Left', 'isometricOffAxis2Right', 'isometricOffAxis2Top',
+         'isometricOffAxis3Left', 'isometricOffAxis3Right', 'isometricOffAxis3Bottom',
+         'isometricOffAxis4Left', 'isometricOffAxis4Right', 'isometricOffAxis4Bottom',
+         'obliqueTopLeft',  'obliqueTop', 'obliqueTopRight', 'obliqueLeft', 'obliqueRight',
+         'obliqueBottomLeft', 'obliqueBottom', 'obliqueBottomRight', 'perspectiveFront',
+         'perspectiveLeft', 'perspectiveRight', 'perspectiveAbove', 'perspectiveBelow',
+         'perspectiveAboveLeftFacing', 'perspectiveAboveRightFacing',
+         'perspectiveContrastingLeftFacing', 'perspectiveContrastingRightFacing',
+         'perspectiveHeroicLeftFacing', 'perspectiveHeroicRightFacing',
+         'perspectiveHeroicExtremeLeftFacing', 'perspectiveHeroicExtremeRightFacing',
+         'perspectiveRelaxed', 'perspectiveRelaxedModerately'])
+    fov = Integer(allow_none=True)
+    zoom = Typed(expected_type=Percentage, allow_none=True)
+    rot = Typed(expected_type=SphereCoords, allow_none=True)
+
+
+    def __init__(self,
+                 prst=None,
+                 fov=None,
+                 zoom=None,
+                 rot=None,
+                ):
+        self.prst = prst
+        self.fov = fov
+        self.zoom = zoom
+        self.rot = rot
+
+
+class LightRig(Serialisable):
+
+    tagname = "lightRig"
+
+    rig = Set(values=['legacyFlat1', 'legacyFlat2', 'legacyFlat3', 'legacyFlat4', 'legacyNormal1',
+         'legacyNormal2', 'legacyNormal3', 'legacyNormal4', 'legacyHarsh1',
+         'legacyHarsh2', 'legacyHarsh3', 'legacyHarsh4', 'threePt', 'balanced',
+         'soft', 'harsh', 'flood', 'contrasting', 'morning', 'sunrise', 'sunset',
+         'chilly', 'freezing', 'flat', 'twoPt', 'glow', 'brightRoom']
+    )
+    dir = Set(values=(['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br']))
+    rot = Typed(expected_type=SphereCoords, allow_none=True)
+
+    def __init__(self,
+                 rig=None,
+                 dir=None,
+                 rot=None,
+                ):
+        self.rig = rig
+        self.dir = dir
+        self.rot = rot
+
+
+class Vector3D(Serialisable):
+
+    tagname = "vector"
+
+    dx = Integer() # can be in or universl measure :-/
+    dy = Integer()
+    dz = Integer()
+
+    def __init__(self,
+                 dx=None,
+                 dy=None,
+                 dz=None,
+                ):
+        self.dx = dx
+        self.dy = dy
+        self.dz = dz
+
+
+class Point3D(Serialisable):
+
+    tagname = "anchor"
+
+    x = Integer()
+    y = Integer()
+    z = Integer()
+
+    def __init__(self,
+                 x=None,
+                 y=None,
+                 z=None,
+                ):
+        self.x = x
+        self.y = y
+        self.z = z
+
+
+class Backdrop(Serialisable):
+
+    anchor = Typed(expected_type=Point3D, )
+    norm = Typed(expected_type=Vector3D, )
+    up = Typed(expected_type=Vector3D, )
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 anchor=None,
+                 norm=None,
+                 up=None,
+                 extLst=None,
+                ):
+        self.anchor = anchor
+        self.norm = norm
+        self.up = up
+        self.extLst = extLst
+
+
+class Scene3D(Serialisable):
+
+    camera = Typed(expected_type=Camera, )
+    lightRig = Typed(expected_type=LightRig, )
+    backdrop = Typed(expected_type=Backdrop, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 camera=None,
+                 lightRig=None,
+                 backdrop=None,
+                 extLst=None,
+                ):
+        self.camera = camera
+        self.lightRig = lightRig
+        self.backdrop = backdrop
+        self.extLst = extLst
+
+
+class Bevel(Serialisable):
+
+    tagname = "bevel"
+
+    w = Integer()
+    h = Integer()
+    prst = NoneSet(values=
+               ['relaxedInset', 'circle', 'slope', 'cross', 'angle',
+                'softRound', 'convex', 'coolSlant', 'divot', 'riblet',
+                 'hardEdge', 'artDeco']
+               )
+
+    def __init__(self,
+                 w=None,
+                 h=None,
+                 prst=None,
+                ):
+        self.w = w
+        self.h = h
+        self.prst = prst
+
+
+class Shape3D(Serialisable):
+
+    namespace = DRAWING_NS
+
+    z = Typed(expected_type=Coordinate, allow_none=True)
+    extrusionH = Integer(allow_none=True)
+    contourW = Integer(allow_none=True)
+    prstMaterial = NoneSet(values=[
+        'legacyMatte','legacyPlastic', 'legacyMetal', 'legacyWireframe', 'matte', 'plastic',
+        'metal', 'warmMatte', 'translucentPowder', 'powder', 'dkEdge',
+        'softEdge', 'clear', 'flat', 'softmetal']
+                       )
+    bevelT = Typed(expected_type=Bevel, allow_none=True)
+    bevelB = Typed(expected_type=Bevel, allow_none=True)
+    extrusionClr = Typed(expected_type=Color, allow_none=True)
+    contourClr = Typed(expected_type=Color, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 z=None,
+                 extrusionH=None,
+                 contourW=None,
+                 prstMaterial=None,
+                 bevelT=None,
+                 bevelB=None,
+                 extrusionClr=None,
+                 contourClr=None,
+                 extLst=None,
+                ):
+        self.z = z
+        self.extrusionH = extrusionH
+        self.contourW = contourW
+        self.prstMaterial = prstMaterial
+        self.bevelT = bevelT
+        self.bevelB = bevelB
+        self.extrusionClr = extrusionClr
+        self.contourClr = contourClr
+        self.extLst = extLst
+
+
+class Path2D(Serialisable):
+
+    w = Float()
+    h = Float()
+    fill = NoneSet(values=(['norm', 'lighten', 'lightenLess', 'darken', 'darkenLess']))
+    stroke = Bool(allow_none=True)
+    extrusionOk = Bool(allow_none=True)
+
+    def __init__(self,
+                 w=None,
+                 h=None,
+                 fill=None,
+                 stroke=None,
+                 extrusionOk=None,
+                ):
+        self.w = w
+        self.h = h
+        self.fill = fill
+        self.stroke = stroke
+        self.extrusionOk = extrusionOk
+
+
+class Path2DList(Serialisable):
+
+    path = Typed(expected_type=Path2D, allow_none=True)
+
+    def __init__(self,
+                 path=None,
+                ):
+        self.path = path
+
+
+class GeomRect(Serialisable):
+
+    l = Coordinate()
+    t = Coordinate()
+    r = Coordinate()
+    b = Coordinate()
+
+    def __init__(self,
+                 l=None,
+                 t=None,
+                 r=None,
+                 b=None,
+                ):
+        self.l = l
+        self.t = t
+        self.r = r
+        self.b = b
+
+
+class AdjPoint2D(Serialisable):
+
+    x = Coordinate()
+    y = Coordinate()
+
+    def __init__(self,
+                 x=None,
+                 y=None,
+                ):
+        self.x = x
+        self.y = y
+
+
+class ConnectionSite(Serialisable):
+
+    ang = MinMax(min=0, max=360) # guess work, can also be a name
+    pos = Typed(expected_type=AdjPoint2D, )
+
+    def __init__(self,
+                 ang=None,
+                 pos=None,
+                ):
+        self.ang = ang
+        self.pos = pos
+
+
+class ConnectionSiteList(Serialisable):
+
+    cxn = Typed(expected_type=ConnectionSite, allow_none=True)
+
+    def __init__(self,
+                 cxn=None,
+                ):
+        self.cxn = cxn
+
+
+class AdjustHandleList(Serialisable):
+
+    pass
+
+class GeomGuide(Serialisable):
+
+    name = String()
+    fmla = String()
+
+    def __init__(self,
+                 name=None,
+                 fmla=None,
+                ):
+        self.name = name
+        self.fmla = fmla
+
+
+class GeomGuideList(Serialisable):
+
+    gd = Typed(expected_type=GeomGuide, allow_none=True)
+
+    def __init__(self,
+                 gd=None,
+                ):
+        self.gd = gd
+
+
+class CustomGeometry2D(Serialisable):
+
+    avLst = Typed(expected_type=GeomGuideList, allow_none=True)
+    gdLst = Typed(expected_type=GeomGuideList, allow_none=True)
+    ahLst = Typed(expected_type=AdjustHandleList, allow_none=True)
+    cxnLst = Typed(expected_type=ConnectionSiteList, allow_none=True)
+    #rect = Typed(expected_type=GeomRect, allow_none=True)
+    pathLst = Typed(expected_type=Path2DList, )
+
+    def __init__(self,
+                 avLst=None,
+                 gdLst=None,
+                 ahLst=None,
+                 cxnLst=None,
+                 rect=None,
+                 pathLst=None,
+                ):
+        self.avLst = avLst
+        self.gdLst = gdLst
+        self.ahLst = ahLst
+        self.cxnLst = cxnLst
+        self.rect = None
+        self.pathLst = pathLst
+
+
+class PresetGeometry2D(Serialisable):
+
+    namespace = DRAWING_NS
+
+    prst = Set(values=(
+        ['line', 'lineInv', 'triangle', 'rtTriangle', 'rect',
+         'diamond', 'parallelogram', 'trapezoid', 'nonIsoscelesTrapezoid',
+         'pentagon', 'hexagon', 'heptagon', 'octagon', 'decagon', 'dodecagon',
+         'star4', 'star5', 'star6', 'star7', 'star8', 'star10', 'star12',
+         'star16', 'star24', 'star32', 'roundRect', 'round1Rect',
+         'round2SameRect', 'round2DiagRect', 'snipRoundRect', 'snip1Rect',
+         'snip2SameRect', 'snip2DiagRect', 'plaque', 'ellipse', 'teardrop',
+         'homePlate', 'chevron', 'pieWedge', 'pie', 'blockArc', 'donut',
+         'noSmoking', 'rightArrow', 'leftArrow', 'upArrow', 'downArrow',
+         'stripedRightArrow', 'notchedRightArrow', 'bentUpArrow',
+         'leftRightArrow', 'upDownArrow', 'leftUpArrow', 'leftRightUpArrow',
+         'quadArrow', 'leftArrowCallout', 'rightArrowCallout', 'upArrowCallout',
+         'downArrowCallout', 'leftRightArrowCallout', 'upDownArrowCallout',
+         'quadArrowCallout', 'bentArrow', 'uturnArrow', 'circularArrow',
+         'leftCircularArrow', 'leftRightCircularArrow', 'curvedRightArrow',
+         'curvedLeftArrow', 'curvedUpArrow', 'curvedDownArrow', 'swooshArrow',
+         'cube', 'can', 'lightningBolt', 'heart', 'sun', 'moon', 'smileyFace',
+         'irregularSeal1', 'irregularSeal2', 'foldedCorner', 'bevel', 'frame',
+         'halfFrame', 'corner', 'diagStripe', 'chord', 'arc', 'leftBracket',
+         'rightBracket', 'leftBrace', 'rightBrace', 'bracketPair', 'bracePair',
+         'straightConnector1', 'bentConnector2', 'bentConnector3',
+         'bentConnector4', 'bentConnector5', 'curvedConnector2',
+         'curvedConnector3', 'curvedConnector4', 'curvedConnector5', 'callout1',
+         'callout2', 'callout3', 'accentCallout1', 'accentCallout2',
+         'accentCallout3', 'borderCallout1', 'borderCallout2', 'borderCallout3',
+         'accentBorderCallout1', 'accentBorderCallout2', 'accentBorderCallout3',
+         'wedgeRectCallout', 'wedgeRoundRectCallout', 'wedgeEllipseCallout',
+         'cloudCallout', 'cloud', 'ribbon', 'ribbon2', 'ellipseRibbon',
+         'ellipseRibbon2', 'leftRightRibbon', 'verticalScroll',
+         'horizontalScroll', 'wave', 'doubleWave', 'plus', 'flowChartProcess',
+         'flowChartDecision', 'flowChartInputOutput',
+         'flowChartPredefinedProcess', 'flowChartInternalStorage',
+         'flowChartDocument', 'flowChartMultidocument', 'flowChartTerminator',
+         'flowChartPreparation', 'flowChartManualInput',
+         'flowChartManualOperation', 'flowChartConnector', 'flowChartPunchedCard',
+         'flowChartPunchedTape', 'flowChartSummingJunction', 'flowChartOr',
+         'flowChartCollate', 'flowChartSort', 'flowChartExtract',
+         'flowChartMerge', 'flowChartOfflineStorage', 'flowChartOnlineStorage',
+         'flowChartMagneticTape', 'flowChartMagneticDisk',
+         'flowChartMagneticDrum', 'flowChartDisplay', 'flowChartDelay',
+         'flowChartAlternateProcess', 'flowChartOffpageConnector',
+         'actionButtonBlank', 'actionButtonHome', 'actionButtonHelp',
+         'actionButtonInformation', 'actionButtonForwardNext',
+         'actionButtonBackPrevious', 'actionButtonEnd', 'actionButtonBeginning',
+         'actionButtonReturn', 'actionButtonDocument', 'actionButtonSound',
+         'actionButtonMovie', 'gear6', 'gear9', 'funnel', 'mathPlus', 'mathMinus',
+         'mathMultiply', 'mathDivide', 'mathEqual', 'mathNotEqual', 'cornerTabs',
+         'squareTabs', 'plaqueTabs', 'chartX', 'chartStar', 'chartPlus']))
+    avLst = Typed(expected_type=GeomGuideList, allow_none=True)
+
+    def __init__(self,
+                 prst=None,
+                 avLst=None,
+                ):
+        self.prst = prst
+        self.avLst = avLst
+
+
+class FontReference(Serialisable):
+
+    idx = NoneSet(values=(['major', 'minor']))
+
+    def __init__(self,
+                 idx=None,
+                ):
+        self.idx = idx
+
+
+class StyleMatrixReference(Serialisable):
+
+    idx = Integer()
+
+    def __init__(self,
+                 idx=None,
+                ):
+        self.idx = idx
+
+
+class ShapeStyle(Serialisable):
+
+    lnRef = Typed(expected_type=StyleMatrixReference, )
+    fillRef = Typed(expected_type=StyleMatrixReference, )
+    effectRef = Typed(expected_type=StyleMatrixReference, )
+    fontRef = Typed(expected_type=FontReference, )
+
+    def __init__(self,
+                 lnRef=None,
+                 fillRef=None,
+                 effectRef=None,
+                 fontRef=None,
+                ):
+        self.lnRef = lnRef
+        self.fillRef = fillRef
+        self.effectRef = effectRef
+        self.fontRef = fontRef
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/graphic.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/graphic.py
new file mode 100644
index 00000000..2c340870
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/graphic.py
@@ -0,0 +1,177 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.xml.constants import CHART_NS, DRAWING_NS
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+
+from .effect import (
+    EffectList,
+    EffectContainer,
+)
+from .fill import (
+    Blip,
+    GradientFillProperties,
+    BlipFillProperties,
+)
+from .picture import PictureFrame
+from .properties import (
+    NonVisualDrawingProps,
+    NonVisualGroupShape,
+    GroupShapeProperties,
+)
+from .relation import ChartRelation
+from .xdr import XDRTransform2D
+
+
+class GraphicFrameLocking(Serialisable):
+
+    noGrp = Bool(allow_none=True)
+    noDrilldown = Bool(allow_none=True)
+    noSelect = Bool(allow_none=True)
+    noChangeAspect = Bool(allow_none=True)
+    noMove = Bool(allow_none=True)
+    noResize = Bool(allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 noGrp=None,
+                 noDrilldown=None,
+                 noSelect=None,
+                 noChangeAspect=None,
+                 noMove=None,
+                 noResize=None,
+                 extLst=None,
+                ):
+        self.noGrp = noGrp
+        self.noDrilldown = noDrilldown
+        self.noSelect = noSelect
+        self.noChangeAspect = noChangeAspect
+        self.noMove = noMove
+        self.noResize = noResize
+        self.extLst = extLst
+
+
+class NonVisualGraphicFrameProperties(Serialisable):
+
+    tagname = "cNvGraphicFramePr"
+
+    graphicFrameLocks = Typed(expected_type=GraphicFrameLocking, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 graphicFrameLocks=None,
+                 extLst=None,
+                ):
+        self.graphicFrameLocks = graphicFrameLocks
+        self.extLst = extLst
+
+
+class NonVisualGraphicFrame(Serialisable):
+
+    tagname = "nvGraphicFramePr"
+
+    cNvPr = Typed(expected_type=NonVisualDrawingProps)
+    cNvGraphicFramePr = Typed(expected_type=NonVisualGraphicFrameProperties)
+
+    __elements__ = ('cNvPr', 'cNvGraphicFramePr')
+
+    def __init__(self,
+                 cNvPr=None,
+                 cNvGraphicFramePr=None,
+                ):
+        if cNvPr is None:
+            cNvPr = NonVisualDrawingProps(id=0, name="Chart 0")
+        self.cNvPr = cNvPr
+        if cNvGraphicFramePr is None:
+            cNvGraphicFramePr = NonVisualGraphicFrameProperties()
+        self.cNvGraphicFramePr = cNvGraphicFramePr
+
+
+class GraphicData(Serialisable):
+
+    tagname = "graphicData"
+    namespace = DRAWING_NS
+
+    uri = String()
+    chart = Typed(expected_type=ChartRelation, allow_none=True)
+
+
+    def __init__(self,
+                 uri=CHART_NS,
+                 chart=None,
+                ):
+        self.uri = uri
+        self.chart = chart
+
+
+class GraphicObject(Serialisable):
+
+    tagname = "graphic"
+    namespace = DRAWING_NS
+
+    graphicData = Typed(expected_type=GraphicData)
+
+    def __init__(self,
+                 graphicData=None,
+                ):
+        if graphicData is None:
+            graphicData = GraphicData()
+        self.graphicData = graphicData
+
+
+class GraphicFrame(Serialisable):
+
+    tagname = "graphicFrame"
+
+    nvGraphicFramePr = Typed(expected_type=NonVisualGraphicFrame)
+    xfrm = Typed(expected_type=XDRTransform2D)
+    graphic = Typed(expected_type=GraphicObject)
+    macro = String(allow_none=True)
+    fPublished = Bool(allow_none=True)
+
+    __elements__ = ('nvGraphicFramePr', 'xfrm', 'graphic', 'macro', 'fPublished')
+
+    def __init__(self,
+                 nvGraphicFramePr=None,
+                 xfrm=None,
+                 graphic=None,
+                 macro=None,
+                 fPublished=None,
+                 ):
+        if nvGraphicFramePr is None:
+            nvGraphicFramePr = NonVisualGraphicFrame()
+        self.nvGraphicFramePr = nvGraphicFramePr
+        if xfrm is None:
+            xfrm = XDRTransform2D()
+        self.xfrm = xfrm
+        if graphic is None:
+            graphic = GraphicObject()
+        self.graphic = graphic
+        self.macro = macro
+        self.fPublished = fPublished
+
+
+class GroupShape(Serialisable):
+
+    nvGrpSpPr = Typed(expected_type=NonVisualGroupShape)
+    nonVisualProperties = Alias("nvGrpSpPr")
+    grpSpPr = Typed(expected_type=GroupShapeProperties)
+    visualProperties = Alias("grpSpPr")
+    pic = Typed(expected_type=PictureFrame, allow_none=True)
+
+    __elements__ = ["nvGrpSpPr", "grpSpPr", "pic"]
+
+    def __init__(self,
+                 nvGrpSpPr=None,
+                 grpSpPr=None,
+                 pic=None,
+                ):
+        self.nvGrpSpPr = nvGrpSpPr
+        self.grpSpPr = grpSpPr
+        self.pic = pic
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/image.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/image.py
new file mode 100644
index 00000000..9d0446fe
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/image.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from io import BytesIO
+
+try:
+    from PIL import Image as PILImage
+except ImportError:
+    PILImage = False
+
+
+def _import_image(img):
+    if not PILImage:
+        raise ImportError('You must install Pillow to fetch image objects')
+
+    if not isinstance(img, PILImage.Image):
+        img = PILImage.open(img)
+
+    return img
+
+
+class Image:
+    """Image in a spreadsheet"""
+
+    _id = 1
+    _path = "/xl/media/image{0}.{1}"
+    anchor = "A1"
+
+    def __init__(self, img):
+
+        self.ref = img
+        mark_to_close = isinstance(img, str)
+        image = _import_image(img)
+        self.width, self.height = image.size
+
+        try:
+            self.format = image.format.lower()
+        except AttributeError:
+            self.format = "png"
+        if mark_to_close:
+            # PIL instances created for metadata should be closed.
+            image.close()
+
+
+    def _data(self):
+        """
+        Return image data, convert to supported types if necessary
+        """
+        img = _import_image(self.ref)
+        # don't convert these file formats
+        if self.format in ['gif', 'jpeg', 'png']:
+            img.fp.seek(0)
+            fp = img.fp
+        else:
+            fp = BytesIO()
+            img.save(fp, format="png")
+            fp.seek(0)
+
+        data = fp.read()
+        fp.close()
+        return data
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id, self.format)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/line.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/line.py
new file mode 100644
index 00000000..43388e63
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/line.py
@@ -0,0 +1,144 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    MinMax,
+    NoneSet,
+    Alias,
+    Sequence
+)
+
+from openpyxl.descriptors.nested import (
+    NestedInteger,
+    NestedNoneSet,
+    EmptyTag,
+)
+from openpyxl.xml.constants import DRAWING_NS
+
+from .colors import ColorChoiceDescriptor
+from .fill import GradientFillProperties, PatternFillProperties
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+
+"""
+Line elements from drawing main schema
+"""
+
+
+class LineEndProperties(Serialisable):
+
+    tagname = "end"
+    namespace = DRAWING_NS
+
+    type = NoneSet(values=(['none', 'triangle', 'stealth', 'diamond', 'oval', 'arrow']))
+    w = NoneSet(values=(['sm', 'med', 'lg']))
+    len = NoneSet(values=(['sm', 'med', 'lg']))
+
+    def __init__(self,
+                 type=None,
+                 w=None,
+                 len=None,
+                ):
+        self.type = type
+        self.w = w
+        self.len = len
+
+
+class DashStop(Serialisable):
+
+    tagname = "ds"
+    namespace = DRAWING_NS
+
+    d = Integer()
+    length = Alias('d')
+    sp = Integer()
+    space = Alias('sp')
+
+    def __init__(self,
+                 d=0,
+                 sp=0,
+                ):
+        self.d = d
+        self.sp = sp
+
+
+class DashStopList(Serialisable):
+
+    ds = Sequence(expected_type=DashStop, allow_none=True)
+
+    def __init__(self,
+                 ds=None,
+                ):
+        self.ds = ds
+
+
+class LineProperties(Serialisable):
+
+    tagname = "ln"
+    namespace = DRAWING_NS
+
+    w = MinMax(min=0, max=20116800, allow_none=True) # EMU
+    width = Alias('w')
+    cap = NoneSet(values=(['rnd', 'sq', 'flat']))
+    cmpd = NoneSet(values=(['sng', 'dbl', 'thickThin', 'thinThick', 'tri']))
+    algn = NoneSet(values=(['ctr', 'in']))
+
+    noFill = EmptyTag()
+    solidFill = ColorChoiceDescriptor()
+    gradFill = Typed(expected_type=GradientFillProperties, allow_none=True)
+    pattFill = Typed(expected_type=PatternFillProperties, allow_none=True)
+
+    prstDash = NestedNoneSet(values=(['solid', 'dot', 'dash', 'lgDash', 'dashDot',
+                       'lgDashDot', 'lgDashDotDot', 'sysDash', 'sysDot', 'sysDashDot',
+                       'sysDashDotDot']), namespace=namespace)
+    dashStyle = Alias('prstDash')
+
+    custDash = Typed(expected_type=DashStop, allow_none=True)
+
+    round = EmptyTag()
+    bevel = EmptyTag()
+    miter = NestedInteger(allow_none=True, attribute="lim")
+
+    headEnd = Typed(expected_type=LineEndProperties, allow_none=True)
+    tailEnd = Typed(expected_type=LineEndProperties, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ('noFill', 'solidFill', 'gradFill', 'pattFill',
+                    'prstDash', 'custDash', 'round', 'bevel', 'miter', 'headEnd', 'tailEnd')
+
+    def __init__(self,
+                 w=None,
+                 cap=None,
+                 cmpd=None,
+                 algn=None,
+                 noFill=None,
+                 solidFill=None,
+                 gradFill=None,
+                 pattFill=None,
+                 prstDash=None,
+                 custDash=None,
+                 round=None,
+                 bevel=None,
+                 miter=None,
+                 headEnd=None,
+                 tailEnd=None,
+                 extLst=None,
+                ):
+        self.w = w
+        self.cap = cap
+        self.cmpd = cmpd
+        self.algn = algn
+        self.noFill = noFill
+        self.solidFill = solidFill
+        self.gradFill = gradFill
+        self.pattFill = pattFill
+        if prstDash is None:
+            prstDash = "solid"
+        self.prstDash = prstDash
+        self.custDash = custDash
+        self.round = round
+        self.bevel = bevel
+        self.miter = miter
+        self.headEnd = headEnd
+        self.tailEnd = tailEnd
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/picture.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/picture.py
new file mode 100644
index 00000000..9a83facf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/picture.py
@@ -0,0 +1,144 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.xml.constants import DRAWING_NS
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+
+from openpyxl.chart.shapes import GraphicalProperties
+
+from .fill import BlipFillProperties
+from .properties import NonVisualDrawingProps
+from .geometry import ShapeStyle
+
+
+class PictureLocking(Serialisable):
+
+    tagname = "picLocks"
+    namespace = DRAWING_NS
+
+    # Using attribute group AG_Locking
+    noCrop = Bool(allow_none=True)
+    noGrp = Bool(allow_none=True)
+    noSelect = Bool(allow_none=True)
+    noRot = Bool(allow_none=True)
+    noChangeAspect = Bool(allow_none=True)
+    noMove = Bool(allow_none=True)
+    noResize = Bool(allow_none=True)
+    noEditPoints = Bool(allow_none=True)
+    noAdjustHandles = Bool(allow_none=True)
+    noChangeArrowheads = Bool(allow_none=True)
+    noChangeShapeType = Bool(allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 noCrop=None,
+                 noGrp=None,
+                 noSelect=None,
+                 noRot=None,
+                 noChangeAspect=None,
+                 noMove=None,
+                 noResize=None,
+                 noEditPoints=None,
+                 noAdjustHandles=None,
+                 noChangeArrowheads=None,
+                 noChangeShapeType=None,
+                 extLst=None,
+                ):
+        self.noCrop = noCrop
+        self.noGrp = noGrp
+        self.noSelect = noSelect
+        self.noRot = noRot
+        self.noChangeAspect = noChangeAspect
+        self.noMove = noMove
+        self.noResize = noResize
+        self.noEditPoints = noEditPoints
+        self.noAdjustHandles = noAdjustHandles
+        self.noChangeArrowheads = noChangeArrowheads
+        self.noChangeShapeType = noChangeShapeType
+
+
+class NonVisualPictureProperties(Serialisable):
+
+    tagname = "cNvPicPr"
+
+    preferRelativeResize = Bool(allow_none=True)
+    picLocks = Typed(expected_type=PictureLocking, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ("picLocks",)
+
+    def __init__(self,
+                 preferRelativeResize=None,
+                 picLocks=None,
+                 extLst=None,
+                ):
+        self.preferRelativeResize = preferRelativeResize
+        self.picLocks = picLocks
+
+
+class PictureNonVisual(Serialisable):
+
+    tagname = "nvPicPr"
+
+    cNvPr = Typed(expected_type=NonVisualDrawingProps, )
+    cNvPicPr = Typed(expected_type=NonVisualPictureProperties, )
+
+    __elements__ = ("cNvPr", "cNvPicPr")
+
+    def __init__(self,
+                 cNvPr=None,
+                 cNvPicPr=None,
+                ):
+        if cNvPr is None:
+            cNvPr = NonVisualDrawingProps(id=0, name="Image 1", descr="Name of file")
+        self.cNvPr = cNvPr
+        if cNvPicPr is None:
+            cNvPicPr = NonVisualPictureProperties()
+        self.cNvPicPr = cNvPicPr
+
+
+
+
+class PictureFrame(Serialisable):
+
+    tagname = "pic"
+
+    macro = String(allow_none=True)
+    fPublished = Bool(allow_none=True)
+    nvPicPr = Typed(expected_type=PictureNonVisual, )
+    blipFill = Typed(expected_type=BlipFillProperties, )
+    spPr = Typed(expected_type=GraphicalProperties, )
+    graphicalProperties = Alias('spPr')
+    style = Typed(expected_type=ShapeStyle, allow_none=True)
+
+    __elements__ = ("nvPicPr", "blipFill", "spPr", "style")
+
+    def __init__(self,
+                 macro=None,
+                 fPublished=None,
+                 nvPicPr=None,
+                 blipFill=None,
+                 spPr=None,
+                 style=None,
+                ):
+        self.macro = macro
+        self.fPublished = fPublished
+        if nvPicPr is None:
+            nvPicPr = PictureNonVisual()
+        self.nvPicPr = nvPicPr
+        if blipFill is None:
+            blipFill = BlipFillProperties()
+        self.blipFill = blipFill
+        if spPr is None:
+            spPr = GraphicalProperties()
+        self.spPr = spPr
+        self.style = style
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/properties.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/properties.py
new file mode 100644
index 00000000..77b00728
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/properties.py
@@ -0,0 +1,174 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.xml.constants import DRAWING_NS
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    Integer,
+    Set,
+    String,
+    Alias,
+    NoneSet,
+)
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+
+from .geometry import GroupTransform2D, Scene3D
+from .text import Hyperlink
+
+
+class GroupShapeProperties(Serialisable):
+
+    tagname = "grpSpPr"
+
+    bwMode = NoneSet(values=(['clr', 'auto', 'gray', 'ltGray', 'invGray',
+                          'grayWhite', 'blackGray', 'blackWhite', 'black', 'white', 'hidden']))
+    xfrm = Typed(expected_type=GroupTransform2D, allow_none=True)
+    scene3d = Typed(expected_type=Scene3D, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    def __init__(self,
+                 bwMode=None,
+                 xfrm=None,
+                 scene3d=None,
+                 extLst=None,
+                ):
+        self.bwMode = bwMode
+        self.xfrm = xfrm
+        self.scene3d = scene3d
+        self.extLst = extLst
+
+
+class GroupLocking(Serialisable):
+
+    tagname = "grpSpLocks"
+    namespace = DRAWING_NS
+
+    noGrp = Bool(allow_none=True)
+    noUngrp = Bool(allow_none=True)
+    noSelect = Bool(allow_none=True)
+    noRot = Bool(allow_none=True)
+    noChangeAspect = Bool(allow_none=True)
+    noMove = Bool(allow_none=True)
+    noResize = Bool(allow_none=True)
+    noChangeArrowheads = Bool(allow_none=True)
+    noEditPoints = Bool(allow_none=True)
+    noAdjustHandles = Bool(allow_none=True)
+    noChangeArrowheads = Bool(allow_none=True)
+    noChangeShapeType = Bool(allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 noGrp=None,
+                 noUngrp=None,
+                 noSelect=None,
+                 noRot=None,
+                 noChangeAspect=None,
+                 noChangeArrowheads=None,
+                 noMove=None,
+                 noResize=None,
+                 noEditPoints=None,
+                 noAdjustHandles=None,
+                 noChangeShapeType=None,
+                 extLst=None,
+                ):
+        self.noGrp = noGrp
+        self.noUngrp = noUngrp
+        self.noSelect = noSelect
+        self.noRot = noRot
+        self.noChangeAspect = noChangeAspect
+        self.noChangeArrowheads = noChangeArrowheads
+        self.noMove = noMove
+        self.noResize = noResize
+        self.noEditPoints = noEditPoints
+        self.noAdjustHandles = noAdjustHandles
+        self.noChangeShapeType = noChangeShapeType
+
+
+class NonVisualGroupDrawingShapeProps(Serialisable):
+
+    tagname = "cNvGrpSpPr"
+
+    grpSpLocks = Typed(expected_type=GroupLocking, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ("grpSpLocks",)
+
+    def __init__(self,
+                 grpSpLocks=None,
+                 extLst=None,
+                ):
+        self.grpSpLocks = grpSpLocks
+
+
+class NonVisualDrawingShapeProps(Serialisable):
+
+    tagname = "cNvSpPr"
+
+    spLocks = Typed(expected_type=GroupLocking, allow_none=True)
+    txBax = Bool(allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ("spLocks", "txBax")
+
+    def __init__(self,
+                 spLocks=None,
+                 txBox=None,
+                 extLst=None,
+                ):
+        self.spLocks = spLocks
+        self.txBox = txBox
+
+
+class NonVisualDrawingProps(Serialisable):
+
+    tagname = "cNvPr"
+
+    id = Integer()
+    name = String()
+    descr = String(allow_none=True)
+    hidden = Bool(allow_none=True)
+    title = String(allow_none=True)
+    hlinkClick = Typed(expected_type=Hyperlink, allow_none=True)
+    hlinkHover = Typed(expected_type=Hyperlink, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ["hlinkClick", "hlinkHover"]
+
+    def __init__(self,
+                 id=None,
+                 name=None,
+                 descr=None,
+                 hidden=None,
+                 title=None,
+                 hlinkClick=None,
+                 hlinkHover=None,
+                 extLst=None,
+                ):
+        self.id = id
+        self.name = name
+        self.descr = descr
+        self.hidden = hidden
+        self.title = title
+        self.hlinkClick = hlinkClick
+        self.hlinkHover = hlinkHover
+        self.extLst = extLst
+
+class NonVisualGroupShape(Serialisable):
+
+    tagname = "nvGrpSpPr"
+
+    cNvPr = Typed(expected_type=NonVisualDrawingProps)
+    cNvGrpSpPr = Typed(expected_type=NonVisualGroupDrawingShapeProps)
+
+    __elements__ = ("cNvPr", "cNvGrpSpPr")
+
+    def __init__(self,
+                 cNvPr=None,
+                 cNvGrpSpPr=None,
+                ):
+        self.cNvPr = cNvPr
+        self.cNvGrpSpPr = cNvGrpSpPr
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/relation.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/relation.py
new file mode 100644
index 00000000..01632934
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/relation.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.xml.constants import CHART_NS
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.excel import Relation
+
+
+class ChartRelation(Serialisable):
+
+    tagname = "chart"
+    namespace = CHART_NS
+
+    id = Relation()
+
+    def __init__(self, id):
+        self.id = id
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/spreadsheet_drawing.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/spreadsheet_drawing.py
new file mode 100644
index 00000000..4f378ca2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/spreadsheet_drawing.py
@@ -0,0 +1,382 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    NoneSet,
+    Integer,
+    Sequence,
+    Alias,
+)
+from openpyxl.descriptors.nested import (
+    NestedText,
+    NestedNoneSet,
+)
+from openpyxl.descriptors.excel import Relation
+
+from openpyxl.packaging.relationship import (
+    Relationship,
+    RelationshipList,
+)
+from openpyxl.utils import coordinate_to_tuple
+from openpyxl.utils.units import (
+    cm_to_EMU,
+    pixels_to_EMU,
+)
+from openpyxl.drawing.image import Image
+
+from openpyxl.xml.constants import SHEET_DRAWING_NS
+
+from openpyxl.chart._chart import ChartBase
+from .xdr import (
+    XDRPoint2D,
+    XDRPositiveSize2D,
+)
+from .fill import Blip
+from .connector import Shape
+from .graphic import (
+    GroupShape,
+    GraphicFrame,
+    )
+from .geometry import PresetGeometry2D
+from .picture import PictureFrame
+from .relation import ChartRelation
+
+
+class AnchorClientData(Serialisable):
+
+    fLocksWithSheet = Bool(allow_none=True)
+    fPrintsWithSheet = Bool(allow_none=True)
+
+    def __init__(self,
+                 fLocksWithSheet=None,
+                 fPrintsWithSheet=None,
+                 ):
+        self.fLocksWithSheet = fLocksWithSheet
+        self.fPrintsWithSheet = fPrintsWithSheet
+
+
+class AnchorMarker(Serialisable):
+
+    tagname = "marker"
+
+    col = NestedText(expected_type=int)
+    colOff = NestedText(expected_type=int)
+    row = NestedText(expected_type=int)
+    rowOff = NestedText(expected_type=int)
+
+    def __init__(self,
+                 col=0,
+                 colOff=0,
+                 row=0,
+                 rowOff=0,
+                 ):
+        self.col = col
+        self.colOff = colOff
+        self.row = row
+        self.rowOff = rowOff
+
+
+class _AnchorBase(Serialisable):
+
+    #one of
+    sp = Typed(expected_type=Shape, allow_none=True)
+    shape = Alias("sp")
+    grpSp = Typed(expected_type=GroupShape, allow_none=True)
+    groupShape = Alias("grpSp")
+    graphicFrame = Typed(expected_type=GraphicFrame, allow_none=True)
+    cxnSp = Typed(expected_type=Shape, allow_none=True)
+    connectionShape = Alias("cxnSp")
+    pic = Typed(expected_type=PictureFrame, allow_none=True)
+    contentPart = Relation()
+
+    clientData = Typed(expected_type=AnchorClientData)
+
+    __elements__ = ('sp', 'grpSp', 'graphicFrame',
+                    'cxnSp', 'pic', 'contentPart', 'clientData')
+
+    def __init__(self,
+                 clientData=None,
+                 sp=None,
+                 grpSp=None,
+                 graphicFrame=None,
+                 cxnSp=None,
+                 pic=None,
+                 contentPart=None
+                 ):
+        if clientData is None:
+            clientData = AnchorClientData()
+        self.clientData = clientData
+        self.sp = sp
+        self.grpSp = grpSp
+        self.graphicFrame = graphicFrame
+        self.cxnSp = cxnSp
+        self.pic = pic
+        self.contentPart = contentPart
+
+
+class AbsoluteAnchor(_AnchorBase):
+
+    tagname = "absoluteAnchor"
+
+    pos = Typed(expected_type=XDRPoint2D)
+    ext = Typed(expected_type=XDRPositiveSize2D)
+
+    sp = _AnchorBase.sp
+    grpSp = _AnchorBase.grpSp
+    graphicFrame = _AnchorBase.graphicFrame
+    cxnSp = _AnchorBase.cxnSp
+    pic = _AnchorBase.pic
+    contentPart = _AnchorBase.contentPart
+    clientData = _AnchorBase.clientData
+
+    __elements__ = ('pos', 'ext') + _AnchorBase.__elements__
+
+    def __init__(self,
+                 pos=None,
+                 ext=None,
+                 **kw
+                ):
+        if pos is None:
+            pos = XDRPoint2D(0, 0)
+        self.pos = pos
+        if ext is None:
+            ext = XDRPositiveSize2D(0, 0)
+        self.ext = ext
+        super().__init__(**kw)
+
+
+class OneCellAnchor(_AnchorBase):
+
+    tagname = "oneCellAnchor"
+
+    _from = Typed(expected_type=AnchorMarker)
+    ext = Typed(expected_type=XDRPositiveSize2D)
+
+    sp = _AnchorBase.sp
+    grpSp = _AnchorBase.grpSp
+    graphicFrame = _AnchorBase.graphicFrame
+    cxnSp = _AnchorBase.cxnSp
+    pic = _AnchorBase.pic
+    contentPart = _AnchorBase.contentPart
+    clientData = _AnchorBase.clientData
+
+    __elements__ = ('_from', 'ext') + _AnchorBase.__elements__
+
+
+    def __init__(self,
+                 _from=None,
+                 ext=None,
+                 **kw
+                ):
+        if _from is None:
+            _from = AnchorMarker()
+        self._from = _from
+        if ext is None:
+            ext = XDRPositiveSize2D(0, 0)
+        self.ext = ext
+        super().__init__(**kw)
+
+
+class TwoCellAnchor(_AnchorBase):
+
+    tagname = "twoCellAnchor"
+
+    editAs = NoneSet(values=(['twoCell', 'oneCell', 'absolute']))
+    _from = Typed(expected_type=AnchorMarker)
+    to = Typed(expected_type=AnchorMarker)
+
+    sp = _AnchorBase.sp
+    grpSp = _AnchorBase.grpSp
+    graphicFrame = _AnchorBase.graphicFrame
+    cxnSp = _AnchorBase.cxnSp
+    pic = _AnchorBase.pic
+    contentPart = _AnchorBase.contentPart
+    clientData = _AnchorBase.clientData
+
+    __elements__ = ('_from', 'to') + _AnchorBase.__elements__
+
+    def __init__(self,
+                 editAs=None,
+                 _from=None,
+                 to=None,
+                 **kw
+                 ):
+        self.editAs = editAs
+        if _from is None:
+            _from = AnchorMarker()
+        self._from = _from
+        if to is None:
+            to = AnchorMarker()
+        self.to = to
+        super().__init__(**kw)
+
+
+def _check_anchor(obj):
+    """
+    Check whether an object has an existing Anchor object
+    If not create a OneCellAnchor using the provided coordinate
+    """
+    anchor = obj.anchor
+    if not isinstance(anchor, _AnchorBase):
+        row, col = coordinate_to_tuple(anchor.upper())
+        anchor = OneCellAnchor()
+        anchor._from.row = row -1
+        anchor._from.col = col -1
+        if isinstance(obj, ChartBase):
+            anchor.ext.width = cm_to_EMU(obj.width)
+            anchor.ext.height = cm_to_EMU(obj.height)
+        elif isinstance(obj, Image):
+            anchor.ext.width = pixels_to_EMU(obj.width)
+            anchor.ext.height = pixels_to_EMU(obj.height)
+    return anchor
+
+
+class SpreadsheetDrawing(Serialisable):
+
+    tagname = "wsDr"
+    mime_type = "application/vnd.openxmlformats-officedocument.drawing+xml"
+    _rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
+    _path = PartName="/xl/drawings/drawing{0}.xml"
+    _id = None
+
+    twoCellAnchor = Sequence(expected_type=TwoCellAnchor, allow_none=True)
+    oneCellAnchor = Sequence(expected_type=OneCellAnchor, allow_none=True)
+    absoluteAnchor = Sequence(expected_type=AbsoluteAnchor, allow_none=True)
+
+    __elements__ = ("twoCellAnchor", "oneCellAnchor", "absoluteAnchor")
+
+    def __init__(self,
+                 twoCellAnchor=(),
+                 oneCellAnchor=(),
+                 absoluteAnchor=(),
+                 ):
+        self.twoCellAnchor = twoCellAnchor
+        self.oneCellAnchor = oneCellAnchor
+        self.absoluteAnchor = absoluteAnchor
+        self.charts = []
+        self.images = []
+        self._rels = []
+
+
+    def __hash__(self):
+        """
+        Just need to check for identity
+        """
+        return id(self)
+
+
+    def __bool__(self):
+        return bool(self.charts) or bool(self.images)
+
+
+
+    def _write(self):
+        """
+        create required structure and the serialise
+        """
+        anchors = []
+        for idx, obj in enumerate(self.charts + self.images, 1):
+            anchor = _check_anchor(obj)
+            if isinstance(obj, ChartBase):
+                rel = Relationship(type="chart", Target=obj.path)
+                anchor.graphicFrame = self._chart_frame(idx)
+            elif isinstance(obj, Image):
+                rel = Relationship(type="image", Target=obj.path)
+                child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
+                if not child:
+                    anchor.pic = self._picture_frame(idx)
+                else:
+                    child.blipFill.blip.embed = "rId{0}".format(idx)
+
+            anchors.append(anchor)
+            self._rels.append(rel)
+
+        for a in anchors:
+            if isinstance(a, OneCellAnchor):
+                self.oneCellAnchor.append(a)
+            elif isinstance(a, TwoCellAnchor):
+                self.twoCellAnchor.append(a)
+            else:
+                self.absoluteAnchor.append(a)
+
+        tree = self.to_tree()
+        tree.set('xmlns', SHEET_DRAWING_NS)
+        return tree
+
+
+    def _chart_frame(self, idx):
+        chart_rel = ChartRelation(f"rId{idx}")
+        frame = GraphicFrame()
+        nv = frame.nvGraphicFramePr.cNvPr
+        nv.id = idx
+        nv.name = "Chart {0}".format(idx)
+        frame.graphic.graphicData.chart = chart_rel
+        return frame
+
+
+    def _picture_frame(self, idx):
+        pic = PictureFrame()
+        pic.nvPicPr.cNvPr.descr = "Picture"
+        pic.nvPicPr.cNvPr.id = idx
+        pic.nvPicPr.cNvPr.name = "Image {0}".format(idx)
+
+        pic.blipFill.blip = Blip()
+        pic.blipFill.blip.embed = "rId{0}".format(idx)
+        pic.blipFill.blip.cstate = "print"
+
+        pic.spPr.prstGeom = PresetGeometry2D(prst="rect")
+        pic.spPr.ln = None
+        return pic
+
+
+    def _write_rels(self):
+        rels = RelationshipList()
+        for r in self._rels:
+            rels.append(r)
+        return rels.to_tree()
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
+
+
+    @property
+    def _chart_rels(self):
+        """
+        Get relationship information for each chart and bind anchor to it
+        """
+        rels = []
+        anchors = self.absoluteAnchor + self.oneCellAnchor + self.twoCellAnchor
+        for anchor in anchors:
+            if anchor.graphicFrame is not None:
+                graphic = anchor.graphicFrame.graphic
+                rel = graphic.graphicData.chart
+                if rel is not None:
+                    rel.anchor = anchor
+                    rel.anchor.graphicFrame = None
+                    rels.append(rel)
+        return rels
+
+
+    @property
+    def _blip_rels(self):
+        """
+        Get relationship information for each blip and bind anchor to it
+
+        Images that are not part of the XLSX package will be ignored.
+        """
+        rels = []
+        anchors = self.absoluteAnchor + self.oneCellAnchor + self.twoCellAnchor
+
+        for anchor in anchors:
+            child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
+            if child and child.blipFill:
+                rel = child.blipFill.blip
+                if rel is not None and rel.embed:
+                    rel.anchor = anchor
+                    rels.append(rel)
+
+        return rels
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/text.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/text.py
new file mode 100644
index 00000000..5bdc771f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/text.py
@@ -0,0 +1,717 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    Set,
+    NoneSet,
+    Sequence,
+    String,
+    Bool,
+    MinMax,
+    Integer
+)
+from openpyxl.descriptors.excel import (
+    HexBinary,
+    Coordinate,
+    Relation,
+)
+from openpyxl.descriptors.nested import (
+    NestedInteger,
+    NestedText,
+    NestedValue,
+    EmptyTag
+)
+from openpyxl.xml.constants import DRAWING_NS
+
+
+from .colors import ColorChoiceDescriptor
+from .effect import (
+    EffectList,
+    EffectContainer,
+)
+from .fill import(
+    GradientFillProperties,
+    BlipFillProperties,
+    PatternFillProperties,
+    Blip
+)
+from .geometry import (
+    LineProperties,
+    Color,
+    Scene3D
+)
+
+from openpyxl.descriptors.excel import ExtensionList as OfficeArtExtensionList
+from openpyxl.descriptors.nested import NestedBool
+
+
+class EmbeddedWAVAudioFile(Serialisable):
+
+    name = String(allow_none=True)
+
+    def __init__(self,
+                 name=None,
+                ):
+        self.name = name
+
+
+class Hyperlink(Serialisable):
+
+    tagname = "hlinkClick"
+    namespace = DRAWING_NS
+
+    invalidUrl = String(allow_none=True)
+    action = String(allow_none=True)
+    tgtFrame = String(allow_none=True)
+    tooltip = String(allow_none=True)
+    history = Bool(allow_none=True)
+    highlightClick = Bool(allow_none=True)
+    endSnd = Bool(allow_none=True)
+    snd = Typed(expected_type=EmbeddedWAVAudioFile, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+    id = Relation(allow_none=True)
+
+    __elements__ = ('snd',)
+
+    def __init__(self,
+                 invalidUrl=None,
+                 action=None,
+                 tgtFrame=None,
+                 tooltip=None,
+                 history=None,
+                 highlightClick=None,
+                 endSnd=None,
+                 snd=None,
+                 extLst=None,
+                 id=None,
+                ):
+        self.invalidUrl = invalidUrl
+        self.action = action
+        self.tgtFrame = tgtFrame
+        self.tooltip = tooltip
+        self.history = history
+        self.highlightClick = highlightClick
+        self.endSnd = endSnd
+        self.snd = snd
+        self.id = id
+
+
+class Font(Serialisable):
+
+    tagname = "latin"
+    namespace = DRAWING_NS
+
+    typeface = String()
+    panose = HexBinary(allow_none=True)
+    pitchFamily = MinMax(min=0, max=52, allow_none=True)
+    charset = Integer(allow_none=True)
+
+    def __init__(self,
+                 typeface=None,
+                 panose=None,
+                 pitchFamily=None,
+                 charset=None,
+                ):
+        self.typeface = typeface
+        self.panose = panose
+        self.pitchFamily = pitchFamily
+        self.charset = charset
+
+
+class CharacterProperties(Serialisable):
+
+    tagname = "defRPr"
+    namespace = DRAWING_NS
+
+    kumimoji = Bool(allow_none=True)
+    lang = String(allow_none=True)
+    altLang = String(allow_none=True)
+    sz = MinMax(allow_none=True, min=100, max=400000) # 100ths of a point
+    b = Bool(allow_none=True)
+    i = Bool(allow_none=True)
+    u = NoneSet(values=(['words', 'sng', 'dbl', 'heavy', 'dotted',
+                         'dottedHeavy', 'dash', 'dashHeavy', 'dashLong', 'dashLongHeavy',
+                         'dotDash', 'dotDashHeavy', 'dotDotDash', 'dotDotDashHeavy', 'wavy',
+                         'wavyHeavy', 'wavyDbl']))
+    strike = NoneSet(values=(['noStrike', 'sngStrike', 'dblStrike']))
+    kern = Integer(allow_none=True)
+    cap = NoneSet(values=(['small', 'all']))
+    spc = Integer(allow_none=True)
+    normalizeH = Bool(allow_none=True)
+    baseline = Integer(allow_none=True)
+    noProof = Bool(allow_none=True)
+    dirty = Bool(allow_none=True)
+    err = Bool(allow_none=True)
+    smtClean = Bool(allow_none=True)
+    smtId = Integer(allow_none=True)
+    bmk = String(allow_none=True)
+    ln = Typed(expected_type=LineProperties, allow_none=True)
+    highlight = Typed(expected_type=Color, allow_none=True)
+    latin = Typed(expected_type=Font, allow_none=True)
+    ea = Typed(expected_type=Font, allow_none=True)
+    cs = Typed(expected_type=Font, allow_none=True)
+    sym = Typed(expected_type=Font, allow_none=True)
+    hlinkClick = Typed(expected_type=Hyperlink, allow_none=True)
+    hlinkMouseOver = Typed(expected_type=Hyperlink, allow_none=True)
+    rtl = NestedBool(allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+    # uses element group EG_FillProperties
+    noFill = EmptyTag(namespace=DRAWING_NS)
+    solidFill = ColorChoiceDescriptor()
+    gradFill = Typed(expected_type=GradientFillProperties, allow_none=True)
+    blipFill = Typed(expected_type=BlipFillProperties, allow_none=True)
+    pattFill = Typed(expected_type=PatternFillProperties, allow_none=True)
+    grpFill = EmptyTag(namespace=DRAWING_NS)
+    # uses element group EG_EffectProperties
+    effectLst = Typed(expected_type=EffectList, allow_none=True)
+    effectDag = Typed(expected_type=EffectContainer, allow_none=True)
+    # uses element group EG_TextUnderlineLine
+    uLnTx = EmptyTag()
+    uLn = Typed(expected_type=LineProperties, allow_none=True)
+    # uses element group EG_TextUnderlineFill
+    uFillTx = EmptyTag()
+    uFill = EmptyTag()
+
+    __elements__ = ('ln', 'noFill', 'solidFill', 'gradFill', 'blipFill',
+                    'pattFill', 'grpFill', 'effectLst', 'effectDag', 'highlight','uLnTx',
+                    'uLn', 'uFillTx', 'uFill', 'latin', 'ea', 'cs', 'sym', 'hlinkClick',
+                    'hlinkMouseOver', 'rtl', )
+
+    def __init__(self,
+                 kumimoji=None,
+                 lang=None,
+                 altLang=None,
+                 sz=None,
+                 b=None,
+                 i=None,
+                 u=None,
+                 strike=None,
+                 kern=None,
+                 cap=None,
+                 spc=None,
+                 normalizeH=None,
+                 baseline=None,
+                 noProof=None,
+                 dirty=None,
+                 err=None,
+                 smtClean=None,
+                 smtId=None,
+                 bmk=None,
+                 ln=None,
+                 highlight=None,
+                 latin=None,
+                 ea=None,
+                 cs=None,
+                 sym=None,
+                 hlinkClick=None,
+                 hlinkMouseOver=None,
+                 rtl=None,
+                 extLst=None,
+                 noFill=None,
+                 solidFill=None,
+                 gradFill=None,
+                 blipFill=None,
+                 pattFill=None,
+                 grpFill=None,
+                 effectLst=None,
+                 effectDag=None,
+                 uLnTx=None,
+                 uLn=None,
+                 uFillTx=None,
+                 uFill=None,
+                ):
+        self.kumimoji = kumimoji
+        self.lang = lang
+        self.altLang = altLang
+        self.sz = sz
+        self.b = b
+        self.i = i
+        self.u = u
+        self.strike = strike
+        self.kern = kern
+        self.cap = cap
+        self.spc = spc
+        self.normalizeH = normalizeH
+        self.baseline = baseline
+        self.noProof = noProof
+        self.dirty = dirty
+        self.err = err
+        self.smtClean = smtClean
+        self.smtId = smtId
+        self.bmk = bmk
+        self.ln = ln
+        self.highlight = highlight
+        self.latin = latin
+        self.ea = ea
+        self.cs = cs
+        self.sym = sym
+        self.hlinkClick = hlinkClick
+        self.hlinkMouseOver = hlinkMouseOver
+        self.rtl = rtl
+        self.noFill = noFill
+        self.solidFill = solidFill
+        self.gradFill = gradFill
+        self.blipFill = blipFill
+        self.pattFill = pattFill
+        self.grpFill = grpFill
+        self.effectLst = effectLst
+        self.effectDag = effectDag
+        self.uLnTx = uLnTx
+        self.uLn = uLn
+        self.uFillTx = uFillTx
+        self.uFill = uFill
+
+
+class TabStop(Serialisable):
+
+    pos = Typed(expected_type=Coordinate, allow_none=True)
+    algn = Typed(expected_type=Set(values=(['l', 'ctr', 'r', 'dec'])))
+
+    def __init__(self,
+                 pos=None,
+                 algn=None,
+                ):
+        self.pos = pos
+        self.algn = algn
+
+
+class TabStopList(Serialisable):
+
+    tab = Typed(expected_type=TabStop, allow_none=True)
+
+    def __init__(self,
+                 tab=None,
+                ):
+        self.tab = tab
+
+
+class Spacing(Serialisable):
+
+    spcPct = NestedInteger(allow_none=True)
+    spcPts = NestedInteger(allow_none=True)
+
+    __elements__ = ('spcPct', 'spcPts')
+
+    def __init__(self,
+                 spcPct=None,
+                 spcPts=None,
+                 ):
+        self.spcPct = spcPct
+        self.spcPts = spcPts
+
+
+class AutonumberBullet(Serialisable):
+
+    type = Set(values=(['alphaLcParenBoth', 'alphaUcParenBoth',
+                        'alphaLcParenR', 'alphaUcParenR', 'alphaLcPeriod', 'alphaUcPeriod',
+                        'arabicParenBoth', 'arabicParenR', 'arabicPeriod', 'arabicPlain',
+                        'romanLcParenBoth', 'romanUcParenBoth', 'romanLcParenR', 'romanUcParenR',
+                        'romanLcPeriod', 'romanUcPeriod', 'circleNumDbPlain',
+                        'circleNumWdBlackPlain', 'circleNumWdWhitePlain', 'arabicDbPeriod',
+                        'arabicDbPlain', 'ea1ChsPeriod', 'ea1ChsPlain', 'ea1ChtPeriod',
+                        'ea1ChtPlain', 'ea1JpnChsDbPeriod', 'ea1JpnKorPlain', 'ea1JpnKorPeriod',
+                        'arabic1Minus', 'arabic2Minus', 'hebrew2Minus', 'thaiAlphaPeriod',
+                        'thaiAlphaParenR', 'thaiAlphaParenBoth', 'thaiNumPeriod',
+                        'thaiNumParenR', 'thaiNumParenBoth', 'hindiAlphaPeriod',
+                        'hindiNumPeriod', 'hindiNumParenR', 'hindiAlpha1Period']))
+    startAt = Integer()
+
+    def __init__(self,
+                 type=None,
+                 startAt=None,
+                ):
+        self.type = type
+        self.startAt = startAt
+
+
+class ParagraphProperties(Serialisable):
+
+    tagname = "pPr"
+    namespace = DRAWING_NS
+
+    marL = Integer(allow_none=True)
+    marR = Integer(allow_none=True)
+    lvl = Integer(allow_none=True)
+    indent = Integer(allow_none=True)
+    algn = NoneSet(values=(['l', 'ctr', 'r', 'just', 'justLow', 'dist', 'thaiDist']))
+    defTabSz = Integer(allow_none=True)
+    rtl = Bool(allow_none=True)
+    eaLnBrk = Bool(allow_none=True)
+    fontAlgn = NoneSet(values=(['auto', 't', 'ctr', 'base', 'b']))
+    latinLnBrk = Bool(allow_none=True)
+    hangingPunct = Bool(allow_none=True)
+
+    # uses element group EG_TextBulletColor
+    # uses element group EG_TextBulletSize
+    # uses element group EG_TextBulletTypeface
+    # uses element group EG_TextBullet
+    lnSpc = Typed(expected_type=Spacing, allow_none=True)
+    spcBef = Typed(expected_type=Spacing, allow_none=True)
+    spcAft = Typed(expected_type=Spacing, allow_none=True)
+    tabLst = Typed(expected_type=TabStopList, allow_none=True)
+    defRPr = Typed(expected_type=CharacterProperties, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+    buClrTx = EmptyTag()
+    buClr = Typed(expected_type=Color, allow_none=True)
+    buSzTx = EmptyTag()
+    buSzPct = NestedInteger(allow_none=True)
+    buSzPts = NestedInteger(allow_none=True)
+    buFontTx = EmptyTag()
+    buFont = Typed(expected_type=Font, allow_none=True)
+    buNone = EmptyTag()
+    buAutoNum = EmptyTag()
+    buChar = NestedValue(expected_type=str, attribute="char", allow_none=True)
+    buBlip = NestedValue(expected_type=Blip, attribute="blip", allow_none=True)
+
+    __elements__ = ('lnSpc', 'spcBef', 'spcAft', 'tabLst', 'defRPr',
+                    'buClrTx', 'buClr', 'buSzTx', 'buSzPct', 'buSzPts', 'buFontTx', 'buFont',
+                    'buNone', 'buAutoNum', 'buChar', 'buBlip')
+
+    def __init__(self,
+                 marL=None,
+                 marR=None,
+                 lvl=None,
+                 indent=None,
+                 algn=None,
+                 defTabSz=None,
+                 rtl=None,
+                 eaLnBrk=None,
+                 fontAlgn=None,
+                 latinLnBrk=None,
+                 hangingPunct=None,
+                 lnSpc=None,
+                 spcBef=None,
+                 spcAft=None,
+                 tabLst=None,
+                 defRPr=None,
+                 extLst=None,
+                 buClrTx=None,
+                 buClr=None,
+                 buSzTx=None,
+                 buSzPct=None,
+                 buSzPts=None,
+                 buFontTx=None,
+                 buFont=None,
+                 buNone=None,
+                 buAutoNum=None,
+                 buChar=None,
+                 buBlip=None,
+                 ):
+        self.marL = marL
+        self.marR = marR
+        self.lvl = lvl
+        self.indent = indent
+        self.algn = algn
+        self.defTabSz = defTabSz
+        self.rtl = rtl
+        self.eaLnBrk = eaLnBrk
+        self.fontAlgn = fontAlgn
+        self.latinLnBrk = latinLnBrk
+        self.hangingPunct = hangingPunct
+        self.lnSpc = lnSpc
+        self.spcBef = spcBef
+        self.spcAft = spcAft
+        self.tabLst = tabLst
+        self.defRPr = defRPr
+        self.buClrTx = buClrTx
+        self.buClr = buClr
+        self.buSzTx = buSzTx
+        self.buSzPct = buSzPct
+        self.buSzPts = buSzPts
+        self.buFontTx = buFontTx
+        self.buFont = buFont
+        self.buNone = buNone
+        self.buAutoNum = buAutoNum
+        self.buChar = buChar
+        self.buBlip = buBlip
+        self.defRPr = defRPr
+
+
+class ListStyle(Serialisable):
+
+    tagname = "lstStyle"
+    namespace = DRAWING_NS
+
+    defPPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl1pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl2pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl3pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl4pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl5pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl6pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl7pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl8pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    lvl9pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+
+    __elements__ = ("defPPr", "lvl1pPr", "lvl2pPr", "lvl3pPr", "lvl4pPr",
+                    "lvl5pPr", "lvl6pPr", "lvl7pPr", "lvl8pPr", "lvl9pPr")
+
+    def __init__(self,
+                 defPPr=None,
+                 lvl1pPr=None,
+                 lvl2pPr=None,
+                 lvl3pPr=None,
+                 lvl4pPr=None,
+                 lvl5pPr=None,
+                 lvl6pPr=None,
+                 lvl7pPr=None,
+                 lvl8pPr=None,
+                 lvl9pPr=None,
+                 extLst=None,
+                ):
+        self.defPPr = defPPr
+        self.lvl1pPr = lvl1pPr
+        self.lvl2pPr = lvl2pPr
+        self.lvl3pPr = lvl3pPr
+        self.lvl4pPr = lvl4pPr
+        self.lvl5pPr = lvl5pPr
+        self.lvl6pPr = lvl6pPr
+        self.lvl7pPr = lvl7pPr
+        self.lvl8pPr = lvl8pPr
+        self.lvl9pPr = lvl9pPr
+
+
+class RegularTextRun(Serialisable):
+
+    tagname = "r"
+    namespace = DRAWING_NS
+
+    rPr = Typed(expected_type=CharacterProperties, allow_none=True)
+    properties = Alias("rPr")
+    t = NestedText(expected_type=str)
+    value = Alias("t")
+
+    __elements__ = ('rPr', 't')
+
+    def __init__(self,
+                 rPr=None,
+                 t="",
+                ):
+        self.rPr = rPr
+        self.t = t
+
+
+class LineBreak(Serialisable):
+
+    tagname = "br"
+    namespace = DRAWING_NS
+
+    rPr = Typed(expected_type=CharacterProperties, allow_none=True)
+
+    __elements__ = ('rPr',)
+
+    def __init__(self,
+                 rPr=None,
+                ):
+        self.rPr = rPr
+
+
+class TextField(Serialisable):
+
+    id = String()
+    type = String(allow_none=True)
+    rPr = Typed(expected_type=CharacterProperties, allow_none=True)
+    pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    t = String(allow_none=True)
+
+    __elements__ = ('rPr', 'pPr')
+
+    def __init__(self,
+                 id=None,
+                 type=None,
+                 rPr=None,
+                 pPr=None,
+                 t=None,
+                ):
+        self.id = id
+        self.type = type
+        self.rPr = rPr
+        self.pPr = pPr
+        self.t = t
+
+
+class Paragraph(Serialisable):
+
+    tagname = "p"
+    namespace = DRAWING_NS
+
+    # uses element group EG_TextRun
+    pPr = Typed(expected_type=ParagraphProperties, allow_none=True)
+    properties = Alias("pPr")
+    endParaRPr = Typed(expected_type=CharacterProperties, allow_none=True)
+    r = Sequence(expected_type=RegularTextRun)
+    text = Alias('r')
+    br = Typed(expected_type=LineBreak, allow_none=True)
+    fld = Typed(expected_type=TextField, allow_none=True)
+
+    __elements__ = ('pPr', 'r', 'br', 'fld', 'endParaRPr')
+
+    def __init__(self,
+                 pPr=None,
+                 endParaRPr=None,
+                 r=None,
+                 br=None,
+                 fld=None,
+                 ):
+        self.pPr = pPr
+        self.endParaRPr = endParaRPr
+        if r is None:
+            r = [RegularTextRun()]
+        self.r = r
+        self.br = br
+        self.fld = fld
+
+
+class GeomGuide(Serialisable):
+
+    name = String(())
+    fmla = String(())
+
+    def __init__(self,
+                 name=None,
+                 fmla=None,
+                ):
+        self.name = name
+        self.fmla = fmla
+
+
+class GeomGuideList(Serialisable):
+
+    gd = Sequence(expected_type=GeomGuide, allow_none=True)
+
+    def __init__(self,
+                 gd=None,
+                ):
+        self.gd = gd
+
+
+class PresetTextShape(Serialisable):
+
+    prst = Typed(expected_type=Set(values=(
+        ['textNoShape', 'textPlain','textStop', 'textTriangle', 'textTriangleInverted', 'textChevron',
+         'textChevronInverted', 'textRingInside', 'textRingOutside', 'textArchUp',
+         'textArchDown', 'textCircle', 'textButton', 'textArchUpPour',
+         'textArchDownPour', 'textCirclePour', 'textButtonPour', 'textCurveUp',
+         'textCurveDown', 'textCanUp', 'textCanDown', 'textWave1', 'textWave2',
+         'textDoubleWave1', 'textWave4', 'textInflate', 'textDeflate',
+         'textInflateBottom', 'textDeflateBottom', 'textInflateTop',
+         'textDeflateTop', 'textDeflateInflate', 'textDeflateInflateDeflate',
+         'textFadeRight', 'textFadeLeft', 'textFadeUp', 'textFadeDown',
+         'textSlantUp', 'textSlantDown', 'textCascadeUp', 'textCascadeDown'
+         ]
+    )))
+    avLst = Typed(expected_type=GeomGuideList, allow_none=True)
+
+    def __init__(self,
+                 prst=None,
+                 avLst=None,
+                ):
+        self.prst = prst
+        self.avLst = avLst
+
+
+class TextNormalAutofit(Serialisable):
+
+    fontScale = Integer()
+    lnSpcReduction = Integer()
+
+    def __init__(self,
+                 fontScale=None,
+                 lnSpcReduction=None,
+                ):
+        self.fontScale = fontScale
+        self.lnSpcReduction = lnSpcReduction
+
+
+class RichTextProperties(Serialisable):
+
+    tagname = "bodyPr"
+    namespace = DRAWING_NS
+
+    rot = Integer(allow_none=True)
+    spcFirstLastPara = Bool(allow_none=True)
+    vertOverflow = NoneSet(values=(['overflow', 'ellipsis', 'clip']))
+    horzOverflow = NoneSet(values=(['overflow', 'clip']))
+    vert = NoneSet(values=(['horz', 'vert', 'vert270', 'wordArtVert',
+                            'eaVert', 'mongolianVert', 'wordArtVertRtl']))
+    wrap = NoneSet(values=(['none', 'square']))
+    lIns = Integer(allow_none=True)
+    tIns = Integer(allow_none=True)
+    rIns = Integer(allow_none=True)
+    bIns = Integer(allow_none=True)
+    numCol = Integer(allow_none=True)
+    spcCol = Integer(allow_none=True)
+    rtlCol = Bool(allow_none=True)
+    fromWordArt = Bool(allow_none=True)
+    anchor = NoneSet(values=(['t', 'ctr', 'b', 'just', 'dist']))
+    anchorCtr = Bool(allow_none=True)
+    forceAA = Bool(allow_none=True)
+    upright = Bool(allow_none=True)
+    compatLnSpc = Bool(allow_none=True)
+    prstTxWarp = Typed(expected_type=PresetTextShape, allow_none=True)
+    scene3d = Typed(expected_type=Scene3D, allow_none=True)
+    extLst = Typed(expected_type=OfficeArtExtensionList, allow_none=True)
+    noAutofit = EmptyTag()
+    normAutofit = EmptyTag()
+    spAutoFit = EmptyTag()
+    flatTx = NestedInteger(attribute="z", allow_none=True)
+
+    __elements__ = ('prstTxWarp', 'scene3d', 'noAutofit', 'normAutofit', 'spAutoFit')
+
+    def __init__(self,
+                 rot=None,
+                 spcFirstLastPara=None,
+                 vertOverflow=None,
+                 horzOverflow=None,
+                 vert=None,
+                 wrap=None,
+                 lIns=None,
+                 tIns=None,
+                 rIns=None,
+                 bIns=None,
+                 numCol=None,
+                 spcCol=None,
+                 rtlCol=None,
+                 fromWordArt=None,
+                 anchor=None,
+                 anchorCtr=None,
+                 forceAA=None,
+                 upright=None,
+                 compatLnSpc=None,
+                 prstTxWarp=None,
+                 scene3d=None,
+                 extLst=None,
+                 noAutofit=None,
+                 normAutofit=None,
+                 spAutoFit=None,
+                 flatTx=None,
+                ):
+        self.rot = rot
+        self.spcFirstLastPara = spcFirstLastPara
+        self.vertOverflow = vertOverflow
+        self.horzOverflow = horzOverflow
+        self.vert = vert
+        self.wrap = wrap
+        self.lIns = lIns
+        self.tIns = tIns
+        self.rIns = rIns
+        self.bIns = bIns
+        self.numCol = numCol
+        self.spcCol = spcCol
+        self.rtlCol = rtlCol
+        self.fromWordArt = fromWordArt
+        self.anchor = anchor
+        self.anchorCtr = anchorCtr
+        self.forceAA = forceAA
+        self.upright = upright
+        self.compatLnSpc = compatLnSpc
+        self.prstTxWarp = prstTxWarp
+        self.scene3d = scene3d
+        self.noAutofit = noAutofit
+        self.normAutofit = normAutofit
+        self.spAutoFit = spAutoFit
+        self.flatTx = flatTx
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/drawing/xdr.py b/.venv/lib/python3.12/site-packages/openpyxl/drawing/xdr.py
new file mode 100644
index 00000000..335480ce
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/drawing/xdr.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Spreadsheet Drawing has some copies of Drawing ML elements
+"""
+
+from .geometry import Point2D, PositiveSize2D, Transform2D
+
+
+class XDRPoint2D(Point2D):
+
+    namespace = None
+    x = Point2D.x
+    y = Point2D.y
+
+
+class XDRPositiveSize2D(PositiveSize2D):
+
+    namespace = None
+    cx = PositiveSize2D.cx
+    cy = PositiveSize2D.cy
+
+
+class XDRTransform2D(Transform2D):
+
+    namespace = None
+    rot = Transform2D.rot
+    flipH = Transform2D.flipH
+    flipV = Transform2D.flipV
+    off = Transform2D.off
+    ext = Transform2D.ext
+    chOff = Transform2D.chOff
+    chExt = Transform2D.chExt
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formatting/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/formatting/__init__.py
new file mode 100644
index 00000000..bedc2bc4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formatting/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .rule import Rule
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formatting/formatting.py b/.venv/lib/python3.12/site-packages/openpyxl/formatting/formatting.py
new file mode 100644
index 00000000..bf622bf9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formatting/formatting.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from collections import OrderedDict
+
+from openpyxl.descriptors import (
+    Bool,
+    Sequence,
+    Alias,
+    Convertible,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+from .rule import Rule
+
+from openpyxl.worksheet.cell_range import MultiCellRange
+
+class ConditionalFormatting(Serialisable):
+
+    tagname = "conditionalFormatting"
+
+    sqref = Convertible(expected_type=MultiCellRange)
+    cells = Alias("sqref")
+    pivot = Bool(allow_none=True)
+    cfRule = Sequence(expected_type=Rule)
+    rules = Alias("cfRule")
+
+
+    def __init__(self, sqref=(), pivot=None, cfRule=(), extLst=None):
+        self.sqref = sqref
+        self.pivot = pivot
+        self.cfRule = cfRule
+
+
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return False
+        return self.sqref == other.sqref
+
+
+    def __hash__(self):
+        return hash(self.sqref)
+
+
+    def __repr__(self):
+        return "<{cls} {cells}>".format(cls=self.__class__.__name__, cells=self.sqref)
+
+
+    def __contains__(self, coord):
+        """
+        Check whether a certain cell is affected by the formatting
+        """
+        return coord in self.sqref
+
+
+class ConditionalFormattingList:
+    """Conditional formatting rules."""
+
+
+    def __init__(self):
+        self._cf_rules = OrderedDict()
+        self.max_priority = 0
+
+
+    def add(self, range_string, cfRule):
+        """Add a rule such as ColorScaleRule, FormulaRule or CellIsRule
+
+         The priority will be added automatically.
+        """
+        cf = range_string
+        if isinstance(range_string, str):
+            cf = ConditionalFormatting(range_string)
+        if not isinstance(cfRule, Rule):
+            raise ValueError("Only instances of openpyxl.formatting.rule.Rule may be added")
+        rule = cfRule
+        self.max_priority += 1
+        if not rule.priority:
+            rule.priority = self.max_priority
+
+        self._cf_rules.setdefault(cf, []).append(rule)
+
+
+    def __bool__(self):
+        return bool(self._cf_rules)
+
+
+    def __len__(self):
+        return len(self._cf_rules)
+
+
+    def __iter__(self):
+        for cf, rules in self._cf_rules.items():
+            cf.rules = rules
+            yield cf
+
+
+    def __getitem__(self, key):
+        """
+        Get the rules for a cell range
+        """
+        if isinstance(key, str):
+            key = ConditionalFormatting(sqref=key)
+        return self._cf_rules[key]
+
+
+    def __delitem__(self, key):
+        key = ConditionalFormatting(sqref=key)
+        del self._cf_rules[key]
+
+
+    def __setitem__(self, key, rule):
+        """
+        Add a rule for a cell range
+        """
+        self.add(key, rule)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formatting/rule.py b/.venv/lib/python3.12/site-packages/openpyxl/formatting/rule.py
new file mode 100644
index 00000000..c4ba7f8f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formatting/rule.py
@@ -0,0 +1,291 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Sequence,
+    Bool,
+    NoneSet,
+    Set,
+    Integer,
+    Float,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.styles.colors import Color, ColorDescriptor
+from openpyxl.styles.differential import DifferentialStyle
+
+from openpyxl.utils.cell import COORD_RE
+
+
+class ValueDescriptor(Float):
+    """
+    Expected type depends upon type attribute of parent :-(
+
+    Most values should be numeric BUT they can also be cell references
+    """
+
+    def __set__(self, instance, value):
+        ref = None
+        if value is not None and isinstance(value, str):
+            ref = COORD_RE.match(value)
+        if instance.type == "formula" or ref:
+            self.expected_type = str
+        else:
+            self.expected_type = float
+        super().__set__(instance, value)
+
+
+class FormatObject(Serialisable):
+
+    tagname = "cfvo"
+
+    type = Set(values=(['num', 'percent', 'max', 'min', 'formula', 'percentile']))
+    val = ValueDescriptor(allow_none=True)
+    gte = Bool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 type,
+                 val=None,
+                 gte=None,
+                 extLst=None,
+                ):
+        self.type = type
+        self.val = val
+        self.gte = gte
+
+
+class RuleType(Serialisable):
+
+    cfvo = Sequence(expected_type=FormatObject)
+
+
+class IconSet(RuleType):
+
+    tagname = "iconSet"
+
+    iconSet = NoneSet(values=(['3Arrows', '3ArrowsGray', '3Flags',
+                           '3TrafficLights1', '3TrafficLights2', '3Signs', '3Symbols', '3Symbols2',
+                           '4Arrows', '4ArrowsGray', '4RedToBlack', '4Rating', '4TrafficLights',
+                           '5Arrows', '5ArrowsGray', '5Rating', '5Quarters']))
+    showValue = Bool(allow_none=True)
+    percent = Bool(allow_none=True)
+    reverse = Bool(allow_none=True)
+
+    __elements__ = ("cfvo",)
+
+    def __init__(self,
+                 iconSet=None,
+                 showValue=None,
+                 percent=None,
+                 reverse=None,
+                 cfvo=None,
+                ):
+        self.iconSet = iconSet
+        self.showValue = showValue
+        self.percent = percent
+        self.reverse = reverse
+        self.cfvo = cfvo
+
+
+class DataBar(RuleType):
+
+    tagname = "dataBar"
+
+    minLength = Integer(allow_none=True)
+    maxLength = Integer(allow_none=True)
+    showValue = Bool(allow_none=True)
+    color = ColorDescriptor()
+
+    __elements__ = ('cfvo', 'color')
+
+    def __init__(self,
+                 minLength=None,
+                 maxLength=None,
+                 showValue=None,
+                 cfvo=None,
+                 color=None,
+                ):
+        self.minLength = minLength
+        self.maxLength = maxLength
+        self.showValue = showValue
+        self.cfvo = cfvo
+        self.color = color
+
+
+class ColorScale(RuleType):
+
+    tagname = "colorScale"
+
+    color = Sequence(expected_type=Color)
+
+    __elements__ = ('cfvo', 'color')
+
+    def __init__(self,
+                 cfvo=None,
+                 color=None,
+                ):
+        self.cfvo = cfvo
+        self.color = color
+
+
+class Rule(Serialisable):
+
+    tagname = "cfRule"
+
+    type = Set(values=(['expression', 'cellIs', 'colorScale', 'dataBar',
+                        'iconSet', 'top10', 'uniqueValues', 'duplicateValues', 'containsText',
+                        'notContainsText', 'beginsWith', 'endsWith', 'containsBlanks',
+                        'notContainsBlanks', 'containsErrors', 'notContainsErrors', 'timePeriod',
+                        'aboveAverage']))
+    dxfId = Integer(allow_none=True)
+    priority = Integer()
+    stopIfTrue = Bool(allow_none=True)
+    aboveAverage = Bool(allow_none=True)
+    percent = Bool(allow_none=True)
+    bottom = Bool(allow_none=True)
+    operator = NoneSet(values=(['lessThan', 'lessThanOrEqual', 'equal',
+                            'notEqual', 'greaterThanOrEqual', 'greaterThan', 'between', 'notBetween',
+                            'containsText', 'notContains', 'beginsWith', 'endsWith']))
+    text = String(allow_none=True)
+    timePeriod = NoneSet(values=(['today', 'yesterday', 'tomorrow', 'last7Days',
+                              'thisMonth', 'lastMonth', 'nextMonth', 'thisWeek', 'lastWeek',
+                              'nextWeek']))
+    rank = Integer(allow_none=True)
+    stdDev = Integer(allow_none=True)
+    equalAverage = Bool(allow_none=True)
+    formula = Sequence(expected_type=str)
+    colorScale = Typed(expected_type=ColorScale, allow_none=True)
+    dataBar = Typed(expected_type=DataBar, allow_none=True)
+    iconSet = Typed(expected_type=IconSet, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    dxf = Typed(expected_type=DifferentialStyle, allow_none=True)
+
+    __elements__ = ('colorScale', 'dataBar', 'iconSet', 'formula')
+    __attrs__ = ('type', 'rank', 'priority', 'equalAverage', 'operator',
+                 'aboveAverage', 'dxfId', 'stdDev', 'stopIfTrue', 'timePeriod', 'text',
+                 'percent', 'bottom')
+
+
+    def __init__(self,
+                 type,
+                 dxfId=None,
+                 priority=0,
+                 stopIfTrue=None,
+                 aboveAverage=None,
+                 percent=None,
+                 bottom=None,
+                 operator=None,
+                 text=None,
+                 timePeriod=None,
+                 rank=None,
+                 stdDev=None,
+                 equalAverage=None,
+                 formula=(),
+                 colorScale=None,
+                 dataBar=None,
+                 iconSet=None,
+                 extLst=None,
+                 dxf=None,
+                ):
+        self.type = type
+        self.dxfId = dxfId
+        self.priority = priority
+        self.stopIfTrue = stopIfTrue
+        self.aboveAverage = aboveAverage
+        self.percent = percent
+        self.bottom = bottom
+        self.operator = operator
+        self.text = text
+        self.timePeriod = timePeriod
+        self.rank = rank
+        self.stdDev = stdDev
+        self.equalAverage = equalAverage
+        self.formula = formula
+        self.colorScale = colorScale
+        self.dataBar = dataBar
+        self.iconSet = iconSet
+        self.dxf = dxf
+
+
+def ColorScaleRule(start_type=None,
+                 start_value=None,
+                 start_color=None,
+                 mid_type=None,
+                 mid_value=None,
+                 mid_color=None,
+                 end_type=None,
+                 end_value=None,
+                 end_color=None):
+
+    """Backwards compatibility"""
+    formats = []
+    if start_type is not None:
+        formats.append(FormatObject(type=start_type, val=start_value))
+    if mid_type is not None:
+        formats.append(FormatObject(type=mid_type, val=mid_value))
+    if end_type is not None:
+        formats.append(FormatObject(type=end_type, val=end_value))
+    colors = []
+    for v in (start_color, mid_color, end_color):
+        if v is not None:
+            if not isinstance(v, Color):
+                v = Color(v)
+            colors.append(v)
+    cs = ColorScale(cfvo=formats, color=colors)
+    rule = Rule(type="colorScale", colorScale=cs)
+    return rule
+
+
+def FormulaRule(formula=None, stopIfTrue=None, font=None, border=None,
+                fill=None):
+    """
+    Conditional formatting with custom differential style
+    """
+    rule = Rule(type="expression", formula=formula, stopIfTrue=stopIfTrue)
+    rule.dxf =  DifferentialStyle(font=font, border=border, fill=fill)
+    return rule
+
+
+def CellIsRule(operator=None, formula=None, stopIfTrue=None, font=None, border=None, fill=None):
+    """
+    Conditional formatting rule based on cell contents.
+    """
+    # Excel doesn't use >, >=, etc, but allow for ease of python development
+    expand = {">": "greaterThan", ">=": "greaterThanOrEqual", "<": "lessThan", "<=": "lessThanOrEqual",
+              "=": "equal", "==": "equal", "!=": "notEqual"}
+
+    operator = expand.get(operator, operator)
+
+    rule = Rule(type='cellIs', operator=operator, formula=formula, stopIfTrue=stopIfTrue)
+    rule.dxf = DifferentialStyle(font=font, border=border, fill=fill)
+
+    return rule
+
+
+def IconSetRule(icon_style=None, type=None, values=None, showValue=None, percent=None, reverse=None):
+    """
+    Convenience function for creating icon set rules
+    """
+    cfvo = []
+    for val in values:
+        cfvo.append(FormatObject(type, val))
+    icon_set = IconSet(iconSet=icon_style, cfvo=cfvo, showValue=showValue,
+                       percent=percent, reverse=reverse)
+    rule = Rule(type='iconSet', iconSet=icon_set)
+
+    return rule
+
+
+def DataBarRule(start_type=None, start_value=None, end_type=None,
+                end_value=None, color=None, showValue=None, minLength=None, maxLength=None):
+    start = FormatObject(start_type, start_value)
+    end = FormatObject(end_type, end_value)
+    data_bar = DataBar(cfvo=[start, end], color=color, showValue=showValue,
+                       minLength=minLength, maxLength=maxLength)
+    rule = Rule(type='dataBar', dataBar=data_bar)
+
+    return rule
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formula/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/formula/__init__.py
new file mode 100644
index 00000000..a98a0c4a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formula/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .tokenizer import Tokenizer
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formula/tokenizer.py b/.venv/lib/python3.12/site-packages/openpyxl/formula/tokenizer.py
new file mode 100644
index 00000000..9bf26240
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formula/tokenizer.py
@@ -0,0 +1,446 @@
+"""
+This module contains a tokenizer for Excel formulae.
+
+The tokenizer is based on the Javascript tokenizer found at
+http://ewbi.blogs.com/develops/2004/12/excel_formula_p.html written by Eric
+Bachtal
+"""
+
+import re
+
+
+class TokenizerError(Exception):
+    """Base class for all Tokenizer errors."""
+
+
+class Tokenizer:
+
+    """
+    A tokenizer for Excel worksheet formulae.
+
+    Converts a str string representing an Excel formula (in A1 notation)
+    into a sequence of `Token` objects.
+
+    `formula`: The str string to tokenize
+
+    Tokenizer defines a method `._parse()` to parse the formula into tokens,
+    which can then be accessed through the `.items` attribute.
+
+    """
+
+    SN_RE = re.compile("^[1-9](\\.[0-9]+)?[Ee]$")  # Scientific notation
+    WSPACE_RE = re.compile(r"[ \n]+")
+    STRING_REGEXES = {
+        # Inside a string, all characters are treated as literals, except for
+        # the quote character used to start the string. That character, when
+        # doubled is treated as a single character in the string. If an
+        # unmatched quote appears, the string is terminated.
+        '"': re.compile('"(?:[^"]*"")*[^"]*"(?!")'),
+        "'": re.compile("'(?:[^']*'')*[^']*'(?!')"),
+    }
+    ERROR_CODES = ("#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?",
+                   "#NUM!", "#N/A", "#GETTING_DATA")
+    TOKEN_ENDERS = ',;}) +-*/^&=><%'  # Each of these characters, marks the
+                                       # end of an operand token
+
+    def __init__(self, formula):
+        self.formula = formula
+        self.items = []
+        self.token_stack = []  # Used to keep track of arrays, functions, and
+                               # parentheses
+        self.offset = 0  # How many chars have we read
+        self.token = []  # Used to build up token values char by char
+        self._parse()
+
+    def _parse(self):
+        """Populate self.items with the tokens from the formula."""
+        if self.offset:
+            return  # Already parsed!
+        if not self.formula:
+            return
+        elif self.formula[0] == '=':
+            self.offset += 1
+        else:
+            self.items.append(Token(self.formula, Token.LITERAL))
+            return
+        consumers = (
+            ('"\'', self._parse_string),
+            ('[', self._parse_brackets),
+            ('#', self._parse_error),
+            (' ', self._parse_whitespace),
+            ('\n', self._parse_whitespace),
+            ('+-*/^&=><%', self._parse_operator),
+            ('{(', self._parse_opener),
+            (')}', self._parse_closer),
+            (';,', self._parse_separator),
+        )
+        dispatcher = {}  # maps chars to the specific parsing function
+        for chars, consumer in consumers:
+            dispatcher.update(dict.fromkeys(chars, consumer))
+        while self.offset < len(self.formula):
+            if self.check_scientific_notation():  # May consume one character
+                continue
+            curr_char = self.formula[self.offset]
+            if curr_char in self.TOKEN_ENDERS:
+                self.save_token()
+            if curr_char in dispatcher:
+                self.offset += dispatcher[curr_char]()
+            else:
+                # TODO: this can probably be sped up using a regex to get to
+                # the next interesting character
+                self.token.append(curr_char)
+                self.offset += 1
+        self.save_token()
+
+    def _parse_string(self):
+        """
+        Parse a "-delimited string or '-delimited link.
+
+        The offset must be pointing to either a single quote ("'") or double
+        quote ('"') character. The strings are parsed according to Excel
+        rules where to escape the delimiter you just double it up. E.g.,
+        "abc""def" in Excel is parsed as 'abc"def' in Python.
+
+        Returns the number of characters matched. (Does not update
+        self.offset)
+
+        """
+        self.assert_empty_token(can_follow=':')
+        delim = self.formula[self.offset]
+        assert delim in ('"', "'")
+        regex = self.STRING_REGEXES[delim]
+        match = regex.match(self.formula[self.offset:])
+        if match is None:
+            subtype = "string" if delim == '"' else 'link'
+            raise TokenizerError(f"Reached end of formula while parsing {subtype} in {self.formula}")
+        match = match.group(0)
+        if delim == '"':
+            self.items.append(Token.make_operand(match))
+        else:
+            self.token.append(match)
+        return len(match)
+
+    def _parse_brackets(self):
+        """
+        Consume all the text between square brackets [].
+
+        Returns the number of characters matched. (Does not update
+        self.offset)
+
+        """
+        assert self.formula[self.offset] == '['
+        lefts = [(t.start(), 1) for t in
+                 re.finditer(r"\[", self.formula[self.offset:])]
+        rights = [(t.start(), -1) for t in
+                  re.finditer(r"\]", self.formula[self.offset:])]
+
+        open_count = 0
+        for idx, open_close in sorted(lefts + rights):
+            open_count += open_close
+            if open_count == 0:
+                outer_right = idx + 1
+                self.token.append(
+                    self.formula[self.offset:self.offset + outer_right])
+                return outer_right
+
+        raise TokenizerError(f"Encountered unmatched '[' in {self.formula}")
+
+    def _parse_error(self):
+        """
+        Consume the text following a '#' as an error.
+
+        Looks for a match in self.ERROR_CODES and returns the number of
+        characters matched. (Does not update self.offset)
+
+        """
+        self.assert_empty_token(can_follow='!')
+        assert self.formula[self.offset] == '#'
+        subformula = self.formula[self.offset:]
+        for err in self.ERROR_CODES:
+            if subformula.startswith(err):
+                self.items.append(Token.make_operand(''.join(self.token) + err))
+                del self.token[:]
+                return len(err)
+        raise TokenizerError(f"Invalid error code at position {self.offset} in '{self.formula}'")
+
+    def _parse_whitespace(self):
+        """
+        Consume a string of consecutive spaces.
+
+        Returns the number of spaces found. (Does not update self.offset).
+
+        """
+        assert self.formula[self.offset] in (' ', '\n')
+        self.items.append(Token(self.formula[self.offset], Token.WSPACE))
+        return self.WSPACE_RE.match(self.formula[self.offset:]).end()
+
+    def _parse_operator(self):
+        """
+        Consume the characters constituting an operator.
+
+        Returns the number of characters consumed. (Does not update
+        self.offset)
+
+        """
+        if self.formula[self.offset:self.offset + 2] in ('>=', '<=', '<>'):
+            self.items.append(Token(
+                self.formula[self.offset:self.offset + 2],
+                Token.OP_IN
+            ))
+            return 2
+        curr_char = self.formula[self.offset]  # guaranteed to be 1 char
+        assert curr_char in '%*/^&=><+-'
+        if curr_char == '%':
+            token = Token('%', Token.OP_POST)
+        elif curr_char in "*/^&=><":
+            token = Token(curr_char, Token.OP_IN)
+        # From here on, curr_char is guaranteed to be in '+-'
+        elif not self.items:
+            token = Token(curr_char, Token.OP_PRE)
+        else:
+            prev = next((i for i in reversed(self.items)
+                         if i.type != Token.WSPACE), None)
+            is_infix = prev and (
+                prev.subtype == Token.CLOSE
+                or prev.type == Token.OP_POST
+                or prev.type == Token.OPERAND
+            )
+            if is_infix:
+                token = Token(curr_char, Token.OP_IN)
+            else:
+                token = Token(curr_char, Token.OP_PRE)
+        self.items.append(token)
+        return 1
+
+    def _parse_opener(self):
+        """
+        Consumes a ( or { character.
+
+        Returns the number of characters consumed. (Does not update
+        self.offset)
+
+        """
+        assert self.formula[self.offset] in ('(', '{')
+        if self.formula[self.offset] == '{':
+            self.assert_empty_token()
+            token = Token.make_subexp("{")
+        elif self.token:
+            token_value = "".join(self.token) + '('
+            del self.token[:]
+            token = Token.make_subexp(token_value)
+        else:
+            token = Token.make_subexp("(")
+        self.items.append(token)
+        self.token_stack.append(token)
+        return 1
+
+    def _parse_closer(self):
+        """
+        Consumes a } or ) character.
+
+        Returns the number of characters consumed. (Does not update
+        self.offset)
+
+        """
+        assert self.formula[self.offset] in (')', '}')
+        token = self.token_stack.pop().get_closer()
+        if token.value != self.formula[self.offset]:
+            raise TokenizerError(
+                "Mismatched ( and { pair in '%s'" % self.formula)
+        self.items.append(token)
+        return 1
+
+    def _parse_separator(self):
+        """
+        Consumes a ; or , character.
+
+        Returns the number of characters consumed. (Does not update
+        self.offset)
+
+        """
+        curr_char = self.formula[self.offset]
+        assert curr_char in (';', ',')
+        if curr_char == ';':
+            token = Token.make_separator(";")
+        else:
+            try:
+                top_type = self.token_stack[-1].type
+            except IndexError:
+                token = Token(",", Token.OP_IN)  # Range Union operator
+            else:
+                if top_type == Token.PAREN:
+                    token = Token(",", Token.OP_IN)  # Range Union operator
+                else:
+                    token = Token.make_separator(",")
+        self.items.append(token)
+        return 1
+
+    def check_scientific_notation(self):
+        """
+        Consumes a + or - character if part of a number in sci. notation.
+
+        Returns True if the character was consumed and self.offset was
+        updated, False otherwise.
+
+        """
+        curr_char = self.formula[self.offset]
+        if (curr_char in '+-'
+                and len(self.token) >= 1
+                and self.SN_RE.match("".join(self.token))):
+            self.token.append(curr_char)
+            self.offset += 1
+            return True
+        return False
+
+    def assert_empty_token(self, can_follow=()):
+        """
+        Ensure that there's no token currently being parsed.
+
+        Or if there is a token being parsed, it must end with a character in
+        can_follow.
+
+        If there are unconsumed token contents, it means we hit an unexpected
+        token transition. In this case, we raise a TokenizerError
+
+        """
+        if self.token and self.token[-1] not in can_follow:
+            raise TokenizerError(f"Unexpected character at position {self.offset} in '{self.formula}'")
+
+    def save_token(self):
+        """If there's a token being parsed, add it to the item list."""
+        if self.token:
+            self.items.append(Token.make_operand("".join(self.token)))
+            del self.token[:]
+
+    def render(self):
+        """Convert the parsed tokens back to a string."""
+        if not self.items:
+            return ""
+        elif self.items[0].type == Token.LITERAL:
+            return self.items[0].value
+        return "=" + "".join(token.value for token in self.items)
+
+
+class Token:
+
+    """
+    A token in an Excel formula.
+
+    Tokens have three attributes:
+
+    * `value`: The string value parsed that led to this token
+    * `type`: A string identifying the type of token
+    * `subtype`: A string identifying subtype of the token (optional, and
+                 defaults to "")
+
+    """
+
+    __slots__ = ['value', 'type', 'subtype']
+
+    LITERAL = "LITERAL"
+    OPERAND = "OPERAND"
+    FUNC = "FUNC"
+    ARRAY = "ARRAY"
+    PAREN = "PAREN"
+    SEP = "SEP"
+    OP_PRE = "OPERATOR-PREFIX"
+    OP_IN = "OPERATOR-INFIX"
+    OP_POST = "OPERATOR-POSTFIX"
+    WSPACE = "WHITE-SPACE"
+
+    def __init__(self, value, type_, subtype=""):
+        self.value = value
+        self.type = type_
+        self.subtype = subtype
+
+    # Literal operands:
+    #
+    # Literal operands are always of type 'OPERAND' and can be of subtype
+    # 'TEXT' (for text strings), 'NUMBER' (for all numeric types), 'LOGICAL'
+    # (for TRUE and FALSE), 'ERROR' (for literal error values), or 'RANGE'
+    # (for all range references).
+
+    TEXT = 'TEXT'
+    NUMBER = 'NUMBER'
+    LOGICAL = 'LOGICAL'
+    ERROR = 'ERROR'
+    RANGE = 'RANGE'
+
+    def __repr__(self):
+        return u"{0} {1} {2}:".format(self.type, self.subtype, self.value)
+
+    @classmethod
+    def make_operand(cls, value):
+        """Create an operand token."""
+        if value.startswith('"'):
+            subtype = cls.TEXT
+        elif value.startswith('#'):
+            subtype = cls.ERROR
+        elif value in ('TRUE', 'FALSE'):
+            subtype = cls.LOGICAL
+        else:
+            try:
+                float(value)
+                subtype = cls.NUMBER
+            except ValueError:
+                subtype = cls.RANGE
+        return cls(value, cls.OPERAND, subtype)
+
+
+    # Subexpresssions
+    #
+    # There are 3 types of `Subexpressions`: functions, array literals, and
+    # parentheticals. Subexpressions have 'OPEN' and 'CLOSE' tokens. 'OPEN'
+    # is used when parsing the initial expression token (i.e., '(' or '{')
+    # and 'CLOSE' is used when parsing the closing expression token ('}' or
+    # ')').
+
+    OPEN = "OPEN"
+    CLOSE = "CLOSE"
+
+    @classmethod
+    def make_subexp(cls, value, func=False):
+        """
+        Create a subexpression token.
+
+        `value`: The value of the token
+        `func`: If True, force the token to be of type FUNC
+
+        """
+        assert value[-1] in ('{', '}', '(', ')')
+        if func:
+            assert re.match('.+\\(|\\)', value)
+            type_ = Token.FUNC
+        elif value in '{}':
+            type_ = Token.ARRAY
+        elif value in '()':
+            type_ = Token.PAREN
+        else:
+            type_ = Token.FUNC
+        subtype = cls.CLOSE if value in ')}' else cls.OPEN
+        return cls(value, type_, subtype)
+
+    def get_closer(self):
+        """Return a closing token that matches this token's type."""
+        assert self.type in (self.FUNC, self.ARRAY, self.PAREN)
+        assert self.subtype == self.OPEN
+        value = "}" if self.type == self.ARRAY else ")"
+        return self.make_subexp(value, func=self.type == self.FUNC)
+
+    # Separator tokens
+    #
+    # Argument separators always have type 'SEP' and can have one of two
+    # subtypes: 'ARG', 'ROW'. 'ARG' is used for the ',' token, when used to
+    # delimit either function arguments or array elements. 'ROW' is used for
+    # the ';' token, which is always used to delimit rows in an array
+    # literal.
+
+    ARG = "ARG"
+    ROW = "ROW"
+
+    @classmethod
+    def make_separator(cls, value):
+        """Create a separator token"""
+        assert value in (',', ';')
+        subtype = cls.ARG if value == ',' else cls.ROW
+        return cls(value, cls.SEP, subtype)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py b/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py
new file mode 100644
index 00000000..a7e90ec8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/formula/translate.py
@@ -0,0 +1,166 @@
+"""
+This module contains code to translate formulae across cells in a worksheet.
+
+The idea is that if A1 has formula "=B1+C1", then translating it to cell A2
+results in formula "=B2+C2". The algorithm relies on the formula tokenizer
+to identify the parts of the formula that need to change.
+
+"""
+
+import re
+from .tokenizer import Tokenizer, Token
+from openpyxl.utils import (
+    coordinate_to_tuple,
+    column_index_from_string,
+    get_column_letter
+)
+
+class TranslatorError(Exception):
+    """
+    Raised when a formula can't be translated across cells.
+
+    This error arises when a formula's references would be translated outside
+    the worksheet's bounds on the top or left. Excel represents these
+    situations with a #REF! literal error. E.g., if the formula at B2 is
+    '=A1', attempting to translate the formula to B1 raises TranslatorError,
+    since there's no cell above A1. Similarly, translating the same formula
+    from B2 to A2 raises TranslatorError, since there's no cell to the left of
+    A1.
+
+    """
+
+
+class Translator:
+
+    """
+    Modifies a formula so that it can be translated from one cell to another.
+
+    `formula`: The str string to translate. Must include the leading '='
+               character.
+    `origin`: The cell address (in A1 notation) where this formula was
+              defined (excluding the worksheet name).
+
+    """
+
+    def __init__(self, formula, origin):
+        # Excel errors out when a workbook has formulae in R1C1 notation,
+        # regardless of the calcPr:refMode setting, so I'm assuming the
+        # formulae stored in the workbook must be in A1 notation.
+        self.row, self.col = coordinate_to_tuple(origin)
+        self.tokenizer = Tokenizer(formula)
+
+    def get_tokens(self):
+        "Returns a list with the tokens comprising the formula."
+        return self.tokenizer.items
+
+    ROW_RANGE_RE = re.compile(r"(\$?[1-9][0-9]{0,6}):(\$?[1-9][0-9]{0,6})$")
+    COL_RANGE_RE = re.compile(r"(\$?[A-Za-z]{1,3}):(\$?[A-Za-z]{1,3})$")
+    CELL_REF_RE = re.compile(r"(\$?[A-Za-z]{1,3})(\$?[1-9][0-9]{0,6})$")
+
+    @staticmethod
+    def translate_row(row_str, rdelta):
+        """
+        Translate a range row-snippet by the given number of rows.
+        """
+        if row_str.startswith('$'):
+            return row_str
+        else:
+            new_row = int(row_str) + rdelta
+            if new_row <= 0:
+                raise TranslatorError("Formula out of range")
+            return str(new_row)
+
+    @staticmethod
+    def translate_col(col_str, cdelta):
+        """
+        Translate a range col-snippet by the given number of columns
+        """
+        if col_str.startswith('$'):
+            return col_str
+        else:
+            try:
+                return get_column_letter(
+                    column_index_from_string(col_str) + cdelta)
+            except ValueError:
+                raise TranslatorError("Formula out of range")
+
+    @staticmethod
+    def strip_ws_name(range_str):
+        "Splits out the worksheet reference, if any, from a range reference."
+        # This code assumes that named ranges cannot contain any exclamation
+        # marks. Excel refuses to create these (even using VBA), and
+        # complains of a corrupt workbook when there are names with
+        # exclamation marks. The ECMA spec only states that named ranges will
+        # be of `ST_Xstring` type, which in theory allows '!' (char code
+        # 0x21) per http://www.w3.org/TR/xml/#charsets
+        if '!' in range_str:
+            sheet, range_str = range_str.rsplit('!', 1)
+            return sheet + "!", range_str
+        return "", range_str
+
+    @classmethod
+    def translate_range(cls, range_str, rdelta, cdelta):
+        """
+        Translate an A1-style range reference to the destination cell.
+
+        `rdelta`: the row offset to add to the range
+        `cdelta`: the column offset to add to the range
+        `range_str`: an A1-style reference to a range. Potentially includes
+                     the worksheet reference. Could also be a named range.
+
+        """
+        ws_part, range_str = cls.strip_ws_name(range_str)
+        match = cls.ROW_RANGE_RE.match(range_str)  # e.g. `3:4`
+        if match is not None:
+            return (ws_part + cls.translate_row(match.group(1), rdelta) + ":"
+                    + cls.translate_row(match.group(2), rdelta))
+        match = cls.COL_RANGE_RE.match(range_str)  # e.g. `A:BC`
+        if match is not None:
+            return (ws_part + cls.translate_col(match.group(1), cdelta) + ':'
+                    + cls.translate_col(match.group(2), cdelta))
+        if ':' in range_str: # e.g. `A1:B5`
+            # The check is necessarily general because range references can
+            # have one or both endpoints specified by named ranges. I.e.,
+            # `named_range:C2`, `C2:named_range`, and `name1:name2` are all
+            # valid references. Further, Excel allows chaining multiple
+            # colons together (with unclear meaning)
+            return ws_part + ":".join(
+                cls.translate_range(piece, rdelta, cdelta)
+                for piece in range_str.split(':'))
+        match = cls.CELL_REF_RE.match(range_str)
+        if match is None:  # Must be a named range
+            return range_str
+        return (ws_part + cls.translate_col(match.group(1), cdelta)
+                + cls.translate_row(match.group(2), rdelta))
+
+    def translate_formula(self, dest=None, row_delta=0, col_delta=0):
+        """
+        Convert the formula into A1 notation, or as row and column coordinates
+
+        The formula is converted into A1 assuming it is assigned to the cell
+        whose address is `dest` (no worksheet name).
+
+        """
+        tokens = self.get_tokens()
+        if not tokens:
+            return ""
+        elif tokens[0].type == Token.LITERAL:
+            return tokens[0].value
+        out = ['=']
+        # per the spec:
+        # A compliant producer or consumer considers a defined name in the
+        # range A1-XFD1048576 to be an error. All other names outside this
+        # range can be defined as names and overrides a cell reference if an
+        # ambiguity exists. (I.18.2.5)
+        if dest:
+            row, col = coordinate_to_tuple(dest)
+            row_delta = row - self.row
+            col_delta = col - self.col
+        for token in tokens:
+            if (token.type == Token.OPERAND
+                and token.subtype == Token.RANGE):
+                out.append(self.translate_range(token.value, row_delta,
+                                                col_delta))
+            else:
+                out.append(token.value)
+        return "".join(out)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/__init__.py
new file mode 100644
index 00000000..c3085ee5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/__init__.py
@@ -0,0 +1,3 @@
+"""
+Stuff related to Office OpenXML packaging: relationships, archive, content types.
+"""
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/core.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/core.py
new file mode 100644
index 00000000..45153732
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/core.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import datetime
+
+from openpyxl.descriptors import (
+    DateTime,
+    Alias,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.nested import NestedText
+from openpyxl.xml.functions import (
+    Element,
+    QName,
+)
+from openpyxl.xml.constants import (
+    COREPROPS_NS,
+    DCORE_NS,
+    XSI_NS,
+    DCTERMS_NS,
+)
+
+
+class NestedDateTime(DateTime, NestedText):
+
+    expected_type = datetime.datetime
+
+    def to_tree(self, tagname=None, value=None, namespace=None):
+        namespace = getattr(self, "namespace", namespace)
+        if namespace is not None:
+            tagname = "{%s}%s" % (namespace, tagname)
+        el = Element(tagname)
+        if value is not None:
+            value = value.replace(tzinfo=None)
+            el.text = value.isoformat(timespec="seconds") + 'Z'
+            return el
+
+
+class QualifiedDateTime(NestedDateTime):
+
+    """In certain situations Excel will complain if the additional type
+    attribute isn't set"""
+
+    def to_tree(self, tagname=None, value=None, namespace=None):
+        el = super().to_tree(tagname, value, namespace)
+        el.set("{%s}type" % XSI_NS, QName(DCTERMS_NS, "W3CDTF"))
+        return el
+
+
+class DocumentProperties(Serialisable):
+    """High-level properties of the document.
+    Defined in ECMA-376 Par2 Annex D
+    """
+
+    tagname = "coreProperties"
+    namespace = COREPROPS_NS
+
+    category = NestedText(expected_type=str, allow_none=True)
+    contentStatus = NestedText(expected_type=str, allow_none=True)
+    keywords = NestedText(expected_type=str, allow_none=True)
+    lastModifiedBy = NestedText(expected_type=str, allow_none=True)
+    lastPrinted = NestedDateTime(allow_none=True)
+    revision = NestedText(expected_type=str, allow_none=True)
+    version = NestedText(expected_type=str, allow_none=True)
+    last_modified_by = Alias("lastModifiedBy")
+
+    # Dublin Core Properties
+    subject = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    title = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    creator = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    description = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    identifier = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    language = NestedText(expected_type=str, allow_none=True, namespace=DCORE_NS)
+    # Dublin Core Terms
+    created = QualifiedDateTime(allow_none=True, namespace=DCTERMS_NS) # assumed to be UTC
+    modified = QualifiedDateTime(allow_none=True, namespace=DCTERMS_NS) # assumed to be UTC
+
+    __elements__ = ("creator", "title", "description", "subject","identifier",
+                    "language", "created", "modified", "lastModifiedBy", "category",
+                    "contentStatus", "version", "revision", "keywords", "lastPrinted",
+                    )
+
+
+    def __init__(self,
+                 category=None,
+                 contentStatus=None,
+                 keywords=None,
+                 lastModifiedBy=None,
+                 lastPrinted=None,
+                 revision=None,
+                 version=None,
+                 created=None,
+                 creator="openpyxl",
+                 description=None,
+                 identifier=None,
+                 language=None,
+                 modified=None,
+                 subject=None,
+                 title=None,
+                 ):
+        now = datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
+        self.contentStatus = contentStatus
+        self.lastPrinted = lastPrinted
+        self.revision = revision
+        self.version = version
+        self.creator = creator
+        self.lastModifiedBy = lastModifiedBy
+        self.modified = modified or now
+        self.created = created or now
+        self.title = title
+        self.subject = subject
+        self.description = description
+        self.identifier = identifier
+        self.language = language
+        self.keywords = keywords
+        self.category = category
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/custom.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/custom.py
new file mode 100644
index 00000000..7e253d78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/custom.py
@@ -0,0 +1,289 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Implementation of custom properties see § 22.3 in the specification"""
+
+
+from warnings import warn
+
+from openpyxl.descriptors import Strict
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.sequence import Sequence
+from openpyxl.descriptors import (
+    Alias,
+    String,
+    Integer,
+    Float,
+    DateTime,
+    Bool,
+)
+from openpyxl.descriptors.nested import (
+    NestedText,
+)
+
+from openpyxl.xml.constants import (
+    CUSTPROPS_NS,
+    VTYPES_NS,
+    CPROPS_FMTID,
+)
+
+from .core import NestedDateTime
+
+
+class NestedBoolText(Bool, NestedText):
+    """
+    Descriptor for handling nested elements with the value stored in the text part
+    """
+
+    pass
+
+
+class _CustomDocumentProperty(Serialisable):
+
+    """
+    Low-level representation of a Custom Document Property.
+    Not used directly
+    Must always contain a child element, even if this is empty
+    """
+
+    tagname = "property"
+    _typ = None
+
+    name = String(allow_none=True)
+    lpwstr = NestedText(expected_type=str, allow_none=True, namespace=VTYPES_NS)
+    i4 = NestedText(expected_type=int, allow_none=True, namespace=VTYPES_NS)
+    r8 = NestedText(expected_type=float, allow_none=True, namespace=VTYPES_NS)
+    filetime = NestedDateTime(allow_none=True, namespace=VTYPES_NS)
+    bool = NestedBoolText(expected_type=bool, allow_none=True, namespace=VTYPES_NS)
+    linkTarget = String(expected_type=str, allow_none=True)
+    fmtid = String()
+    pid = Integer()
+
+    def __init__(self,
+                 name=None,
+                 pid=0,
+                 fmtid=CPROPS_FMTID,
+                 linkTarget=None,
+                 **kw):
+        self.fmtid = fmtid
+        self.pid = pid
+        self.name = name
+        self._typ = None
+        self.linkTarget = linkTarget
+
+        for k, v in kw.items():
+            setattr(self, k, v)
+            setattr(self, "_typ", k) # ugh!
+        for e in self.__elements__:
+            if e not in kw:
+                setattr(self, e, None)
+
+
+    @property
+    def type(self):
+        if self._typ is not None:
+            return self._typ
+        for a in self.__elements__:
+            if getattr(self, a) is not None:
+                return a
+        if self.linkTarget is not None:
+            return "linkTarget"
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        child = getattr(self, self._typ, None)
+        if child is None:
+            setattr(self, self._typ, "")
+
+        return super().to_tree(tagname=None, idx=None, namespace=None)
+
+
+class _CustomDocumentPropertyList(Serialisable):
+
+    """
+    Parses and seriliases property lists but is not used directly
+    """
+
+    tagname = "Properties"
+
+    property = Sequence(expected_type=_CustomDocumentProperty, namespace=CUSTPROPS_NS)
+    customProps = Alias("property")
+
+
+    def __init__(self, property=()):
+        self.property = property
+
+
+    def __len__(self):
+        return len(self.property)
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        for idx, p in enumerate(self.property, 2):
+            p.pid = idx
+        tree = super().to_tree(tagname, idx, namespace)
+        tree.set("xmlns", CUSTPROPS_NS)
+
+        return tree
+
+
+class _TypedProperty(Strict):
+
+    name = String()
+
+    def __init__(self,
+                 name,
+                 value):
+        self.name = name
+        self.value = value
+
+
+    def __eq__(self, other):
+        return self.name == other.name and self.value == other.value
+
+
+    def __repr__(self):
+        return f"{self.__class__.__name__}, name={self.name}, value={self.value}"
+
+
+class IntProperty(_TypedProperty):
+
+    value = Integer()
+
+
+class FloatProperty(_TypedProperty):
+
+    value = Float()
+
+
+class StringProperty(_TypedProperty):
+
+    value = String(allow_none=True)
+
+
+class DateTimeProperty(_TypedProperty):
+
+    value = DateTime()
+
+
+class BoolProperty(_TypedProperty):
+
+    value = Bool()
+
+
+class LinkProperty(_TypedProperty):
+
+    value = String()
+
+
+# from Python
+CLASS_MAPPING = {
+    StringProperty: "lpwstr",
+    IntProperty: "i4",
+    FloatProperty: "r8",
+    DateTimeProperty: "filetime",
+    BoolProperty: "bool",
+    LinkProperty: "linkTarget"
+}
+
+XML_MAPPING = {v:k for k,v in CLASS_MAPPING.items()}
+
+
+class CustomPropertyList(Strict):
+
+
+    props = Sequence(expected_type=_TypedProperty)
+
+    def __init__(self):
+        self.props = []
+
+
+    @classmethod
+    def from_tree(cls, tree):
+        """
+        Create list from OOXML element
+        """
+        prop_list = _CustomDocumentPropertyList.from_tree(tree)
+        props = []
+
+        for prop in prop_list.property:
+            attr = prop.type
+
+            typ = XML_MAPPING.get(attr, None)
+            if not typ:
+                warn(f"Unknown type for {prop.name}")
+                continue
+            value = getattr(prop, attr)
+            link = prop.linkTarget
+            if link is not None:
+                typ = LinkProperty
+                value = prop.linkTarget
+
+            new_prop = typ(name=prop.name, value=value)
+            props.append(new_prop)
+
+        new_prop_list = cls()
+        new_prop_list.props = props
+        return new_prop_list
+
+
+    def append(self, prop):
+        if prop.name in self.names:
+            raise ValueError(f"Property with name {prop.name} already exists")
+
+        self.props.append(prop)
+
+
+    def to_tree(self):
+        props = []
+
+        for p in self.props:
+            attr = CLASS_MAPPING.get(p.__class__, None)
+            if not attr:
+                raise TypeError("Unknown adapter for {p}")
+            np = _CustomDocumentProperty(name=p.name, **{attr:p.value})
+            if isinstance(p, LinkProperty):
+                np._typ = "lpwstr"
+                #np.lpwstr = ""
+            props.append(np)
+
+        prop_list = _CustomDocumentPropertyList(property=props)
+        return prop_list.to_tree()
+
+
+    def __len__(self):
+        return len(self.props)
+
+
+    @property
+    def names(self):
+        """List of property names"""
+        return [p.name for p in self.props]
+
+
+    def __getitem__(self, name):
+        """
+        Get property by name
+        """
+        for p in self.props:
+            if p.name == name:
+                return p
+        raise KeyError(f"Property with name {name} not found")
+
+
+    def __delitem__(self, name):
+        """
+        Delete a propery by name
+        """
+        for idx, p in enumerate(self.props):
+            if p.name == name:
+                self.props.pop(idx)
+                return
+        raise KeyError(f"Property with name {name} not found")
+
+
+    def __repr__(self):
+        return f"{self.__class__.__name__} containing {self.props}"
+
+
+    def __iter__(self):
+        return iter(self.props)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/extended.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/extended.py
new file mode 100644
index 00000000..fbd794af
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/extended.py
@@ -0,0 +1,137 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+)
+from openpyxl.descriptors.nested import (
+    NestedText,
+)
+
+from openpyxl.xml.constants import XPROPS_NS
+from openpyxl import __version__
+
+
+class DigSigBlob(Serialisable):
+
+    __elements__ = __attrs__ = ()
+
+
+class VectorLpstr(Serialisable):
+
+    __elements__ = __attrs__ = ()
+
+
+class VectorVariant(Serialisable):
+
+    __elements__ = __attrs__ = ()
+
+
+class ExtendedProperties(Serialisable):
+
+    """
+    See 22.2
+
+    Most of this is irrelevant but Excel is very picky about the version number
+
+    It uses XX.YYYY (Version.Build) and expects everyone else to
+
+    We provide Major.Minor and the full version in the application name
+    """
+
+    tagname = "Properties"
+
+    Template = NestedText(expected_type=str, allow_none=True)
+    Manager = NestedText(expected_type=str, allow_none=True)
+    Company = NestedText(expected_type=str, allow_none=True)
+    Pages = NestedText(expected_type=int, allow_none=True)
+    Words = NestedText(expected_type=int,allow_none=True)
+    Characters = NestedText(expected_type=int, allow_none=True)
+    PresentationFormat = NestedText(expected_type=str, allow_none=True)
+    Lines = NestedText(expected_type=int, allow_none=True)
+    Paragraphs = NestedText(expected_type=int, allow_none=True)
+    Slides = NestedText(expected_type=int, allow_none=True)
+    Notes = NestedText(expected_type=int, allow_none=True)
+    TotalTime = NestedText(expected_type=int, allow_none=True)
+    HiddenSlides = NestedText(expected_type=int, allow_none=True)
+    MMClips = NestedText(expected_type=int, allow_none=True)
+    ScaleCrop = NestedText(expected_type=bool, allow_none=True)
+    HeadingPairs = Typed(expected_type=VectorVariant, allow_none=True)
+    TitlesOfParts = Typed(expected_type=VectorLpstr, allow_none=True)
+    LinksUpToDate = NestedText(expected_type=bool, allow_none=True)
+    CharactersWithSpaces = NestedText(expected_type=int, allow_none=True)
+    SharedDoc = NestedText(expected_type=bool, allow_none=True)
+    HyperlinkBase = NestedText(expected_type=str, allow_none=True)
+    HLinks = Typed(expected_type=VectorVariant, allow_none=True)
+    HyperlinksChanged = NestedText(expected_type=bool, allow_none=True)
+    DigSig = Typed(expected_type=DigSigBlob, allow_none=True)
+    Application = NestedText(expected_type=str, allow_none=True)
+    AppVersion = NestedText(expected_type=str, allow_none=True)
+    DocSecurity = NestedText(expected_type=int, allow_none=True)
+
+    __elements__ = ('Application', 'AppVersion', 'DocSecurity', 'ScaleCrop',
+                    'LinksUpToDate', 'SharedDoc', 'HyperlinksChanged')
+
+    def __init__(self,
+                 Template=None,
+                 Manager=None,
+                 Company=None,
+                 Pages=None,
+                 Words=None,
+                 Characters=None,
+                 PresentationFormat=None,
+                 Lines=None,
+                 Paragraphs=None,
+                 Slides=None,
+                 Notes=None,
+                 TotalTime=None,
+                 HiddenSlides=None,
+                 MMClips=None,
+                 ScaleCrop=None,
+                 HeadingPairs=None,
+                 TitlesOfParts=None,
+                 LinksUpToDate=None,
+                 CharactersWithSpaces=None,
+                 SharedDoc=None,
+                 HyperlinkBase=None,
+                 HLinks=None,
+                 HyperlinksChanged=None,
+                 DigSig=None,
+                 Application=None,
+                 AppVersion=None,
+                 DocSecurity=None,
+                ):
+        self.Template = Template
+        self.Manager = Manager
+        self.Company = Company
+        self.Pages = Pages
+        self.Words = Words
+        self.Characters = Characters
+        self.PresentationFormat = PresentationFormat
+        self.Lines = Lines
+        self.Paragraphs = Paragraphs
+        self.Slides = Slides
+        self.Notes = Notes
+        self.TotalTime = TotalTime
+        self.HiddenSlides = HiddenSlides
+        self.MMClips = MMClips
+        self.ScaleCrop = ScaleCrop
+        self.HeadingPairs = None
+        self.TitlesOfParts = None
+        self.LinksUpToDate = LinksUpToDate
+        self.CharactersWithSpaces = CharactersWithSpaces
+        self.SharedDoc = SharedDoc
+        self.HyperlinkBase = HyperlinkBase
+        self.HLinks = None
+        self.HyperlinksChanged = HyperlinksChanged
+        self.DigSig = None
+        self.Application = f"Microsoft Excel Compatible / Openpyxl {__version__}"
+        self.AppVersion = ".".join(__version__.split(".")[:-1])
+        self.DocSecurity = DocSecurity
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", XPROPS_NS)
+        return tree
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/interface.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/interface.py
new file mode 100644
index 00000000..cacc0462
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/interface.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from abc import abstractproperty
+from openpyxl.compat.abc import ABC
+
+
+class ISerialisableFile(ABC):
+
+    """
+    Interface for Serialisable classes that represent files in the archive
+    """
+
+
+    @abstractproperty
+    def id(self):
+        """
+        Object id making it unique
+        """
+        pass
+
+
+    @abstractproperty
+    def _path(self):
+        """
+        File path in the archive
+        """
+        pass
+
+
+    @abstractproperty
+    def _namespace(self):
+        """
+        Qualified namespace when serialised
+        """
+        pass
+
+
+    @abstractproperty
+    def _type(self):
+        """
+        The content type for the manifest
+        """
+
+
+    @abstractproperty
+    def _rel_type(self):
+        """
+        The content type for relationships
+        """
+
+
+    @abstractproperty
+    def _rel_id(self):
+        """
+        Links object with parent
+        """
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/manifest.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/manifest.py
new file mode 100644
index 00000000..41da07f4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/manifest.py
@@ -0,0 +1,194 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+File manifest
+"""
+from mimetypes import MimeTypes
+import os.path
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import String, Sequence
+from openpyxl.xml.functions import fromstring
+from openpyxl.xml.constants import (
+    ARC_CONTENT_TYPES,
+    ARC_THEME,
+    ARC_STYLE,
+    THEME_TYPE,
+    STYLES_TYPE,
+    CONTYPES_NS,
+    ACTIVEX,
+    CTRL,
+    VBA,
+)
+from openpyxl.xml.functions import tostring
+
+# initialise mime-types
+mimetypes = MimeTypes()
+mimetypes.add_type('application/xml', ".xml")
+mimetypes.add_type('application/vnd.openxmlformats-package.relationships+xml', ".rels")
+mimetypes.add_type("application/vnd.ms-office.vbaProject", ".bin")
+mimetypes.add_type("application/vnd.openxmlformats-officedocument.vmlDrawing", ".vml")
+mimetypes.add_type("image/x-emf", ".emf")
+
+
+class FileExtension(Serialisable):
+
+    tagname = "Default"
+
+    Extension = String()
+    ContentType = String()
+
+    def __init__(self, Extension, ContentType):
+        self.Extension = Extension
+        self.ContentType = ContentType
+
+
+class Override(Serialisable):
+
+    tagname = "Override"
+
+    PartName = String()
+    ContentType = String()
+
+    def __init__(self, PartName, ContentType):
+        self.PartName = PartName
+        self.ContentType = ContentType
+
+
+DEFAULT_TYPES = [
+    FileExtension("rels", "application/vnd.openxmlformats-package.relationships+xml"),
+    FileExtension("xml", "application/xml"),
+]
+
+DEFAULT_OVERRIDE = [
+    Override("/" + ARC_STYLE, STYLES_TYPE), # Styles
+    Override("/" + ARC_THEME, THEME_TYPE), # Theme
+    Override("/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"),
+    Override("/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml")
+]
+
+
+class Manifest(Serialisable):
+
+    tagname = "Types"
+
+    Default = Sequence(expected_type=FileExtension, unique=True)
+    Override = Sequence(expected_type=Override, unique=True)
+    path = "[Content_Types].xml"
+
+    __elements__ = ("Default", "Override")
+
+    def __init__(self,
+                 Default=(),
+                 Override=(),
+                 ):
+        if not Default:
+            Default = DEFAULT_TYPES
+        self.Default = Default
+        if not Override:
+            Override = DEFAULT_OVERRIDE
+        self.Override = Override
+
+
+    @property
+    def filenames(self):
+        return [part.PartName for part in self.Override]
+
+
+    @property
+    def extensions(self):
+        """
+        Map content types to file extensions
+        Skip parts without extensions
+        """
+        exts = {os.path.splitext(part.PartName)[-1] for part in self.Override}
+        return [(ext[1:], mimetypes.types_map[True][ext]) for ext in sorted(exts) if ext]
+
+
+    def to_tree(self):
+        """
+        Custom serialisation method to allow setting a default namespace
+        """
+        defaults = [t.Extension for t in self.Default]
+        for ext, mime in self.extensions:
+            if ext not in defaults:
+                mime = FileExtension(ext, mime)
+                self.Default.append(mime)
+        tree = super().to_tree()
+        tree.set("xmlns", CONTYPES_NS)
+        return tree
+
+
+    def __contains__(self, content_type):
+        """
+        Check whether a particular content type is contained
+        """
+        for t in self.Override:
+            if t.ContentType == content_type:
+                return True
+
+
+    def find(self, content_type):
+        """
+        Find specific content-type
+        """
+        try:
+            return next(self.findall(content_type))
+        except StopIteration:
+            return
+
+
+    def findall(self, content_type):
+        """
+        Find all elements of a specific content-type
+        """
+        for t in self.Override:
+            if t.ContentType == content_type:
+                yield t
+
+
+    def append(self, obj):
+        """
+        Add content object to the package manifest
+        # needs a contract...
+        """
+        ct = Override(PartName=obj.path, ContentType=obj.mime_type)
+        self.Override.append(ct)
+
+
+    def _write(self, archive, workbook):
+        """
+        Write manifest to the archive
+        """
+        self.append(workbook)
+        self._write_vba(workbook)
+        self._register_mimetypes(filenames=archive.namelist())
+        archive.writestr(self.path, tostring(self.to_tree()))
+
+
+    def _register_mimetypes(self, filenames):
+        """
+        Make sure that the mime type for all file extensions is registered
+        """
+        for fn in filenames:
+            ext = os.path.splitext(fn)[-1]
+            if not ext:
+                continue
+            mime = mimetypes.types_map[True][ext]
+            fe = FileExtension(ext[1:], mime)
+            self.Default.append(fe)
+
+
+    def _write_vba(self, workbook):
+        """
+        Add content types from cached workbook when keeping VBA
+        """
+        if workbook.vba_archive:
+            node = fromstring(workbook.vba_archive.read(ARC_CONTENT_TYPES))
+            mf = Manifest.from_tree(node)
+            filenames = self.filenames
+            for override in mf.Override:
+                if override.PartName not in (ACTIVEX, CTRL, VBA):
+                    continue
+                if override.PartName not in filenames:
+                    self.Override.append(override)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/relationship.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/relationship.py
new file mode 100644
index 00000000..4318282d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/relationship.py
@@ -0,0 +1,158 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import posixpath
+from warnings import warn
+
+from openpyxl.descriptors import (
+    String,
+    Alias,
+    Sequence,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.container import ElementList
+
+from openpyxl.xml.constants import REL_NS, PKG_REL_NS
+from openpyxl.xml.functions import (
+    Element,
+    fromstring,
+)
+
+
+class Relationship(Serialisable):
+    """Represents many kinds of relationships."""
+
+    tagname = "Relationship"
+
+    Type = String()
+    Target = String()
+    target = Alias("Target")
+    TargetMode = String(allow_none=True)
+    Id = String(allow_none=True)
+    id = Alias("Id")
+
+
+    def __init__(self,
+                 Id=None,
+                 Type=None,
+                 type=None,
+                 Target=None,
+                 TargetMode=None
+                 ):
+        """
+        `type` can be used as a shorthand with the default relationships namespace
+        otherwise the `Type` must be a fully qualified URL
+        """
+        if type is not None:
+            Type = "{0}/{1}".format(REL_NS, type)
+        self.Type = Type
+        self.Target = Target
+        self.TargetMode = TargetMode
+        self.Id = Id
+
+
+class RelationshipList(ElementList):
+
+    tagname = "Relationships"
+    expected_type = Relationship
+
+
+    def append(self, value):
+        super().append(value)
+        if not value.Id:
+            value.Id = f"rId{len(self)}"
+
+
+    def find(self, content_type):
+        """
+        Find relationships by content-type
+        NB. these content-types namespaced objects and different to the MIME-types
+        in the package manifest :-(
+        """
+        for r in self:
+            if r.Type == content_type:
+                yield r
+
+
+    def get(self, key):
+        for r in self:
+            if r.Id == key:
+                return r
+        raise KeyError("Unknown relationship: {0}".format(key))
+
+
+    def to_dict(self):
+        """Return a dictionary of relations keyed by id"""
+        return {r.id:r for r in self}
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", PKG_REL_NS)
+        return tree
+
+
+def get_rels_path(path):
+    """
+    Convert relative path to absolutes that can be loaded from a zip
+    archive.
+    The path to be passed in is that of containing object (workbook,
+    worksheet, etc.)
+    """
+    folder, obj = posixpath.split(path)
+    filename = posixpath.join(folder, '_rels', '{0}.rels'.format(obj))
+    return filename
+
+
+def get_dependents(archive, filename):
+    """
+    Normalise dependency file paths to absolute ones
+
+    Relative paths are relative to parent object
+    """
+    src = archive.read(filename)
+    node = fromstring(src)
+    try:
+        rels = RelationshipList.from_tree(node)
+    except TypeError:
+        msg = "{0} contains invalid dependency definitions".format(filename)
+        warn(msg)
+        rels = RelationshipList()
+    folder = posixpath.dirname(filename)
+    parent = posixpath.split(folder)[0]
+    for r in rels:
+        if r.TargetMode == "External":
+            continue
+        elif r.target.startswith("/"):
+            r.target = r.target[1:]
+        else:
+            pth = posixpath.join(parent, r.target)
+            r.target = posixpath.normpath(pth)
+    return rels
+
+
+def get_rel(archive, deps, id=None, cls=None):
+    """
+    Get related object based on id or rel_type
+    """
+    if not any([id, cls]):
+        raise ValueError("Either the id or the content type are required")
+    if id is not None:
+        rel = deps.get(id)
+    else:
+        try:
+            rel = next(deps.find(cls.rel_type))
+        except StopIteration: # no known dependency
+            return
+
+    path = rel.target
+    src = archive.read(path)
+    tree = fromstring(src)
+    obj = cls.from_tree(tree)
+
+    rels_path = get_rels_path(path)
+    try:
+        obj.deps = get_dependents(archive, rels_path)
+    except KeyError:
+        obj.deps = []
+
+    return obj
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/packaging/workbook.py b/.venv/lib/python3.12/site-packages/openpyxl/packaging/workbook.py
new file mode 100644
index 00000000..a6413cdc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/packaging/workbook.py
@@ -0,0 +1,185 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    String,
+    Integer,
+    Bool,
+    NoneSet,
+)
+from openpyxl.descriptors.excel import ExtensionList, Relation
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.descriptors.nested import NestedString
+
+from openpyxl.xml.constants import SHEET_MAIN_NS
+
+from openpyxl.workbook.defined_name import DefinedNameList
+from openpyxl.workbook.external_reference import ExternalReference
+from openpyxl.workbook.function_group import FunctionGroupList
+from openpyxl.workbook.properties import WorkbookProperties, CalcProperties, FileVersion
+from openpyxl.workbook.protection import WorkbookProtection, FileSharing
+from openpyxl.workbook.smart_tags import SmartTagList, SmartTagProperties
+from openpyxl.workbook.views import CustomWorkbookView, BookView
+from openpyxl.workbook.web import WebPublishing, WebPublishObjectList
+
+
+class FileRecoveryProperties(Serialisable):
+
+    tagname = "fileRecoveryPr"
+
+    autoRecover = Bool(allow_none=True)
+    crashSave = Bool(allow_none=True)
+    dataExtractLoad = Bool(allow_none=True)
+    repairLoad = Bool(allow_none=True)
+
+    def __init__(self,
+                 autoRecover=None,
+                 crashSave=None,
+                 dataExtractLoad=None,
+                 repairLoad=None,
+                ):
+        self.autoRecover = autoRecover
+        self.crashSave = crashSave
+        self.dataExtractLoad = dataExtractLoad
+        self.repairLoad = repairLoad
+
+
+class ChildSheet(Serialisable):
+    """
+    Represents a reference to a worksheet or chartsheet in workbook.xml
+
+    It contains the title, order and state but only an indirect reference to
+    the objects themselves.
+    """
+
+    tagname = "sheet"
+
+    name = String()
+    sheetId = Integer()
+    state = NoneSet(values=(['visible', 'hidden', 'veryHidden']))
+    id = Relation()
+
+    def __init__(self,
+                 name=None,
+                 sheetId=None,
+                 state="visible",
+                 id=None,
+                ):
+        self.name = name
+        self.sheetId = sheetId
+        self.state = state
+        self.id = id
+
+
+class PivotCache(Serialisable):
+
+    tagname = "pivotCache"
+
+    cacheId = Integer()
+    id = Relation()
+
+    def __init__(self,
+                 cacheId=None,
+                 id=None
+                ):
+        self.cacheId = cacheId
+        self.id = id
+
+
+class WorkbookPackage(Serialisable):
+
+    """
+    Represent the workbook file in the archive
+    """
+
+    tagname = "workbook"
+
+    conformance = NoneSet(values=['strict', 'transitional'])
+    fileVersion = Typed(expected_type=FileVersion, allow_none=True)
+    fileSharing = Typed(expected_type=FileSharing, allow_none=True)
+    workbookPr = Typed(expected_type=WorkbookProperties, allow_none=True)
+    properties = Alias("workbookPr")
+    workbookProtection = Typed(expected_type=WorkbookProtection, allow_none=True)
+    bookViews = NestedSequence(expected_type=BookView)
+    sheets = NestedSequence(expected_type=ChildSheet)
+    functionGroups = Typed(expected_type=FunctionGroupList, allow_none=True)
+    externalReferences = NestedSequence(expected_type=ExternalReference)
+    definedNames = Typed(expected_type=DefinedNameList, allow_none=True)
+    calcPr = Typed(expected_type=CalcProperties, allow_none=True)
+    oleSize = NestedString(allow_none=True, attribute="ref")
+    customWorkbookViews = NestedSequence(expected_type=CustomWorkbookView)
+    pivotCaches = NestedSequence(expected_type=PivotCache, allow_none=True)
+    smartTagPr = Typed(expected_type=SmartTagProperties, allow_none=True)
+    smartTagTypes = Typed(expected_type=SmartTagList, allow_none=True)
+    webPublishing = Typed(expected_type=WebPublishing, allow_none=True)
+    fileRecoveryPr = Typed(expected_type=FileRecoveryProperties, allow_none=True)
+    webPublishObjects = Typed(expected_type=WebPublishObjectList, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    Ignorable = NestedString(namespace="http://schemas.openxmlformats.org/markup-compatibility/2006", allow_none=True)
+
+    __elements__ = ('fileVersion', 'fileSharing', 'workbookPr',
+                    'workbookProtection', 'bookViews', 'sheets', 'functionGroups',
+                    'externalReferences', 'definedNames', 'calcPr', 'oleSize',
+                    'customWorkbookViews', 'pivotCaches', 'smartTagPr', 'smartTagTypes',
+                    'webPublishing', 'fileRecoveryPr', 'webPublishObjects')
+
+    def __init__(self,
+                 conformance=None,
+                 fileVersion=None,
+                 fileSharing=None,
+                 workbookPr=None,
+                 workbookProtection=None,
+                 bookViews=(),
+                 sheets=(),
+                 functionGroups=None,
+                 externalReferences=(),
+                 definedNames=None,
+                 calcPr=None,
+                 oleSize=None,
+                 customWorkbookViews=(),
+                 pivotCaches=(),
+                 smartTagPr=None,
+                 smartTagTypes=None,
+                 webPublishing=None,
+                 fileRecoveryPr=None,
+                 webPublishObjects=None,
+                 extLst=None,
+                 Ignorable=None,
+                ):
+        self.conformance = conformance
+        self.fileVersion = fileVersion
+        self.fileSharing = fileSharing
+        if workbookPr is None:
+            workbookPr = WorkbookProperties()
+        self.workbookPr = workbookPr
+        self.workbookProtection = workbookProtection
+        self.bookViews = bookViews
+        self.sheets = sheets
+        self.functionGroups = functionGroups
+        self.externalReferences = externalReferences
+        self.definedNames = definedNames
+        self.calcPr = calcPr
+        self.oleSize = oleSize
+        self.customWorkbookViews = customWorkbookViews
+        self.pivotCaches = pivotCaches
+        self.smartTagPr = smartTagPr
+        self.smartTagTypes = smartTagTypes
+        self.webPublishing = webPublishing
+        self.fileRecoveryPr = fileRecoveryPr
+        self.webPublishObjects = webPublishObjects
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+    @property
+    def active(self):
+        for view in self.bookViews:
+            if view.activeTab is not None:
+                return view.activeTab
+        return 0
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/pivot/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/pivot/__init__.py
new file mode 100644
index 00000000..ab6cdead
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/pivot/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) 2010-2024 openpyxl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/pivot/cache.py b/.venv/lib/python3.12/site-packages/openpyxl/pivot/cache.py
new file mode 100644
index 00000000..7ae2b4dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/pivot/cache.py
@@ -0,0 +1,965 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    Float,
+    Set,
+    NoneSet,
+    String,
+    Integer,
+    DateTime,
+    Sequence,
+)
+
+from openpyxl.descriptors.excel import (
+    HexBinary,
+    ExtensionList,
+    Relation,
+)
+from openpyxl.descriptors.nested import NestedInteger
+from openpyxl.descriptors.sequence import (
+    NestedSequence,
+    MultiSequence,
+    MultiSequencePart,
+)
+from openpyxl.xml.constants import SHEET_MAIN_NS
+from openpyxl.xml.functions import tostring
+from openpyxl.packaging.relationship import (
+    RelationshipList,
+    Relationship,
+    get_rels_path
+)
+
+from .table import (
+    PivotArea,
+    Reference,
+)
+from .fields import (
+    Boolean,
+    Error,
+    Missing,
+    Number,
+    Text,
+    TupleList,
+    DateTimeField,
+)
+
+class MeasureDimensionMap(Serialisable):
+
+    tagname = "map"
+
+    measureGroup = Integer(allow_none=True)
+    dimension = Integer(allow_none=True)
+
+    def __init__(self,
+                 measureGroup=None,
+                 dimension=None,
+                ):
+        self.measureGroup = measureGroup
+        self.dimension = dimension
+
+
+class MeasureGroup(Serialisable):
+
+    tagname = "measureGroup"
+
+    name = String()
+    caption = String()
+
+    def __init__(self,
+                 name=None,
+                 caption=None,
+                ):
+        self.name = name
+        self.caption = caption
+
+
+class PivotDimension(Serialisable):
+
+    tagname = "dimension"
+
+    measure = Bool()
+    name = String()
+    uniqueName = String()
+    caption = String()
+
+    def __init__(self,
+                 measure=None,
+                 name=None,
+                 uniqueName=None,
+                 caption=None,
+                ):
+        self.measure = measure
+        self.name = name
+        self.uniqueName = uniqueName
+        self.caption = caption
+
+
+class CalculatedMember(Serialisable):
+
+    tagname = "calculatedMember"
+
+    name = String()
+    mdx = String()
+    memberName = String(allow_none=True)
+    hierarchy = String(allow_none=True)
+    parent = String(allow_none=True)
+    solveOrder = Integer(allow_none=True)
+    set = Bool()
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 name=None,
+                 mdx=None,
+                 memberName=None,
+                 hierarchy=None,
+                 parent=None,
+                 solveOrder=None,
+                 set=None,
+                 extLst=None,
+                ):
+        self.name = name
+        self.mdx = mdx
+        self.memberName = memberName
+        self.hierarchy = hierarchy
+        self.parent = parent
+        self.solveOrder = solveOrder
+        self.set = set
+        #self.extLst = extLst
+
+
+class CalculatedItem(Serialisable):
+
+    tagname = "calculatedItem"
+
+    field = Integer(allow_none=True)
+    formula = String()
+    pivotArea = Typed(expected_type=PivotArea, )
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('pivotArea', 'extLst')
+
+    def __init__(self,
+                 field=None,
+                 formula=None,
+                 pivotArea=None,
+                 extLst=None,
+                ):
+        self.field = field
+        self.formula = formula
+        self.pivotArea = pivotArea
+        self.extLst = extLst
+
+
+class ServerFormat(Serialisable):
+
+    tagname = "serverFormat"
+
+    culture = String(allow_none=True)
+    format = String(allow_none=True)
+
+    def __init__(self,
+                 culture=None,
+                 format=None,
+                ):
+        self.culture = culture
+        self.format = format
+
+
+class Query(Serialisable):
+
+    tagname = "query"
+
+    mdx = String()
+    tpls = Typed(expected_type=TupleList, allow_none=True)
+
+    __elements__ = ('tpls',)
+
+    def __init__(self,
+                 mdx=None,
+                 tpls=None,
+                ):
+        self.mdx = mdx
+        self.tpls = tpls
+
+
+class OLAPSet(Serialisable):
+
+    tagname = "set"
+
+    count = Integer()
+    maxRank = Integer()
+    setDefinition = String()
+    sortType = NoneSet(values=(['ascending', 'descending', 'ascendingAlpha',
+                                'descendingAlpha', 'ascendingNatural', 'descendingNatural']))
+    queryFailed = Bool()
+    tpls = Typed(expected_type=TupleList, allow_none=True)
+    sortByTuple = Typed(expected_type=TupleList, allow_none=True)
+
+    __elements__ = ('tpls', 'sortByTuple')
+
+    def __init__(self,
+                 count=None,
+                 maxRank=None,
+                 setDefinition=None,
+                 sortType=None,
+                 queryFailed=None,
+                 tpls=None,
+                 sortByTuple=None,
+                ):
+        self.count = count
+        self.maxRank = maxRank
+        self.setDefinition = setDefinition
+        self.sortType = sortType
+        self.queryFailed = queryFailed
+        self.tpls = tpls
+        self.sortByTuple = sortByTuple
+
+
+class PCDSDTCEntries(Serialisable):
+    # Implements CT_PCDSDTCEntries
+
+    tagname = "entries"
+
+    count = Integer(allow_none=True)
+    # elements are choice
+    m = Typed(expected_type=Missing, allow_none=True)
+    n = Typed(expected_type=Number, allow_none=True)
+    e = Typed(expected_type=Error, allow_none=True)
+    s = Typed(expected_type=Text, allow_none=True)
+
+    __elements__ = ('m', 'n', 'e', 's')
+
+    def __init__(self,
+                 count=None,
+                 m=None,
+                 n=None,
+                 e=None,
+                 s=None,
+                ):
+        self.count = count
+        self.m = m
+        self.n = n
+        self.e = e
+        self.s = s
+
+
+class TupleCache(Serialisable):
+
+    tagname = "tupleCache"
+
+    entries = Typed(expected_type=PCDSDTCEntries, allow_none=True)
+    sets = NestedSequence(expected_type=OLAPSet, count=True)
+    queryCache = NestedSequence(expected_type=Query, count=True)
+    serverFormats = NestedSequence(expected_type=ServerFormat, count=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('entries', 'sets', 'queryCache', 'serverFormats', 'extLst')
+
+    def __init__(self,
+                 entries=None,
+                 sets=(),
+                 queryCache=(),
+                 serverFormats=(),
+                 extLst=None,
+                ):
+        self.entries = entries
+        self.sets = sets
+        self.queryCache = queryCache
+        self.serverFormats = serverFormats
+        self.extLst = extLst
+
+
+class OLAPKPI(Serialisable):
+
+    tagname = "kpi"
+
+    uniqueName = String()
+    caption = String(allow_none=True)
+    displayFolder = String(allow_none=True)
+    measureGroup = String(allow_none=True)
+    parent = String(allow_none=True)
+    value = String()
+    goal = String(allow_none=True)
+    status = String(allow_none=True)
+    trend = String(allow_none=True)
+    weight = String(allow_none=True)
+    time = String(allow_none=True)
+
+    def __init__(self,
+                 uniqueName=None,
+                 caption=None,
+                 displayFolder=None,
+                 measureGroup=None,
+                 parent=None,
+                 value=None,
+                 goal=None,
+                 status=None,
+                 trend=None,
+                 weight=None,
+                 time=None,
+                ):
+        self.uniqueName = uniqueName
+        self.caption = caption
+        self.displayFolder = displayFolder
+        self.measureGroup = measureGroup
+        self.parent = parent
+        self.value = value
+        self.goal = goal
+        self.status = status
+        self.trend = trend
+        self.weight = weight
+        self.time = time
+
+
+class GroupMember(Serialisable):
+
+    tagname = "groupMember"
+
+    uniqueName = String()
+    group = Bool()
+
+    def __init__(self,
+                 uniqueName=None,
+                 group=None,
+                ):
+        self.uniqueName = uniqueName
+        self.group = group
+
+
+class LevelGroup(Serialisable):
+
+    tagname = "group"
+
+    name = String()
+    uniqueName = String()
+    caption = String()
+    uniqueParent = String()
+    id = Integer()
+    groupMembers = NestedSequence(expected_type=GroupMember, count=True)
+
+    __elements__ = ('groupMembers',)
+
+    def __init__(self,
+                 name=None,
+                 uniqueName=None,
+                 caption=None,
+                 uniqueParent=None,
+                 id=None,
+                 groupMembers=(),
+                ):
+        self.name = name
+        self.uniqueName = uniqueName
+        self.caption = caption
+        self.uniqueParent = uniqueParent
+        self.id = id
+        self.groupMembers = groupMembers
+
+
+class GroupLevel(Serialisable):
+
+    tagname = "groupLevel"
+
+    uniqueName = String()
+    caption = String()
+    user = Bool()
+    customRollUp = Bool()
+    groups = NestedSequence(expected_type=LevelGroup, count=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('groups', 'extLst')
+
+    def __init__(self,
+                 uniqueName=None,
+                 caption=None,
+                 user=None,
+                 customRollUp=None,
+                 groups=(),
+                 extLst=None,
+                ):
+        self.uniqueName = uniqueName
+        self.caption = caption
+        self.user = user
+        self.customRollUp = customRollUp
+        self.groups = groups
+        self.extLst = extLst
+
+
+class FieldUsage(Serialisable):
+
+    tagname = "fieldUsage"
+
+    x = Integer()
+
+    def __init__(self,
+                 x=None,
+                ):
+        self.x = x
+
+
+class CacheHierarchy(Serialisable):
+
+    tagname = "cacheHierarchy"
+
+    uniqueName = String()
+    caption = String(allow_none=True)
+    measure = Bool()
+    set = Bool()
+    parentSet = Integer(allow_none=True)
+    iconSet = Integer()
+    attribute = Bool()
+    time = Bool()
+    keyAttribute = Bool()
+    defaultMemberUniqueName = String(allow_none=True)
+    allUniqueName = String(allow_none=True)
+    allCaption = String(allow_none=True)
+    dimensionUniqueName = String(allow_none=True)
+    displayFolder = String(allow_none=True)
+    measureGroup = String(allow_none=True)
+    measures = Bool()
+    count = Integer()
+    oneField = Bool()
+    memberValueDatatype = Integer(allow_none=True)
+    unbalanced = Bool(allow_none=True)
+    unbalancedGroup = Bool(allow_none=True)
+    hidden = Bool()
+    fieldsUsage = NestedSequence(expected_type=FieldUsage, count=True)
+    groupLevels = NestedSequence(expected_type=GroupLevel, count=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('fieldsUsage', 'groupLevels')
+
+    def __init__(self,
+                 uniqueName="",
+                 caption=None,
+                 measure=None,
+                 set=None,
+                 parentSet=None,
+                 iconSet=0,
+                 attribute=None,
+                 time=None,
+                 keyAttribute=None,
+                 defaultMemberUniqueName=None,
+                 allUniqueName=None,
+                 allCaption=None,
+                 dimensionUniqueName=None,
+                 displayFolder=None,
+                 measureGroup=None,
+                 measures=None,
+                 count=None,
+                 oneField=None,
+                 memberValueDatatype=None,
+                 unbalanced=None,
+                 unbalancedGroup=None,
+                 hidden=None,
+                 fieldsUsage=(),
+                 groupLevels=(),
+                 extLst=None,
+                ):
+        self.uniqueName = uniqueName
+        self.caption = caption
+        self.measure = measure
+        self.set = set
+        self.parentSet = parentSet
+        self.iconSet = iconSet
+        self.attribute = attribute
+        self.time = time
+        self.keyAttribute = keyAttribute
+        self.defaultMemberUniqueName = defaultMemberUniqueName
+        self.allUniqueName = allUniqueName
+        self.allCaption = allCaption
+        self.dimensionUniqueName = dimensionUniqueName
+        self.displayFolder = displayFolder
+        self.measureGroup = measureGroup
+        self.measures = measures
+        self.count = count
+        self.oneField = oneField
+        self.memberValueDatatype = memberValueDatatype
+        self.unbalanced = unbalanced
+        self.unbalancedGroup = unbalancedGroup
+        self.hidden = hidden
+        self.fieldsUsage = fieldsUsage
+        self.groupLevels = groupLevels
+        self.extLst = extLst
+
+
+class GroupItems(Serialisable):
+
+    tagname = "groupItems"
+
+    m = Sequence(expected_type=Missing)
+    n = Sequence(expected_type=Number)
+    b = Sequence(expected_type=Boolean)
+    e = Sequence(expected_type=Error)
+    s = Sequence(expected_type=Text)
+    d = Sequence(expected_type=DateTimeField,)
+
+    __elements__ = ('m', 'n', 'b', 'e', 's', 'd')
+    __attrs__ = ("count", )
+
+    def __init__(self,
+                 count=None,
+                 m=(),
+                 n=(),
+                 b=(),
+                 e=(),
+                 s=(),
+                 d=(),
+                ):
+        self.m = m
+        self.n = n
+        self.b = b
+        self.e = e
+        self.s = s
+        self.d = d
+
+
+    @property
+    def count(self):
+        return len(self.m + self.n + self.b + self.e + self.s + self.d)
+
+
+class RangePr(Serialisable):
+
+    tagname = "rangePr"
+
+    autoStart = Bool(allow_none=True)
+    autoEnd = Bool(allow_none=True)
+    groupBy = NoneSet(values=(['range', 'seconds', 'minutes', 'hours', 'days',
+                           'months', 'quarters', 'years']))
+    startNum = Float(allow_none=True)
+    endNum = Float(allow_none=True)
+    startDate = DateTime(allow_none=True)
+    endDate = DateTime(allow_none=True)
+    groupInterval = Float(allow_none=True)
+
+    def __init__(self,
+                 autoStart=True,
+                 autoEnd=True,
+                 groupBy="range",
+                 startNum=None,
+                 endNum=None,
+                 startDate=None,
+                 endDate=None,
+                 groupInterval=1,
+                ):
+        self.autoStart = autoStart
+        self.autoEnd = autoEnd
+        self.groupBy = groupBy
+        self.startNum = startNum
+        self.endNum = endNum
+        self.startDate = startDate
+        self.endDate = endDate
+        self.groupInterval = groupInterval
+
+
+class FieldGroup(Serialisable):
+
+    tagname = "fieldGroup"
+
+    par = Integer(allow_none=True)
+    base = Integer(allow_none=True)
+    rangePr = Typed(expected_type=RangePr, allow_none=True)
+    discretePr = NestedSequence(expected_type=NestedInteger, count=True)
+    groupItems = Typed(expected_type=GroupItems, allow_none=True)
+
+    __elements__ = ('rangePr', 'discretePr', 'groupItems')
+
+    def __init__(self,
+                 par=None,
+                 base=None,
+                 rangePr=None,
+                 discretePr=(),
+                 groupItems=None,
+                ):
+        self.par = par
+        self.base = base
+        self.rangePr = rangePr
+        self.discretePr = discretePr
+        self.groupItems = groupItems
+
+
+class SharedItems(Serialisable):
+
+    tagname = "sharedItems"
+
+    _fields = MultiSequence()
+    m = MultiSequencePart(expected_type=Missing, store="_fields")
+    n = MultiSequencePart(expected_type=Number, store="_fields")
+    b = MultiSequencePart(expected_type=Boolean, store="_fields")
+    e = MultiSequencePart(expected_type=Error, store="_fields")
+    s = MultiSequencePart(expected_type=Text,  store="_fields")
+    d = MultiSequencePart(expected_type=DateTimeField, store="_fields")
+    # attributes are optional and must be derived from associated cache records
+    containsSemiMixedTypes = Bool(allow_none=True)
+    containsNonDate = Bool(allow_none=True)
+    containsDate = Bool(allow_none=True)
+    containsString = Bool(allow_none=True)
+    containsBlank = Bool(allow_none=True)
+    containsMixedTypes = Bool(allow_none=True)
+    containsNumber = Bool(allow_none=True)
+    containsInteger = Bool(allow_none=True)
+    minValue = Float(allow_none=True)
+    maxValue = Float(allow_none=True)
+    minDate = DateTime(allow_none=True)
+    maxDate = DateTime(allow_none=True)
+    longText = Bool(allow_none=True)
+
+    __attrs__ = ('count', 'containsBlank', 'containsDate', 'containsInteger',
+                 'containsMixedTypes', 'containsNonDate', 'containsNumber',
+                 'containsSemiMixedTypes', 'containsString', 'minValue', 'maxValue',
+                 'minDate', 'maxDate', 'longText')
+
+    def __init__(self,
+                 _fields=(),
+                 containsSemiMixedTypes=None,
+                 containsNonDate=None,
+                 containsDate=None,
+                 containsString=None,
+                 containsBlank=None,
+                 containsMixedTypes=None,
+                 containsNumber=None,
+                 containsInteger=None,
+                 minValue=None,
+                 maxValue=None,
+                 minDate=None,
+                 maxDate=None,
+                 count=None,
+                 longText=None,
+                ):
+        self._fields = _fields
+        self.containsBlank = containsBlank
+        self.containsDate = containsDate
+        self.containsNonDate = containsNonDate
+        self.containsString = containsString
+        self.containsMixedTypes = containsMixedTypes
+        self.containsSemiMixedTypes = containsSemiMixedTypes
+        self.containsNumber = containsNumber
+        self.containsInteger = containsInteger
+        self.minValue = minValue
+        self.maxValue = maxValue
+        self.minDate = minDate
+        self.maxDate = maxDate
+        self.longText = longText
+
+
+    @property
+    def count(self):
+        return len(self._fields)
+
+
+class CacheField(Serialisable):
+
+    tagname = "cacheField"
+
+    sharedItems = Typed(expected_type=SharedItems, allow_none=True)
+    fieldGroup = Typed(expected_type=FieldGroup, allow_none=True)
+    mpMap = NestedInteger(allow_none=True, attribute="v")
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    name = String()
+    caption = String(allow_none=True)
+    propertyName = String(allow_none=True)
+    serverField = Bool(allow_none=True)
+    uniqueList = Bool(allow_none=True)
+    numFmtId = Integer(allow_none=True)
+    formula = String(allow_none=True)
+    sqlType = Integer(allow_none=True)
+    hierarchy = Integer(allow_none=True)
+    level = Integer(allow_none=True)
+    databaseField = Bool(allow_none=True)
+    mappingCount = Integer(allow_none=True)
+    memberPropertyField = Bool(allow_none=True)
+
+    __elements__ = ('sharedItems', 'fieldGroup', 'mpMap')
+
+    def __init__(self,
+                 sharedItems=None,
+                 fieldGroup=None,
+                 mpMap=None,
+                 extLst=None,
+                 name=None,
+                 caption=None,
+                 propertyName=None,
+                 serverField=None,
+                 uniqueList=True,
+                 numFmtId=None,
+                 formula=None,
+                 sqlType=0,
+                 hierarchy=0,
+                 level=0,
+                 databaseField=True,
+                 mappingCount=None,
+                 memberPropertyField=None,
+                ):
+        self.sharedItems = sharedItems
+        self.fieldGroup = fieldGroup
+        self.mpMap = mpMap
+        self.extLst = extLst
+        self.name = name
+        self.caption = caption
+        self.propertyName = propertyName
+        self.serverField = serverField
+        self.uniqueList = uniqueList
+        self.numFmtId = numFmtId
+        self.formula = formula
+        self.sqlType = sqlType
+        self.hierarchy = hierarchy
+        self.level = level
+        self.databaseField = databaseField
+        self.mappingCount = mappingCount
+        self.memberPropertyField = memberPropertyField
+
+
+class RangeSet(Serialisable):
+
+    tagname = "rangeSet"
+
+    i1 = Integer(allow_none=True)
+    i2 = Integer(allow_none=True)
+    i3 = Integer(allow_none=True)
+    i4 = Integer(allow_none=True)
+    ref = String()
+    name = String(allow_none=True)
+    sheet = String(allow_none=True)
+
+    def __init__(self,
+                 i1=None,
+                 i2=None,
+                 i3=None,
+                 i4=None,
+                 ref=None,
+                 name=None,
+                 sheet=None,
+                ):
+        self.i1 = i1
+        self.i2 = i2
+        self.i3 = i3
+        self.i4 = i4
+        self.ref = ref
+        self.name = name
+        self.sheet = sheet
+
+
+class PageItem(Serialisable):
+
+    tagname = "pageItem"
+
+    name = String()
+
+    def __init__(self,
+                 name=None,
+                ):
+        self.name = name
+
+
+class Consolidation(Serialisable):
+
+    tagname = "consolidation"
+
+    autoPage = Bool(allow_none=True)
+    pages = NestedSequence(expected_type=PageItem, count=True)
+    rangeSets = NestedSequence(expected_type=RangeSet, count=True)
+
+    __elements__ = ('pages', 'rangeSets')
+
+    def __init__(self,
+                 autoPage=None,
+                 pages=(),
+                 rangeSets=(),
+                ):
+        self.autoPage = autoPage
+        self.pages = pages
+        self.rangeSets = rangeSets
+
+
+class WorksheetSource(Serialisable):
+
+    tagname = "worksheetSource"
+
+    ref = String(allow_none=True)
+    name = String(allow_none=True)
+    sheet = String(allow_none=True)
+
+    def __init__(self,
+                 ref=None,
+                 name=None,
+                 sheet=None,
+                ):
+        self.ref = ref
+        self.name = name
+        self.sheet = sheet
+
+
+class CacheSource(Serialisable):
+
+    tagname = "cacheSource"
+
+    type = Set(values=(['worksheet', 'external', 'consolidation', 'scenario']))
+    connectionId = Integer(allow_none=True)
+    # some elements are choice
+    worksheetSource = Typed(expected_type=WorksheetSource, allow_none=True)
+    consolidation = Typed(expected_type=Consolidation, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('worksheetSource', 'consolidation',)
+
+    def __init__(self,
+                 type=None,
+                 connectionId=None,
+                 worksheetSource=None,
+                 consolidation=None,
+                 extLst=None,
+                ):
+        self.type = type
+        self.connectionId = connectionId
+        self.worksheetSource = worksheetSource
+        self.consolidation = consolidation
+
+
+class CacheDefinition(Serialisable):
+
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
+    rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
+    _id = 1
+    _path = "/xl/pivotCache/pivotCacheDefinition{0}.xml"
+    records = None
+
+    tagname = "pivotCacheDefinition"
+
+    invalid = Bool(allow_none=True)
+    saveData = Bool(allow_none=True)
+    refreshOnLoad = Bool(allow_none=True)
+    optimizeMemory = Bool(allow_none=True)
+    enableRefresh = Bool(allow_none=True)
+    refreshedBy = String(allow_none=True)
+    refreshedDate = Float(allow_none=True)
+    refreshedDateIso = DateTime(allow_none=True)
+    backgroundQuery = Bool(allow_none=True)
+    missingItemsLimit = Integer(allow_none=True)
+    createdVersion = Integer(allow_none=True)
+    refreshedVersion = Integer(allow_none=True)
+    minRefreshableVersion = Integer(allow_none=True)
+    recordCount = Integer(allow_none=True)
+    upgradeOnRefresh = Bool(allow_none=True)
+    supportSubquery = Bool(allow_none=True)
+    supportAdvancedDrill = Bool(allow_none=True)
+    cacheSource = Typed(expected_type=CacheSource)
+    cacheFields = NestedSequence(expected_type=CacheField, count=True)
+    cacheHierarchies = NestedSequence(expected_type=CacheHierarchy, allow_none=True)
+    kpis = NestedSequence(expected_type=OLAPKPI, count=True)
+    tupleCache = Typed(expected_type=TupleCache, allow_none=True)
+    calculatedItems = NestedSequence(expected_type=CalculatedItem, count=True)
+    calculatedMembers = NestedSequence(expected_type=CalculatedMember, count=True)
+    dimensions = NestedSequence(expected_type=PivotDimension, allow_none=True)
+    measureGroups = NestedSequence(expected_type=MeasureGroup, count=True)
+    maps = NestedSequence(expected_type=MeasureDimensionMap, count=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    id = Relation()
+
+    __elements__ = ('cacheSource', 'cacheFields', 'cacheHierarchies', 'kpis',
+                    'tupleCache', 'calculatedItems', 'calculatedMembers', 'dimensions',
+                    'measureGroups', 'maps',)
+
+    def __init__(self,
+                 invalid=None,
+                 saveData=None,
+                 refreshOnLoad=None,
+                 optimizeMemory=None,
+                 enableRefresh=None,
+                 refreshedBy=None,
+                 refreshedDate=None,
+                 refreshedDateIso=None,
+                 backgroundQuery=None,
+                 missingItemsLimit=None,
+                 createdVersion=None,
+                 refreshedVersion=None,
+                 minRefreshableVersion=None,
+                 recordCount=None,
+                 upgradeOnRefresh=None,
+                 tupleCache=None,
+                 supportSubquery=None,
+                 supportAdvancedDrill=None,
+                 cacheSource=None,
+                 cacheFields=(),
+                 cacheHierarchies=(),
+                 kpis=(),
+                 calculatedItems=(),
+                 calculatedMembers=(),
+                 dimensions=(),
+                 measureGroups=(),
+                 maps=(),
+                 extLst=None,
+                 id = None,
+                ):
+        self.invalid = invalid
+        self.saveData = saveData
+        self.refreshOnLoad = refreshOnLoad
+        self.optimizeMemory = optimizeMemory
+        self.enableRefresh = enableRefresh
+        self.refreshedBy = refreshedBy
+        self.refreshedDate = refreshedDate
+        self.refreshedDateIso = refreshedDateIso
+        self.backgroundQuery = backgroundQuery
+        self.missingItemsLimit = missingItemsLimit
+        self.createdVersion = createdVersion
+        self.refreshedVersion = refreshedVersion
+        self.minRefreshableVersion = minRefreshableVersion
+        self.recordCount = recordCount
+        self.upgradeOnRefresh = upgradeOnRefresh
+        self.supportSubquery = supportSubquery
+        self.supportAdvancedDrill = supportAdvancedDrill
+        self.cacheSource = cacheSource
+        self.cacheFields = cacheFields
+        self.cacheHierarchies = cacheHierarchies
+        self.kpis = kpis
+        self.tupleCache = tupleCache
+        self.calculatedItems = calculatedItems
+        self.calculatedMembers = calculatedMembers
+        self.dimensions = dimensions
+        self.measureGroups = measureGroups
+        self.maps = maps
+        self.id = id
+
+
+    def to_tree(self):
+        node = super().to_tree()
+        node.set("xmlns", SHEET_MAIN_NS)
+        return node
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
+
+
+    def _write(self, archive, manifest):
+        """
+        Add to zipfile and update manifest
+        """
+        self._write_rels(archive, manifest)
+        xml = tostring(self.to_tree())
+        archive.writestr(self.path[1:], xml)
+        manifest.append(self)
+
+
+    def _write_rels(self, archive, manifest):
+        """
+        Write the relevant child objects and add links
+        """
+        if self.records is None:
+            return
+
+        rels = RelationshipList()
+        r = Relationship(Type=self.records.rel_type, Target=self.records.path)
+        rels.append(r)
+        self.id = r.id
+        self.records._id = self._id
+        self.records._write(archive, manifest)
+
+        path = get_rels_path(self.path)
+        xml = tostring(rels.to_tree())
+        archive.writestr(path[1:], xml)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/pivot/fields.py b/.venv/lib/python3.12/site-packages/openpyxl/pivot/fields.py
new file mode 100644
index 00000000..cd6bcb28
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/pivot/fields.py
@@ -0,0 +1,326 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    DateTime,
+    Bool,
+    Float,
+    String,
+    Integer,
+    Sequence,
+)
+from openpyxl.descriptors.excel import HexBinary
+
+class Index(Serialisable):
+
+    tagname = "x"
+
+    v = Integer(allow_none=True)
+
+    def __init__(self,
+                 v=0,
+                ):
+        self.v = v
+
+
+class Tuple(Serialisable):
+
+    tagname = "tpl"
+
+    fld = Integer(allow_none=True)
+    hier = Integer(allow_none=True)
+    item = Integer()
+
+    def __init__(self,
+                 fld=None,
+                 hier=None,
+                 item=None,
+                ):
+        self.fld = fld
+        self.hier = hier
+        self.item = item
+
+
+class TupleList(Serialisable):
+
+    tagname = "tpls"
+
+    c = Integer(allow_none=True)
+    tpl = Typed(expected_type=Tuple, )
+
+    __elements__ = ('tpl',)
+
+    def __init__(self,
+                 c=None,
+                 tpl=None,
+                ):
+        self.c = c
+        self.tpl = tpl
+
+
+class Missing(Serialisable):
+
+    tagname = "m"
+
+    tpls = Sequence(expected_type=TupleList)
+    x = Sequence(expected_type=Index)
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+    _in = Integer(allow_none=True)
+    bc = HexBinary(allow_none=True)
+    fc = HexBinary(allow_none=True)
+    i = Bool(allow_none=True)
+    un = Bool(allow_none=True)
+    st = Bool(allow_none=True)
+    b = Bool(allow_none=True)
+
+    __elements__ = ('tpls', 'x')
+
+    def __init__(self,
+                 tpls=(),
+                 x=(),
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                 _in=None,
+                 bc=None,
+                 fc=None,
+                 i=None,
+                 un=None,
+                 st=None,
+                 b=None,
+                ):
+        self.tpls = tpls
+        self.x = x
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
+        self._in = _in
+        self.bc = bc
+        self.fc = fc
+        self.i = i
+        self.un = un
+        self.st = st
+        self.b = b
+
+
+class Number(Serialisable):
+
+    tagname = "n"
+
+    tpls = Sequence(expected_type=TupleList)
+    x = Sequence(expected_type=Index)
+    v = Float()
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+    _in = Integer(allow_none=True)
+    bc = HexBinary(allow_none=True)
+    fc = HexBinary(allow_none=True)
+    i = Bool(allow_none=True)
+    un = Bool(allow_none=True)
+    st = Bool(allow_none=True)
+    b = Bool(allow_none=True)
+
+    __elements__ = ('tpls', 'x')
+
+    def __init__(self,
+                 tpls=(),
+                 x=(),
+                 v=None,
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                 _in=None,
+                 bc=None,
+                 fc=None,
+                 i=None,
+                 un=None,
+                 st=None,
+                 b=None,
+                ):
+        self.tpls = tpls
+        self.x = x
+        self.v = v
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
+        self._in = _in
+        self.bc = bc
+        self.fc = fc
+        self.i = i
+        self.un = un
+        self.st = st
+        self.b = b
+
+
+class Error(Serialisable):
+
+    tagname = "e"
+
+    tpls = Typed(expected_type=TupleList, allow_none=True)
+    x = Sequence(expected_type=Index)
+    v = String()
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+    _in = Integer(allow_none=True)
+    bc = HexBinary(allow_none=True)
+    fc = HexBinary(allow_none=True)
+    i = Bool(allow_none=True)
+    un = Bool(allow_none=True)
+    st = Bool(allow_none=True)
+    b = Bool(allow_none=True)
+
+    __elements__ = ('tpls', 'x')
+
+    def __init__(self,
+                 tpls=None,
+                 x=(),
+                 v=None,
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                 _in=None,
+                 bc=None,
+                 fc=None,
+                 i=None,
+                 un=None,
+                 st=None,
+                 b=None,
+                ):
+        self.tpls = tpls
+        self.x = x
+        self.v = v
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
+        self._in = _in
+        self.bc = bc
+        self.fc = fc
+        self.i = i
+        self.un = un
+        self.st = st
+        self.b = b
+
+
+class Boolean(Serialisable):
+
+    tagname = "b"
+
+    x = Sequence(expected_type=Index)
+    v = Bool()
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+
+    __elements__ = ('x',)
+
+    def __init__(self,
+                 x=(),
+                 v=None,
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                ):
+        self.x = x
+        self.v = v
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
+
+
+class Text(Serialisable):
+
+    tagname = "s"
+
+    tpls = Sequence(expected_type=TupleList)
+    x = Sequence(expected_type=Index)
+    v = String()
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+    _in = Integer(allow_none=True)
+    bc = HexBinary(allow_none=True)
+    fc = HexBinary(allow_none=True)
+    i = Bool(allow_none=True)
+    un = Bool(allow_none=True)
+    st = Bool(allow_none=True)
+    b = Bool(allow_none=True)
+
+    __elements__ = ('tpls', 'x')
+
+    def __init__(self,
+                 tpls=(),
+                 x=(),
+                 v=None,
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                 _in=None,
+                 bc=None,
+                 fc=None,
+                 i=None,
+                 un=None,
+                 st=None,
+                 b=None,
+                 ):
+        self.tpls = tpls
+        self.x = x
+        self.v = v
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
+        self._in = _in
+        self.bc = bc
+        self.fc = fc
+        self.i = i
+        self.un = un
+        self.st = st
+        self.b = b
+
+
+class DateTimeField(Serialisable):
+
+    tagname = "d"
+
+    x = Sequence(expected_type=Index)
+    v = DateTime()
+    u = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    c = String(allow_none=True)
+    cp = Integer(allow_none=True)
+
+    __elements__ = ('x',)
+
+    def __init__(self,
+                 x=(),
+                 v=None,
+                 u=None,
+                 f=None,
+                 c=None,
+                 cp=None,
+                 ):
+        self.x = x
+        self.v = v
+        self.u = u
+        self.f = f
+        self.c = c
+        self.cp = cp
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/pivot/record.py b/.venv/lib/python3.12/site-packages/openpyxl/pivot/record.py
new file mode 100644
index 00000000..42603770
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/pivot/record.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Sequence,
+)
+from openpyxl.descriptors.sequence import (
+    MultiSequence,
+    MultiSequencePart,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.nested import (
+    NestedInteger,
+    NestedBool,
+)
+
+from openpyxl.xml.constants import SHEET_MAIN_NS
+from openpyxl.xml.functions import tostring
+
+from .fields import (
+    Boolean,
+    Error,
+    Missing,
+    Number,
+    Text,
+    TupleList,
+    DateTimeField,
+    Index,
+)
+
+
+class Record(Serialisable):
+
+    tagname = "r"
+
+    _fields = MultiSequence()
+    m = MultiSequencePart(expected_type=Missing, store="_fields")
+    n = MultiSequencePart(expected_type=Number, store="_fields")
+    b = MultiSequencePart(expected_type=Boolean, store="_fields")
+    e = MultiSequencePart(expected_type=Error, store="_fields")
+    s = MultiSequencePart(expected_type=Text,  store="_fields")
+    d = MultiSequencePart(expected_type=DateTimeField, store="_fields")
+    x = MultiSequencePart(expected_type=Index, store="_fields")
+
+
+    def __init__(self,
+                 _fields=(),
+                 m=None,
+                 n=None,
+                 b=None,
+                 e=None,
+                 s=None,
+                 d=None,
+                 x=None,
+                ):
+        self._fields = _fields
+
+
+class RecordList(Serialisable):
+
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
+    rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords"
+    _id = 1
+    _path = "/xl/pivotCache/pivotCacheRecords{0}.xml"
+
+    tagname ="pivotCacheRecords"
+
+    r = Sequence(expected_type=Record, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('r', )
+    __attrs__ = ('count', )
+
+    def __init__(self,
+                 count=None,
+                 r=(),
+                 extLst=None,
+                ):
+        self.r = r
+        self.extLst = extLst
+
+
+    @property
+    def count(self):
+        return len(self.r)
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
+
+
+    def _write(self, archive, manifest):
+        """
+        Write to zipfile and update manifest
+        """
+        xml = tostring(self.to_tree())
+        archive.writestr(self.path[1:], xml)
+        manifest.append(self)
+
+
+    def _write_rels(self, archive, manifest):
+        pass
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/pivot/table.py b/.venv/lib/python3.12/site-packages/openpyxl/pivot/table.py
new file mode 100644
index 00000000..cc3548b1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/pivot/table.py
@@ -0,0 +1,1261 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from collections import defaultdict
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    NoneSet,
+    Set,
+    Bool,
+    String,
+    Bool,
+    Sequence,
+)
+
+from openpyxl.descriptors.excel import ExtensionList, Relation
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.xml.constants import SHEET_MAIN_NS
+from openpyxl.xml.functions import tostring
+from openpyxl.packaging.relationship import (
+    RelationshipList,
+    Relationship,
+    get_rels_path
+)
+from .fields import Index
+
+from openpyxl.worksheet.filters import (
+    AutoFilter,
+)
+
+
+class HierarchyUsage(Serialisable):
+
+    tagname = "hierarchyUsage"
+
+    hierarchyUsage = Integer()
+
+    def __init__(self,
+                 hierarchyUsage=None,
+                ):
+        self.hierarchyUsage = hierarchyUsage
+
+
+class ColHierarchiesUsage(Serialisable):
+
+    tagname = "colHierarchiesUsage"
+
+    colHierarchyUsage = Sequence(expected_type=HierarchyUsage, )
+
+    __elements__ = ('colHierarchyUsage',)
+    __attrs__ = ('count', )
+
+    def __init__(self,
+                 count=None,
+                 colHierarchyUsage=(),
+                ):
+        self.colHierarchyUsage = colHierarchyUsage
+
+
+    @property
+    def count(self):
+        return len(self.colHierarchyUsage)
+
+
+class RowHierarchiesUsage(Serialisable):
+
+    tagname = "rowHierarchiesUsage"
+
+    rowHierarchyUsage = Sequence(expected_type=HierarchyUsage, )
+
+    __elements__ = ('rowHierarchyUsage',)
+    __attrs__ = ('count', )
+
+    def __init__(self,
+                 count=None,
+                 rowHierarchyUsage=(),
+                ):
+        self.rowHierarchyUsage = rowHierarchyUsage
+
+    @property
+    def count(self):
+        return len(self.rowHierarchyUsage)
+
+
+class PivotFilter(Serialisable):
+
+    tagname = "filter"
+
+    fld = Integer()
+    mpFld = Integer(allow_none=True)
+    type = Set(values=(['unknown', 'count', 'percent', 'sum', 'captionEqual',
+                        'captionNotEqual', 'captionBeginsWith', 'captionNotBeginsWith',
+                        'captionEndsWith', 'captionNotEndsWith', 'captionContains',
+                        'captionNotContains', 'captionGreaterThan', 'captionGreaterThanOrEqual',
+                        'captionLessThan', 'captionLessThanOrEqual', 'captionBetween',
+                        'captionNotBetween', 'valueEqual', 'valueNotEqual', 'valueGreaterThan',
+                        'valueGreaterThanOrEqual', 'valueLessThan', 'valueLessThanOrEqual',
+                        'valueBetween', 'valueNotBetween', 'dateEqual', 'dateNotEqual',
+                        'dateOlderThan', 'dateOlderThanOrEqual', 'dateNewerThan',
+                        'dateNewerThanOrEqual', 'dateBetween', 'dateNotBetween', 'tomorrow',
+                        'today', 'yesterday', 'nextWeek', 'thisWeek', 'lastWeek', 'nextMonth',
+                        'thisMonth', 'lastMonth', 'nextQuarter', 'thisQuarter', 'lastQuarter',
+                        'nextYear', 'thisYear', 'lastYear', 'yearToDate', 'Q1', 'Q2', 'Q3', 'Q4',
+                        'M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10', 'M11',
+                        'M12']))
+    evalOrder = Integer(allow_none=True)
+    id = Integer()
+    iMeasureHier = Integer(allow_none=True)
+    iMeasureFld = Integer(allow_none=True)
+    name = String(allow_none=True)
+    description = String(allow_none=True)
+    stringValue1 = String(allow_none=True)
+    stringValue2 = String(allow_none=True)
+    autoFilter = Typed(expected_type=AutoFilter, )
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('autoFilter',)
+
+    def __init__(self,
+                 fld=None,
+                 mpFld=None,
+                 type=None,
+                 evalOrder=None,
+                 id=None,
+                 iMeasureHier=None,
+                 iMeasureFld=None,
+                 name=None,
+                 description=None,
+                 stringValue1=None,
+                 stringValue2=None,
+                 autoFilter=None,
+                 extLst=None,
+                ):
+        self.fld = fld
+        self.mpFld = mpFld
+        self.type = type
+        self.evalOrder = evalOrder
+        self.id = id
+        self.iMeasureHier = iMeasureHier
+        self.iMeasureFld = iMeasureFld
+        self.name = name
+        self.description = description
+        self.stringValue1 = stringValue1
+        self.stringValue2 = stringValue2
+        self.autoFilter = autoFilter
+
+
+class PivotFilters(Serialisable):
+
+    count = Integer()
+    filter = Typed(expected_type=PivotFilter, allow_none=True)
+
+    __elements__ = ('filter',)
+
+    def __init__(self,
+                 count=None,
+                 filter=None,
+                ):
+        self.filter = filter
+
+
+class PivotTableStyle(Serialisable):
+
+    tagname = "pivotTableStyleInfo"
+
+    name = String(allow_none=True)
+    showRowHeaders = Bool()
+    showColHeaders = Bool()
+    showRowStripes = Bool()
+    showColStripes = Bool()
+    showLastColumn = Bool()
+
+    def __init__(self,
+                 name=None,
+                 showRowHeaders=None,
+                 showColHeaders=None,
+                 showRowStripes=None,
+                 showColStripes=None,
+                 showLastColumn=None,
+                ):
+        self.name = name
+        self.showRowHeaders = showRowHeaders
+        self.showColHeaders = showColHeaders
+        self.showRowStripes = showRowStripes
+        self.showColStripes = showColStripes
+        self.showLastColumn = showLastColumn
+
+
+class MemberList(Serialisable):
+
+    tagname = "members"
+
+    level = Integer(allow_none=True)
+    member = NestedSequence(expected_type=String, attribute="name")
+
+    __elements__ = ('member',)
+
+    def __init__(self,
+                 count=None,
+                 level=None,
+                 member=(),
+                ):
+        self.level = level
+        self.member = member
+
+    @property
+    def count(self):
+        return len(self.member)
+
+
+class MemberProperty(Serialisable):
+
+    tagname = "mps"
+
+    name = String(allow_none=True)
+    showCell = Bool(allow_none=True)
+    showTip = Bool(allow_none=True)
+    showAsCaption = Bool(allow_none=True)
+    nameLen = Integer(allow_none=True)
+    pPos = Integer(allow_none=True)
+    pLen = Integer(allow_none=True)
+    level = Integer(allow_none=True)
+    field = Integer()
+
+    def __init__(self,
+                 name=None,
+                 showCell=None,
+                 showTip=None,
+                 showAsCaption=None,
+                 nameLen=None,
+                 pPos=None,
+                 pLen=None,
+                 level=None,
+                 field=None,
+                ):
+        self.name = name
+        self.showCell = showCell
+        self.showTip = showTip
+        self.showAsCaption = showAsCaption
+        self.nameLen = nameLen
+        self.pPos = pPos
+        self.pLen = pLen
+        self.level = level
+        self.field = field
+
+
+class PivotHierarchy(Serialisable):
+
+    tagname = "pivotHierarchy"
+
+    outline = Bool()
+    multipleItemSelectionAllowed = Bool()
+    subtotalTop = Bool()
+    showInFieldList = Bool()
+    dragToRow = Bool()
+    dragToCol = Bool()
+    dragToPage = Bool()
+    dragToData = Bool()
+    dragOff = Bool()
+    includeNewItemsInFilter = Bool()
+    caption = String(allow_none=True)
+    mps = NestedSequence(expected_type=MemberProperty, count=True)
+    members = Typed(expected_type=MemberList, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('mps', 'members',)
+
+    def __init__(self,
+                 outline=None,
+                 multipleItemSelectionAllowed=None,
+                 subtotalTop=None,
+                 showInFieldList=None,
+                 dragToRow=None,
+                 dragToCol=None,
+                 dragToPage=None,
+                 dragToData=None,
+                 dragOff=None,
+                 includeNewItemsInFilter=None,
+                 caption=None,
+                 mps=(),
+                 members=None,
+                 extLst=None,
+                ):
+        self.outline = outline
+        self.multipleItemSelectionAllowed = multipleItemSelectionAllowed
+        self.subtotalTop = subtotalTop
+        self.showInFieldList = showInFieldList
+        self.dragToRow = dragToRow
+        self.dragToCol = dragToCol
+        self.dragToPage = dragToPage
+        self.dragToData = dragToData
+        self.dragOff = dragOff
+        self.includeNewItemsInFilter = includeNewItemsInFilter
+        self.caption = caption
+        self.mps = mps
+        self.members = members
+        self.extLst = extLst
+
+
+class Reference(Serialisable):
+
+    tagname = "reference"
+
+    field = Integer(allow_none=True)
+    selected = Bool(allow_none=True)
+    byPosition = Bool(allow_none=True)
+    relative = Bool(allow_none=True)
+    defaultSubtotal = Bool(allow_none=True)
+    sumSubtotal = Bool(allow_none=True)
+    countASubtotal = Bool(allow_none=True)
+    avgSubtotal = Bool(allow_none=True)
+    maxSubtotal = Bool(allow_none=True)
+    minSubtotal = Bool(allow_none=True)
+    productSubtotal = Bool(allow_none=True)
+    countSubtotal = Bool(allow_none=True)
+    stdDevSubtotal = Bool(allow_none=True)
+    stdDevPSubtotal = Bool(allow_none=True)
+    varSubtotal = Bool(allow_none=True)
+    varPSubtotal = Bool(allow_none=True)
+    x = Sequence(expected_type=Index)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('x',)
+
+    def __init__(self,
+                 field=None,
+                 count=None,
+                 selected=None,
+                 byPosition=None,
+                 relative=None,
+                 defaultSubtotal=None,
+                 sumSubtotal=None,
+                 countASubtotal=None,
+                 avgSubtotal=None,
+                 maxSubtotal=None,
+                 minSubtotal=None,
+                 productSubtotal=None,
+                 countSubtotal=None,
+                 stdDevSubtotal=None,
+                 stdDevPSubtotal=None,
+                 varSubtotal=None,
+                 varPSubtotal=None,
+                 x=(),
+                 extLst=None,
+                ):
+        self.field = field
+        self.selected = selected
+        self.byPosition = byPosition
+        self.relative = relative
+        self.defaultSubtotal = defaultSubtotal
+        self.sumSubtotal = sumSubtotal
+        self.countASubtotal = countASubtotal
+        self.avgSubtotal = avgSubtotal
+        self.maxSubtotal = maxSubtotal
+        self.minSubtotal = minSubtotal
+        self.productSubtotal = productSubtotal
+        self.countSubtotal = countSubtotal
+        self.stdDevSubtotal = stdDevSubtotal
+        self.stdDevPSubtotal = stdDevPSubtotal
+        self.varSubtotal = varSubtotal
+        self.varPSubtotal = varPSubtotal
+        self.x = x
+
+
+    @property
+    def count(self):
+        return len(self.field)
+
+
+class PivotArea(Serialisable):
+
+    tagname = "pivotArea"
+
+    references = NestedSequence(expected_type=Reference, count=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    field = Integer(allow_none=True)
+    type = NoneSet(values=(['normal', 'data', 'all', 'origin', 'button',
+                            'topEnd', 'topRight']))
+    dataOnly = Bool(allow_none=True)
+    labelOnly = Bool(allow_none=True)
+    grandRow = Bool(allow_none=True)
+    grandCol = Bool(allow_none=True)
+    cacheIndex = Bool(allow_none=True)
+    outline = Bool(allow_none=True)
+    offset = String(allow_none=True)
+    collapsedLevelsAreSubtotals = Bool(allow_none=True)
+    axis = NoneSet(values=(['axisRow', 'axisCol', 'axisPage', 'axisValues']))
+    fieldPosition = Integer(allow_none=True)
+
+    __elements__ = ('references',)
+
+    def __init__(self,
+                 references=(),
+                 extLst=None,
+                 field=None,
+                 type="normal",
+                 dataOnly=True,
+                 labelOnly=None,
+                 grandRow=None,
+                 grandCol=None,
+                 cacheIndex=None,
+                 outline=True,
+                 offset=None,
+                 collapsedLevelsAreSubtotals=None,
+                 axis=None,
+                 fieldPosition=None,
+                ):
+        self.references = references
+        self.extLst = extLst
+        self.field = field
+        self.type = type
+        self.dataOnly = dataOnly
+        self.labelOnly = labelOnly
+        self.grandRow = grandRow
+        self.grandCol = grandCol
+        self.cacheIndex = cacheIndex
+        self.outline = outline
+        self.offset = offset
+        self.collapsedLevelsAreSubtotals = collapsedLevelsAreSubtotals
+        self.axis = axis
+        self.fieldPosition = fieldPosition
+
+
+class ChartFormat(Serialisable):
+
+    tagname = "chartFormat"
+
+    chart = Integer()
+    format = Integer()
+    series = Bool()
+    pivotArea = Typed(expected_type=PivotArea, )
+
+    __elements__ = ('pivotArea',)
+
+    def __init__(self,
+                 chart=None,
+                 format=None,
+                 series=None,
+                 pivotArea=None,
+                ):
+        self.chart = chart
+        self.format = format
+        self.series = series
+        self.pivotArea = pivotArea
+
+
+class ConditionalFormat(Serialisable):
+
+    tagname = "conditionalFormat"
+
+    scope = Set(values=(['selection', 'data', 'field']))
+    type = NoneSet(values=(['all', 'row', 'column']))
+    priority = Integer()
+    pivotAreas = NestedSequence(expected_type=PivotArea)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('pivotAreas',)
+
+    def __init__(self,
+                 scope="selection",
+                 type=None,
+                 priority=None,
+                 pivotAreas=(),
+                 extLst=None,
+                ):
+        self.scope = scope
+        self.type = type
+        self.priority = priority
+        self.pivotAreas = pivotAreas
+        self.extLst = extLst
+
+
+class ConditionalFormatList(Serialisable):
+
+    tagname = "conditionalFormats"
+
+    conditionalFormat = Sequence(expected_type=ConditionalFormat)
+
+    __attrs__ = ("count",)
+
+    def __init__(self, conditionalFormat=(), count=None):
+        self.conditionalFormat = conditionalFormat
+
+
+    def by_priority(self):
+        """
+        Return a dictionary of format objects keyed by (field id and format property).
+        This can be used to map the formats to field but also to dedupe to match
+        worksheet definitions which are grouped by cell range
+        """
+
+        fmts = {}
+        for fmt in self.conditionalFormat:
+            for area in fmt.pivotAreas:
+                for ref in area.references:
+                    for field in ref.x:
+                        key = (field.v, fmt.priority)
+                        fmts[key] = fmt
+
+        return fmts
+
+
+    def _dedupe(self):
+        """
+        Group formats by field index and priority.
+        Sorted to match sorting and grouping for corresponding worksheet formats
+
+        The implemtenters notes contain significant deviance from the OOXML
+        specification, in particular how conditional formats in tables relate to
+        those defined in corresponding worksheets and how to determine which
+        format applies to which fields.
+
+        There are some magical interdependencies:
+
+        * Every pivot table fmt must have a worksheet cxf with the same priority.
+
+        * In the reference part the field 4294967294 refers to a data field, the
+        spec says -2
+
+        * Data fields are referenced by the 0-index reference.x.v value
+
+        Things are made more complicated by the fact that field items behave
+        diffently if the parent is a reference or shared item: "In Office if the
+        parent is the reference element, then restrictions of this value are
+        defined by reference@field. If the parent is the tables element, then
+        this value specifies the index into the table tag position in @url."
+        Yeah, right!
+        """
+        fmts = self.by_priority()
+        # sort by priority in order, keeping the highest numerical priority, least when
+        # actually applied
+        # this is not documented but it's what Excel is happy with
+        fmts = {field:fmt for (field, priority), fmt in sorted(fmts.items(), reverse=True)}
+        #fmts = {field:fmt for (field, priority), fmt in fmts.items()}
+        if fmts:
+            self.conditionalFormat = list(fmts.values())
+
+
+    @property
+    def count(self):
+        return len(self.conditionalFormat)
+
+
+    def to_tree(self, tagname=None):
+        self._dedupe()
+        return super().to_tree(tagname)
+
+
+class Format(Serialisable):
+
+    tagname = "format"
+
+    action = NoneSet(values=(['blank', 'formatting', 'drill', 'formula']))
+    dxfId = Integer(allow_none=True)
+    pivotArea = Typed(expected_type=PivotArea, )
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('pivotArea',)
+
+    def __init__(self,
+                 action="formatting",
+                 dxfId=None,
+                 pivotArea=None,
+                 extLst=None,
+                ):
+        self.action = action
+        self.dxfId = dxfId
+        self.pivotArea = pivotArea
+        self.extLst = extLst
+
+
+class DataField(Serialisable):
+
+    tagname = "dataField"
+
+    name = String(allow_none=True)
+    fld = Integer()
+    subtotal = Set(values=(['average', 'count', 'countNums', 'max', 'min',
+                            'product', 'stdDev', 'stdDevp', 'sum', 'var', 'varp']))
+    showDataAs = Set(values=(['normal', 'difference', 'percent',
+                              'percentDiff', 'runTotal', 'percentOfRow', 'percentOfCol',
+                              'percentOfTotal', 'index']))
+    baseField = Integer()
+    baseItem = Integer()
+    numFmtId = Integer(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+
+    def __init__(self,
+                 name=None,
+                 fld=None,
+                 subtotal="sum",
+                 showDataAs="normal",
+                 baseField=-1,
+                 baseItem=1048832,
+                 numFmtId=None,
+                 extLst=None,
+                ):
+        self.name = name
+        self.fld = fld
+        self.subtotal = subtotal
+        self.showDataAs = showDataAs
+        self.baseField = baseField
+        self.baseItem = baseItem
+        self.numFmtId = numFmtId
+        self.extLst = extLst
+
+
+class PageField(Serialisable):
+
+    tagname = "pageField"
+
+    fld = Integer()
+    item = Integer(allow_none=True)
+    hier = Integer(allow_none=True)
+    name = String(allow_none=True)
+    cap = String(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 fld=None,
+                 item=None,
+                 hier=None,
+                 name=None,
+                 cap=None,
+                 extLst=None,
+                ):
+        self.fld = fld
+        self.item = item
+        self.hier = hier
+        self.name = name
+        self.cap = cap
+        self.extLst = extLst
+
+
+class RowColItem(Serialisable):
+
+    tagname = "i"
+
+    t = Set(values=(['data', 'default', 'sum', 'countA', 'avg', 'max', 'min',
+                     'product', 'count', 'stdDev', 'stdDevP', 'var', 'varP', 'grand',
+                     'blank']))
+    r = Integer()
+    i = Integer()
+    x = Sequence(expected_type=Index, attribute="v")
+
+    __elements__ = ('x',)
+
+    def __init__(self,
+                 t="data",
+                 r=0,
+                 i=0,
+                 x=(),
+                ):
+        self.t = t
+        self.r = r
+        self.i = i
+        self.x = x
+
+
+class RowColField(Serialisable):
+
+    tagname = "field"
+
+    x = Integer()
+
+    def __init__(self,
+                 x=None,
+                ):
+        self.x = x
+
+
+class AutoSortScope(Serialisable):
+
+    pivotArea = Typed(expected_type=PivotArea, )
+
+    __elements__ = ('pivotArea',)
+
+    def __init__(self,
+                 pivotArea=None,
+                ):
+        self.pivotArea = pivotArea
+
+
+class FieldItem(Serialisable):
+
+    tagname = "item"
+
+    n = String(allow_none=True)
+    t = Set(values=(['data', 'default', 'sum', 'countA', 'avg', 'max', 'min',
+                     'product', 'count', 'stdDev', 'stdDevP', 'var', 'varP', 'grand',
+                     'blank']))
+    h = Bool(allow_none=True)
+    s = Bool(allow_none=True)
+    sd = Bool(allow_none=True)
+    f = Bool(allow_none=True)
+    m = Bool(allow_none=True)
+    c = Bool(allow_none=True)
+    x = Integer(allow_none=True)
+    d = Bool(allow_none=True)
+    e = Bool(allow_none=True)
+
+    def __init__(self,
+                 n=None,
+                 t="data",
+                 h=None,
+                 s=None,
+                 sd=True,
+                 f=None,
+                 m=None,
+                 c=None,
+                 x=None,
+                 d=None,
+                 e=None,
+                ):
+        self.n = n
+        self.t = t
+        self.h = h
+        self.s = s
+        self.sd = sd
+        self.f = f
+        self.m = m
+        self.c = c
+        self.x = x
+        self.d = d
+        self.e = e
+
+
+class PivotField(Serialisable):
+
+    tagname = "pivotField"
+
+    items = NestedSequence(expected_type=FieldItem, count=True)
+    autoSortScope = Typed(expected_type=AutoSortScope, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    name = String(allow_none=True)
+    axis = NoneSet(values=(['axisRow', 'axisCol', 'axisPage', 'axisValues']))
+    dataField = Bool(allow_none=True)
+    subtotalCaption = String(allow_none=True)
+    showDropDowns = Bool(allow_none=True)
+    hiddenLevel = Bool(allow_none=True)
+    uniqueMemberProperty = String(allow_none=True)
+    compact = Bool(allow_none=True)
+    allDrilled = Bool(allow_none=True)
+    numFmtId = Integer(allow_none=True)
+    outline = Bool(allow_none=True)
+    subtotalTop = Bool(allow_none=True)
+    dragToRow = Bool(allow_none=True)
+    dragToCol = Bool(allow_none=True)
+    multipleItemSelectionAllowed = Bool(allow_none=True)
+    dragToPage = Bool(allow_none=True)
+    dragToData = Bool(allow_none=True)
+    dragOff = Bool(allow_none=True)
+    showAll = Bool(allow_none=True)
+    insertBlankRow = Bool(allow_none=True)
+    serverField = Bool(allow_none=True)
+    insertPageBreak = Bool(allow_none=True)
+    autoShow = Bool(allow_none=True)
+    topAutoShow = Bool(allow_none=True)
+    hideNewItems = Bool(allow_none=True)
+    measureFilter = Bool(allow_none=True)
+    includeNewItemsInFilter = Bool(allow_none=True)
+    itemPageCount = Integer(allow_none=True)
+    sortType = Set(values=(['manual', 'ascending', 'descending']))
+    dataSourceSort = Bool(allow_none=True)
+    nonAutoSortDefault = Bool(allow_none=True)
+    rankBy = Integer(allow_none=True)
+    defaultSubtotal = Bool(allow_none=True)
+    sumSubtotal = Bool(allow_none=True)
+    countASubtotal = Bool(allow_none=True)
+    avgSubtotal = Bool(allow_none=True)
+    maxSubtotal = Bool(allow_none=True)
+    minSubtotal = Bool(allow_none=True)
+    productSubtotal = Bool(allow_none=True)
+    countSubtotal = Bool(allow_none=True)
+    stdDevSubtotal = Bool(allow_none=True)
+    stdDevPSubtotal = Bool(allow_none=True)
+    varSubtotal = Bool(allow_none=True)
+    varPSubtotal = Bool(allow_none=True)
+    showPropCell = Bool(allow_none=True)
+    showPropTip = Bool(allow_none=True)
+    showPropAsCaption = Bool(allow_none=True)
+    defaultAttributeDrillState = Bool(allow_none=True)
+
+    __elements__ = ('items', 'autoSortScope',)
+
+    def __init__(self,
+                 items=(),
+                 autoSortScope=None,
+                 name=None,
+                 axis=None,
+                 dataField=None,
+                 subtotalCaption=None,
+                 showDropDowns=True,
+                 hiddenLevel=None,
+                 uniqueMemberProperty=None,
+                 compact=True,
+                 allDrilled=None,
+                 numFmtId=None,
+                 outline=True,
+                 subtotalTop=True,
+                 dragToRow=True,
+                 dragToCol=True,
+                 multipleItemSelectionAllowed=None,
+                 dragToPage=True,
+                 dragToData=True,
+                 dragOff=True,
+                 showAll=True,
+                 insertBlankRow=None,
+                 serverField=None,
+                 insertPageBreak=None,
+                 autoShow=None,
+                 topAutoShow=True,
+                 hideNewItems=None,
+                 measureFilter=None,
+                 includeNewItemsInFilter=None,
+                 itemPageCount=10,
+                 sortType="manual",
+                 dataSourceSort=None,
+                 nonAutoSortDefault=None,
+                 rankBy=None,
+                 defaultSubtotal=True,
+                 sumSubtotal=None,
+                 countASubtotal=None,
+                 avgSubtotal=None,
+                 maxSubtotal=None,
+                 minSubtotal=None,
+                 productSubtotal=None,
+                 countSubtotal=None,
+                 stdDevSubtotal=None,
+                 stdDevPSubtotal=None,
+                 varSubtotal=None,
+                 varPSubtotal=None,
+                 showPropCell=None,
+                 showPropTip=None,
+                 showPropAsCaption=None,
+                 defaultAttributeDrillState=None,
+                 extLst=None,
+                ):
+        self.items = items
+        self.autoSortScope = autoSortScope
+        self.name = name
+        self.axis = axis
+        self.dataField = dataField
+        self.subtotalCaption = subtotalCaption
+        self.showDropDowns = showDropDowns
+        self.hiddenLevel = hiddenLevel
+        self.uniqueMemberProperty = uniqueMemberProperty
+        self.compact = compact
+        self.allDrilled = allDrilled
+        self.numFmtId = numFmtId
+        self.outline = outline
+        self.subtotalTop = subtotalTop
+        self.dragToRow = dragToRow
+        self.dragToCol = dragToCol
+        self.multipleItemSelectionAllowed = multipleItemSelectionAllowed
+        self.dragToPage = dragToPage
+        self.dragToData = dragToData
+        self.dragOff = dragOff
+        self.showAll = showAll
+        self.insertBlankRow = insertBlankRow
+        self.serverField = serverField
+        self.insertPageBreak = insertPageBreak
+        self.autoShow = autoShow
+        self.topAutoShow = topAutoShow
+        self.hideNewItems = hideNewItems
+        self.measureFilter = measureFilter
+        self.includeNewItemsInFilter = includeNewItemsInFilter
+        self.itemPageCount = itemPageCount
+        self.sortType = sortType
+        self.dataSourceSort = dataSourceSort
+        self.nonAutoSortDefault = nonAutoSortDefault
+        self.rankBy = rankBy
+        self.defaultSubtotal = defaultSubtotal
+        self.sumSubtotal = sumSubtotal
+        self.countASubtotal = countASubtotal
+        self.avgSubtotal = avgSubtotal
+        self.maxSubtotal = maxSubtotal
+        self.minSubtotal = minSubtotal
+        self.productSubtotal = productSubtotal
+        self.countSubtotal = countSubtotal
+        self.stdDevSubtotal = stdDevSubtotal
+        self.stdDevPSubtotal = stdDevPSubtotal
+        self.varSubtotal = varSubtotal
+        self.varPSubtotal = varPSubtotal
+        self.showPropCell = showPropCell
+        self.showPropTip = showPropTip
+        self.showPropAsCaption = showPropAsCaption
+        self.defaultAttributeDrillState = defaultAttributeDrillState
+
+
+class Location(Serialisable):
+
+    tagname = "location"
+
+    ref = String()
+    firstHeaderRow = Integer()
+    firstDataRow = Integer()
+    firstDataCol = Integer()
+    rowPageCount = Integer(allow_none=True)
+    colPageCount = Integer(allow_none=True)
+
+    def __init__(self,
+                 ref=None,
+                 firstHeaderRow=None,
+                 firstDataRow=None,
+                 firstDataCol=None,
+                 rowPageCount=None,
+                 colPageCount=None,
+                ):
+        self.ref = ref
+        self.firstHeaderRow = firstHeaderRow
+        self.firstDataRow = firstDataRow
+        self.firstDataCol = firstDataCol
+        self.rowPageCount = rowPageCount
+        self.colPageCount = colPageCount
+
+
+class TableDefinition(Serialisable):
+
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
+    rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
+    _id = 1
+    _path = "/xl/pivotTables/pivotTable{0}.xml"
+
+    tagname = "pivotTableDefinition"
+    cache = None
+
+    name = String()
+    cacheId = Integer()
+    dataOnRows = Bool()
+    dataPosition = Integer(allow_none=True)
+    dataCaption = String()
+    grandTotalCaption = String(allow_none=True)
+    errorCaption = String(allow_none=True)
+    showError = Bool()
+    missingCaption = String(allow_none=True)
+    showMissing = Bool()
+    pageStyle = String(allow_none=True)
+    pivotTableStyle = String(allow_none=True)
+    vacatedStyle = String(allow_none=True)
+    tag = String(allow_none=True)
+    updatedVersion = Integer()
+    minRefreshableVersion = Integer()
+    asteriskTotals = Bool()
+    showItems = Bool()
+    editData = Bool()
+    disableFieldList = Bool()
+    showCalcMbrs = Bool()
+    visualTotals = Bool()
+    showMultipleLabel = Bool()
+    showDataDropDown = Bool()
+    showDrill = Bool()
+    printDrill = Bool()
+    showMemberPropertyTips = Bool()
+    showDataTips = Bool()
+    enableWizard = Bool()
+    enableDrill = Bool()
+    enableFieldProperties = Bool()
+    preserveFormatting = Bool()
+    useAutoFormatting = Bool()
+    pageWrap = Integer()
+    pageOverThenDown = Bool()
+    subtotalHiddenItems = Bool()
+    rowGrandTotals = Bool()
+    colGrandTotals = Bool()
+    fieldPrintTitles = Bool()
+    itemPrintTitles = Bool()
+    mergeItem = Bool()
+    showDropZones = Bool()
+    createdVersion = Integer()
+    indent = Integer()
+    showEmptyRow = Bool()
+    showEmptyCol = Bool()
+    showHeaders = Bool()
+    compact = Bool()
+    outline = Bool()
+    outlineData = Bool()
+    compactData = Bool()
+    published = Bool()
+    gridDropZones = Bool()
+    immersive = Bool()
+    multipleFieldFilters = Bool()
+    chartFormat = Integer()
+    rowHeaderCaption = String(allow_none=True)
+    colHeaderCaption = String(allow_none=True)
+    fieldListSortAscending = Bool()
+    mdxSubqueries = Bool()
+    customListSort = Bool(allow_none=True)
+    autoFormatId = Integer(allow_none=True)
+    applyNumberFormats = Bool()
+    applyBorderFormats = Bool()
+    applyFontFormats = Bool()
+    applyPatternFormats = Bool()
+    applyAlignmentFormats = Bool()
+    applyWidthHeightFormats = Bool()
+    location = Typed(expected_type=Location, )
+    pivotFields = NestedSequence(expected_type=PivotField, count=True)
+    rowFields = NestedSequence(expected_type=RowColField, count=True)
+    rowItems = NestedSequence(expected_type=RowColItem, count=True)
+    colFields = NestedSequence(expected_type=RowColField, count=True)
+    colItems = NestedSequence(expected_type=RowColItem, count=True)
+    pageFields = NestedSequence(expected_type=PageField, count=True)
+    dataFields = NestedSequence(expected_type=DataField, count=True)
+    formats = NestedSequence(expected_type=Format, count=True)
+    conditionalFormats = Typed(expected_type=ConditionalFormatList, allow_none=True)
+    chartFormats = NestedSequence(expected_type=ChartFormat, count=True)
+    pivotHierarchies = NestedSequence(expected_type=PivotHierarchy, count=True)
+    pivotTableStyleInfo = Typed(expected_type=PivotTableStyle, allow_none=True)
+    filters = NestedSequence(expected_type=PivotFilter, count=True)
+    rowHierarchiesUsage = Typed(expected_type=RowHierarchiesUsage, allow_none=True)
+    colHierarchiesUsage = Typed(expected_type=ColHierarchiesUsage, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+    id = Relation()
+
+    __elements__ = ('location', 'pivotFields', 'rowFields', 'rowItems',
+                    'colFields', 'colItems', 'pageFields', 'dataFields', 'formats',
+                    'conditionalFormats', 'chartFormats', 'pivotHierarchies',
+                    'pivotTableStyleInfo', 'filters', 'rowHierarchiesUsage',
+                    'colHierarchiesUsage',)
+
+    def __init__(self,
+                 name=None,
+                 cacheId=None,
+                 dataOnRows=False,
+                 dataPosition=None,
+                 dataCaption=None,
+                 grandTotalCaption=None,
+                 errorCaption=None,
+                 showError=False,
+                 missingCaption=None,
+                 showMissing=True,
+                 pageStyle=None,
+                 pivotTableStyle=None,
+                 vacatedStyle=None,
+                 tag=None,
+                 updatedVersion=0,
+                 minRefreshableVersion=0,
+                 asteriskTotals=False,
+                 showItems=True,
+                 editData=False,
+                 disableFieldList=False,
+                 showCalcMbrs=True,
+                 visualTotals=True,
+                 showMultipleLabel=True,
+                 showDataDropDown=True,
+                 showDrill=True,
+                 printDrill=False,
+                 showMemberPropertyTips=True,
+                 showDataTips=True,
+                 enableWizard=True,
+                 enableDrill=True,
+                 enableFieldProperties=True,
+                 preserveFormatting=True,
+                 useAutoFormatting=False,
+                 pageWrap=0,
+                 pageOverThenDown=False,
+                 subtotalHiddenItems=False,
+                 rowGrandTotals=True,
+                 colGrandTotals=True,
+                 fieldPrintTitles=False,
+                 itemPrintTitles=False,
+                 mergeItem=False,
+                 showDropZones=True,
+                 createdVersion=0,
+                 indent=1,
+                 showEmptyRow=False,
+                 showEmptyCol=False,
+                 showHeaders=True,
+                 compact=True,
+                 outline=False,
+                 outlineData=False,
+                 compactData=True,
+                 published=False,
+                 gridDropZones=False,
+                 immersive=True,
+                 multipleFieldFilters=None,
+                 chartFormat=0,
+                 rowHeaderCaption=None,
+                 colHeaderCaption=None,
+                 fieldListSortAscending=None,
+                 mdxSubqueries=None,
+                 customListSort=None,
+                 autoFormatId=None,
+                 applyNumberFormats=False,
+                 applyBorderFormats=False,
+                 applyFontFormats=False,
+                 applyPatternFormats=False,
+                 applyAlignmentFormats=False,
+                 applyWidthHeightFormats=False,
+                 location=None,
+                 pivotFields=(),
+                 rowFields=(),
+                 rowItems=(),
+                 colFields=(),
+                 colItems=(),
+                 pageFields=(),
+                 dataFields=(),
+                 formats=(),
+                 conditionalFormats=None,
+                 chartFormats=(),
+                 pivotHierarchies=(),
+                 pivotTableStyleInfo=None,
+                 filters=(),
+                 rowHierarchiesUsage=None,
+                 colHierarchiesUsage=None,
+                 extLst=None,
+                 id=None,
+                ):
+        self.name = name
+        self.cacheId = cacheId
+        self.dataOnRows = dataOnRows
+        self.dataPosition = dataPosition
+        self.dataCaption = dataCaption
+        self.grandTotalCaption = grandTotalCaption
+        self.errorCaption = errorCaption
+        self.showError = showError
+        self.missingCaption = missingCaption
+        self.showMissing = showMissing
+        self.pageStyle = pageStyle
+        self.pivotTableStyle = pivotTableStyle
+        self.vacatedStyle = vacatedStyle
+        self.tag = tag
+        self.updatedVersion = updatedVersion
+        self.minRefreshableVersion = minRefreshableVersion
+        self.asteriskTotals = asteriskTotals
+        self.showItems = showItems
+        self.editData = editData
+        self.disableFieldList = disableFieldList
+        self.showCalcMbrs = showCalcMbrs
+        self.visualTotals = visualTotals
+        self.showMultipleLabel = showMultipleLabel
+        self.showDataDropDown = showDataDropDown
+        self.showDrill = showDrill
+        self.printDrill = printDrill
+        self.showMemberPropertyTips = showMemberPropertyTips
+        self.showDataTips = showDataTips
+        self.enableWizard = enableWizard
+        self.enableDrill = enableDrill
+        self.enableFieldProperties = enableFieldProperties
+        self.preserveFormatting = preserveFormatting
+        self.useAutoFormatting = useAutoFormatting
+        self.pageWrap = pageWrap
+        self.pageOverThenDown = pageOverThenDown
+        self.subtotalHiddenItems = subtotalHiddenItems
+        self.rowGrandTotals = rowGrandTotals
+        self.colGrandTotals = colGrandTotals
+        self.fieldPrintTitles = fieldPrintTitles
+        self.itemPrintTitles = itemPrintTitles
+        self.mergeItem = mergeItem
+        self.showDropZones = showDropZones
+        self.createdVersion = createdVersion
+        self.indent = indent
+        self.showEmptyRow = showEmptyRow
+        self.showEmptyCol = showEmptyCol
+        self.showHeaders = showHeaders
+        self.compact = compact
+        self.outline = outline
+        self.outlineData = outlineData
+        self.compactData = compactData
+        self.published = published
+        self.gridDropZones = gridDropZones
+        self.immersive = immersive
+        self.multipleFieldFilters = multipleFieldFilters
+        self.chartFormat = chartFormat
+        self.rowHeaderCaption = rowHeaderCaption
+        self.colHeaderCaption = colHeaderCaption
+        self.fieldListSortAscending = fieldListSortAscending
+        self.mdxSubqueries = mdxSubqueries
+        self.customListSort = customListSort
+        self.autoFormatId = autoFormatId
+        self.applyNumberFormats = applyNumberFormats
+        self.applyBorderFormats = applyBorderFormats
+        self.applyFontFormats = applyFontFormats
+        self.applyPatternFormats = applyPatternFormats
+        self.applyAlignmentFormats = applyAlignmentFormats
+        self.applyWidthHeightFormats = applyWidthHeightFormats
+        self.location = location
+        self.pivotFields = pivotFields
+        self.rowFields = rowFields
+        self.rowItems = rowItems
+        self.colFields = colFields
+        self.colItems = colItems
+        self.pageFields = pageFields
+        self.dataFields = dataFields
+        self.formats = formats
+        self.conditionalFormats = conditionalFormats
+        self.conditionalFormats = None
+        self.chartFormats = chartFormats
+        self.pivotHierarchies = pivotHierarchies
+        self.pivotTableStyleInfo = pivotTableStyleInfo
+        self.filters = filters
+        self.rowHierarchiesUsage = rowHierarchiesUsage
+        self.colHierarchiesUsage = colHierarchiesUsage
+        self.extLst = extLst
+        self.id = id
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
+
+
+    def _write(self, archive, manifest):
+        """
+        Add to zipfile and update manifest
+        """
+        self._write_rels(archive, manifest)
+        xml = tostring(self.to_tree())
+        archive.writestr(self.path[1:], xml)
+        manifest.append(self)
+
+
+    def _write_rels(self, archive, manifest):
+        """
+        Write the relevant child objects and add links
+        """
+        if self.cache is None:
+            return
+
+        rels = RelationshipList()
+        r = Relationship(Type=self.cache.rel_type, Target=self.cache.path)
+        rels.append(r)
+        self.id = r.id
+        if self.cache.path[1:] not in archive.namelist():
+            self.cache._write(archive, manifest)
+
+        path = get_rels_path(self.path)
+        xml = tostring(rels.to_tree())
+        archive.writestr(path[1:], xml)
+
+
+    def formatted_fields(self):
+        """Map fields to associated conditional formats by priority"""
+        if not self.conditionalFormats:
+            return {}
+        fields = defaultdict(list)
+        for idx, prio in self.conditionalFormats.by_priority():
+            name = self.dataFields[idx].name
+            fields[name].append(prio)
+        return fields
+
+
+    @property
+    def summary(self):
+        """
+        Provide a simplified summary of the table
+        """
+
+        return f"{self.name} {dict(self.location)}"
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/reader/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/reader/__init__.py
new file mode 100644
index 00000000..ab6cdead
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/reader/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) 2010-2024 openpyxl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/reader/drawings.py b/.venv/lib/python3.12/site-packages/openpyxl/reader/drawings.py
new file mode 100644
index 00000000..caaa8570
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/reader/drawings.py
@@ -0,0 +1,71 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+
+from io import BytesIO
+from warnings import warn
+
+from openpyxl.xml.functions import fromstring
+from openpyxl.xml.constants import IMAGE_NS
+from openpyxl.packaging.relationship import (
+    get_rel,
+    get_rels_path,
+    get_dependents,
+)
+from openpyxl.drawing.spreadsheet_drawing import SpreadsheetDrawing
+from openpyxl.drawing.image import Image, PILImage
+from openpyxl.chart.chartspace import ChartSpace
+from openpyxl.chart.reader import read_chart
+
+
+def find_images(archive, path):
+    """
+    Given the path to a drawing file extract charts and images
+
+    Ignore errors due to unsupported parts of DrawingML
+    """
+
+    src = archive.read(path)
+    tree = fromstring(src)
+    try:
+        drawing = SpreadsheetDrawing.from_tree(tree)
+    except TypeError:
+        warn("DrawingML support is incomplete and limited to charts and images only. Shapes and drawings will be lost.")
+        return [], []
+
+    rels_path = get_rels_path(path)
+    deps = []
+    if rels_path in archive.namelist():
+        deps = get_dependents(archive, rels_path)
+
+    charts = []
+    for rel in drawing._chart_rels:
+        try:
+            cs = get_rel(archive, deps, rel.id, ChartSpace)
+        except TypeError as e:
+            warn(f"Unable to read chart {rel.id} from {path} {e}")
+            continue
+        chart = read_chart(cs)
+        chart.anchor = rel.anchor
+        charts.append(chart)
+
+    images = []
+    if not PILImage: # Pillow not installed, drop images
+        return charts, images
+
+    for rel in drawing._blip_rels:
+        dep = deps.get(rel.embed)
+        if dep.Type == IMAGE_NS:
+            try:
+                image = Image(BytesIO(archive.read(dep.target)))
+            except OSError:
+                msg = "The image {0} will be removed because it cannot be read".format(dep.target)
+                warn(msg)
+                continue
+            if image.format.upper() == "WMF": # cannot save
+                msg = "{0} image format is not supported so the image is being dropped".format(image.format)
+                warn(msg)
+                continue
+            image.anchor = rel.anchor
+            images.append(image)
+    return charts, images
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/reader/excel.py b/.venv/lib/python3.12/site-packages/openpyxl/reader/excel.py
new file mode 100644
index 00000000..dfd8eeac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/reader/excel.py
@@ -0,0 +1,349 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""Read an xlsx file into Python"""
+
+# Python stdlib imports
+from zipfile import ZipFile, ZIP_DEFLATED
+from io import BytesIO
+import os.path
+import warnings
+
+from openpyxl.pivot.table import TableDefinition
+
+# Allow blanket setting of KEEP_VBA for testing
+try:
+    from ..tests import KEEP_VBA
+except ImportError:
+    KEEP_VBA = False
+
+# package imports
+from openpyxl.utils.exceptions import InvalidFileException
+from openpyxl.xml.constants import (
+    ARC_CORE,
+    ARC_CUSTOM,
+    ARC_CONTENT_TYPES,
+    ARC_WORKBOOK,
+    ARC_THEME,
+    COMMENTS_NS,
+    SHARED_STRINGS,
+    XLTM,
+    XLTX,
+    XLSM,
+    XLSX,
+)
+from openpyxl.cell import MergedCell
+from openpyxl.comments.comment_sheet import CommentSheet
+
+from .strings import read_string_table, read_rich_text
+from .workbook import WorkbookParser
+from openpyxl.styles.stylesheet import apply_stylesheet
+
+from openpyxl.packaging.core import DocumentProperties
+from openpyxl.packaging.custom import CustomPropertyList
+from openpyxl.packaging.manifest import Manifest, Override
+
+from openpyxl.packaging.relationship import (
+    RelationshipList,
+    get_dependents,
+    get_rels_path,
+)
+
+from openpyxl.worksheet._read_only import ReadOnlyWorksheet
+from openpyxl.worksheet._reader import WorksheetReader
+from openpyxl.chartsheet import Chartsheet
+from openpyxl.worksheet.table import Table
+from openpyxl.drawing.spreadsheet_drawing import SpreadsheetDrawing
+
+from openpyxl.xml.functions import fromstring
+
+from .drawings import find_images
+
+
+SUPPORTED_FORMATS = ('.xlsx', '.xlsm', '.xltx', '.xltm')
+
+
+def _validate_archive(filename):
+    """
+    Does a first check whether filename is a string or a file-like
+    object. If it is a string representing a filename, a check is done
+    for supported formats by checking the given file-extension. If the
+    file-extension is not in SUPPORTED_FORMATS an InvalidFileException
+    will raised. Otherwise the filename (resp. file-like object) will
+    forwarded to zipfile.ZipFile returning a ZipFile-Instance.
+    """
+    is_file_like = hasattr(filename, 'read')
+    if not is_file_like:
+        file_format = os.path.splitext(filename)[-1].lower()
+        if file_format not in SUPPORTED_FORMATS:
+            if file_format == '.xls':
+                msg = ('openpyxl does not support the old .xls file format, '
+                       'please use xlrd to read this file, or convert it to '
+                       'the more recent .xlsx file format.')
+            elif file_format == '.xlsb':
+                msg = ('openpyxl does not support binary format .xlsb, '
+                       'please convert this file to .xlsx format if you want '
+                       'to open it with openpyxl')
+            else:
+                msg = ('openpyxl does not support %s file format, '
+                       'please check you can open '
+                       'it with Excel first. '
+                       'Supported formats are: %s') % (file_format,
+                                                       ','.join(SUPPORTED_FORMATS))
+            raise InvalidFileException(msg)
+
+    archive = ZipFile(filename, 'r')
+    return archive
+
+
+def _find_workbook_part(package):
+    workbook_types = [XLTM, XLTX, XLSM, XLSX]
+    for ct in workbook_types:
+        part = package.find(ct)
+        if part:
+            return part
+
+    # some applications reassign the default for application/xml
+    defaults = {p.ContentType for p in package.Default}
+    workbook_type = defaults & set(workbook_types)
+    if workbook_type:
+        return Override("/" + ARC_WORKBOOK, workbook_type.pop())
+
+    raise IOError("File contains no valid workbook part")
+
+
+class ExcelReader:
+
+    """
+    Read an Excel package and dispatch the contents to the relevant modules
+    """
+
+    def __init__(self, fn, read_only=False, keep_vba=KEEP_VBA,
+                 data_only=False, keep_links=True, rich_text=False):
+        self.archive = _validate_archive(fn)
+        self.valid_files = self.archive.namelist()
+        self.read_only = read_only
+        self.keep_vba = keep_vba
+        self.data_only = data_only
+        self.keep_links = keep_links
+        self.rich_text = rich_text
+        self.shared_strings = []
+
+
+    def read_manifest(self):
+        src = self.archive.read(ARC_CONTENT_TYPES)
+        root = fromstring(src)
+        self.package = Manifest.from_tree(root)
+
+
+    def read_strings(self):
+        ct = self.package.find(SHARED_STRINGS)
+        reader = read_string_table
+        if self.rich_text:
+            reader = read_rich_text
+        if ct is not None:
+            strings_path = ct.PartName[1:]
+            with self.archive.open(strings_path,) as src:
+                self.shared_strings = reader(src)
+
+
+    def read_workbook(self):
+        wb_part = _find_workbook_part(self.package)
+        self.parser = WorkbookParser(self.archive, wb_part.PartName[1:], keep_links=self.keep_links)
+        self.parser.parse()
+        wb = self.parser.wb
+        wb._sheets = []
+        wb._data_only = self.data_only
+        wb._read_only = self.read_only
+        wb.template = wb_part.ContentType in (XLTX, XLTM)
+
+        # If are going to preserve the vba then attach a copy of the archive to the
+        # workbook so that is available for the save.
+        if self.keep_vba:
+            wb.vba_archive = ZipFile(BytesIO(), 'a', ZIP_DEFLATED)
+            for name in self.valid_files:
+                wb.vba_archive.writestr(name, self.archive.read(name))
+
+        if self.read_only:
+            wb._archive = self.archive
+
+        self.wb = wb
+
+
+    def read_properties(self):
+        if ARC_CORE in self.valid_files:
+            src = fromstring(self.archive.read(ARC_CORE))
+            self.wb.properties = DocumentProperties.from_tree(src)
+
+
+    def read_custom(self):
+        if ARC_CUSTOM in self.valid_files:
+            src = fromstring(self.archive.read(ARC_CUSTOM))
+            self.wb.custom_doc_props = CustomPropertyList.from_tree(src)
+
+
+    def read_theme(self):
+        if ARC_THEME in self.valid_files:
+            self.wb.loaded_theme = self.archive.read(ARC_THEME)
+
+
+    def read_chartsheet(self, sheet, rel):
+        sheet_path = rel.target
+        rels_path = get_rels_path(sheet_path)
+        rels = []
+        if rels_path in self.valid_files:
+            rels = get_dependents(self.archive, rels_path)
+
+        with self.archive.open(sheet_path, "r") as src:
+            xml = src.read()
+        node = fromstring(xml)
+        cs = Chartsheet.from_tree(node)
+        cs._parent = self.wb
+        cs.title = sheet.name
+        self.wb._add_sheet(cs)
+
+        drawings = rels.find(SpreadsheetDrawing._rel_type)
+        for rel in drawings:
+            charts, images = find_images(self.archive, rel.target)
+            for c in charts:
+                cs.add_chart(c)
+
+
+    def read_worksheets(self):
+        comment_warning = """Cell '{0}':{1} is part of a merged range but has a comment which will be removed because merged cells cannot contain any data."""
+        for sheet, rel in self.parser.find_sheets():
+            if rel.target not in self.valid_files:
+                continue
+
+            if "chartsheet" in rel.Type:
+                self.read_chartsheet(sheet, rel)
+                continue
+
+            rels_path = get_rels_path(rel.target)
+            rels = RelationshipList()
+            if rels_path in self.valid_files:
+                rels = get_dependents(self.archive, rels_path)
+
+            if self.read_only:
+                ws = ReadOnlyWorksheet(self.wb, sheet.name, rel.target, self.shared_strings)
+                ws.sheet_state = sheet.state
+                self.wb._sheets.append(ws)
+                continue
+            else:
+                fh = self.archive.open(rel.target)
+                ws = self.wb.create_sheet(sheet.name)
+                ws._rels = rels
+                ws_parser = WorksheetReader(ws, fh, self.shared_strings, self.data_only, self.rich_text)
+                ws_parser.bind_all()
+                fh.close()
+
+            # assign any comments to cells
+            for r in rels.find(COMMENTS_NS):
+                src = self.archive.read(r.target)
+                comment_sheet = CommentSheet.from_tree(fromstring(src))
+                for ref, comment in comment_sheet.comments:
+                    try:
+                        ws[ref].comment = comment
+                    except AttributeError:
+                        c = ws[ref]
+                        if isinstance(c, MergedCell):
+                            warnings.warn(comment_warning.format(ws.title, c.coordinate))
+                            continue
+
+            # preserve link to VML file if VBA
+            if self.wb.vba_archive and ws.legacy_drawing:
+                ws.legacy_drawing = rels.get(ws.legacy_drawing).target
+            else:
+                ws.legacy_drawing = None
+
+            for t in ws_parser.tables:
+                src = self.archive.read(t)
+                xml = fromstring(src)
+                table = Table.from_tree(xml)
+                ws.add_table(table)
+
+            drawings = rels.find(SpreadsheetDrawing._rel_type)
+            for rel in drawings:
+                charts, images = find_images(self.archive, rel.target)
+                for c in charts:
+                    ws.add_chart(c, c.anchor)
+                for im in images:
+                    ws.add_image(im, im.anchor)
+
+            pivot_rel = rels.find(TableDefinition.rel_type)
+            pivot_caches = self.parser.pivot_caches
+            for r in pivot_rel:
+                pivot_path = r.Target
+                src = self.archive.read(pivot_path)
+                tree = fromstring(src)
+                pivot = TableDefinition.from_tree(tree)
+                pivot.cache = pivot_caches[pivot.cacheId]
+                ws.add_pivot(pivot)
+
+            ws.sheet_state = sheet.state
+
+
+    def read(self):
+        action = "read manifest"
+        try:
+            self.read_manifest()
+            action = "read strings"
+            self.read_strings()
+            action = "read workbook"
+            self.read_workbook()
+            action = "read properties"
+            self.read_properties()
+            action = "read custom properties"
+            self.read_custom()
+            action = "read theme"
+            self.read_theme()
+            action = "read stylesheet"
+            apply_stylesheet(self.archive, self.wb)
+            action = "read worksheets"
+            self.read_worksheets()
+            action = "assign names"
+            self.parser.assign_names()
+            if not self.read_only:
+                self.archive.close()
+        except ValueError as e:
+            raise ValueError(
+                f"Unable to read workbook: could not {action} from {self.archive.filename}.\n"
+                "This is most probably because the workbook source files contain some invalid XML.\n"
+                "Please see the exception for more details."
+                ) from e
+
+
+def load_workbook(filename, read_only=False, keep_vba=KEEP_VBA,
+                  data_only=False, keep_links=True, rich_text=False):
+    """Open the given filename and return the workbook
+
+    :param filename: the path to open or a file-like object
+    :type filename: string or a file-like object open in binary mode c.f., :class:`zipfile.ZipFile`
+
+    :param read_only: optimised for reading, content cannot be edited
+    :type read_only: bool
+
+    :param keep_vba: preserve vba content (this does NOT mean you can use it)
+    :type keep_vba: bool
+
+    :param data_only: controls whether cells with formulae have either the formula (default) or the value stored the last time Excel read the sheet
+    :type data_only: bool
+
+    :param keep_links: whether links to external workbooks should be preserved. The default is True
+    :type keep_links: bool
+
+    :param rich_text: if set to True openpyxl will preserve any rich text formatting in cells. The default is False
+    :type rich_text: bool
+
+    :rtype: :class:`openpyxl.workbook.Workbook`
+
+    .. note::
+
+        When using lazy load, all worksheets will be :class:`openpyxl.worksheet.iter_worksheet.IterableWorksheet`
+        and the returned workbook will be read-only.
+
+    """
+    reader = ExcelReader(filename, read_only, keep_vba,
+                         data_only, keep_links, rich_text)
+    reader.read()
+    return reader.wb
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/reader/strings.py b/.venv/lib/python3.12/site-packages/openpyxl/reader/strings.py
new file mode 100644
index 00000000..5168f201
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/reader/strings.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.cell.text import Text
+
+from openpyxl.xml.functions import iterparse
+from openpyxl.xml.constants import SHEET_MAIN_NS
+from openpyxl.cell.rich_text import CellRichText
+
+
+def read_string_table(xml_source):
+    """Read in all shared strings in the table"""
+
+    strings = []
+    STRING_TAG = '{%s}si' % SHEET_MAIN_NS
+
+    for _, node in iterparse(xml_source):
+        if node.tag == STRING_TAG:
+            text = Text.from_tree(node).content
+            text = text.replace('x005F_', '')
+            node.clear()
+
+            strings.append(text)
+
+    return strings
+
+
+def read_rich_text(xml_source):
+    """Read in all shared strings in the table"""
+
+    strings = []
+    STRING_TAG = '{%s}si' % SHEET_MAIN_NS
+
+    for _, node in iterparse(xml_source):
+        if node.tag == STRING_TAG:
+            text = CellRichText.from_tree(node)
+            if len(text) == 0:
+                text = ''
+            elif len(text) == 1 and isinstance(text[0], str):
+                text = text[0]
+            node.clear()
+
+            strings.append(text)
+
+    return strings
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/reader/workbook.py b/.venv/lib/python3.12/site-packages/openpyxl/reader/workbook.py
new file mode 100644
index 00000000..2afbfddb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/reader/workbook.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from warnings import warn
+
+from openpyxl.xml.functions import fromstring
+
+from openpyxl.packaging.relationship import (
+    get_dependents,
+    get_rels_path,
+    get_rel,
+)
+from openpyxl.packaging.workbook import WorkbookPackage
+from openpyxl.workbook import Workbook
+from openpyxl.workbook.defined_name import DefinedNameList
+from openpyxl.workbook.external_link.external import read_external_link
+from openpyxl.pivot.cache import CacheDefinition
+from openpyxl.pivot.record import RecordList
+from openpyxl.worksheet.print_settings import PrintTitles, PrintArea
+
+from openpyxl.utils.datetime import CALENDAR_MAC_1904
+
+
+class WorkbookParser:
+
+    _rels = None
+
+    def __init__(self, archive, workbook_part_name, keep_links=True):
+        self.archive = archive
+        self.workbook_part_name = workbook_part_name
+        self.defined_names = DefinedNameList()
+        self.wb = Workbook()
+        self.keep_links = keep_links
+        self.sheets = []
+
+
+    @property
+    def rels(self):
+        if self._rels is None:
+            self._rels = get_dependents(self.archive, get_rels_path(self.workbook_part_name)).to_dict()
+        return self._rels
+
+
+    def parse(self):
+        src = self.archive.read(self.workbook_part_name)
+        node = fromstring(src)
+        package = WorkbookPackage.from_tree(node)
+        if package.properties.date1904:
+            self.wb.epoch = CALENDAR_MAC_1904
+
+        self.wb.code_name = package.properties.codeName
+        self.wb.active = package.active
+        self.wb.views = package.bookViews
+        self.sheets = package.sheets
+        self.wb.calculation = package.calcPr
+        self.caches = package.pivotCaches
+
+        # external links contain cached worksheets and can be very big
+        if not self.keep_links:
+            package.externalReferences = []
+
+        for ext_ref in package.externalReferences:
+            rel = self.rels.get(ext_ref.id)
+            self.wb._external_links.append(
+                read_external_link(self.archive, rel.Target)
+            )
+
+        if package.definedNames:
+            self.defined_names = package.definedNames
+
+        self.wb.security = package.workbookProtection
+
+
+    def find_sheets(self):
+        """
+        Find all sheets in the workbook and return the link to the source file.
+
+        Older XLSM files sometimes contain invalid sheet elements.
+        Warn user when these are removed.
+        """
+
+        for sheet in self.sheets:
+            if not sheet.id:
+                msg = f"File contains an invalid specification for {0}. This will be removed".format(sheet.name)
+                warn(msg)
+                continue
+            yield sheet, self.rels[sheet.id]
+
+
+    def assign_names(self):
+        """
+        Bind defined names and other definitions to worksheets or the workbook
+        """
+
+        for idx, names in self.defined_names.by_sheet().items():
+            if idx == "global":
+                self.wb.defined_names = names
+                continue
+
+            try:
+                sheet = self.wb._sheets[idx]
+            except IndexError:
+                warn(f"Defined names for sheet index {idx} cannot be located")
+                continue
+
+            for name, defn in names.items():
+                reserved = defn.is_reserved
+                if reserved is None:
+                    sheet.defined_names[name] = defn
+
+                elif reserved == "Print_Titles":
+                    titles = PrintTitles.from_string(defn.value)
+                    sheet._print_rows = titles.rows
+                    sheet._print_cols = titles.cols
+                elif reserved == "Print_Area":
+                    try:
+                        sheet._print_area = PrintArea.from_string(defn.value)
+                    except TypeError:
+                        warn(f"Print area cannot be set to Defined name: {defn.value}.")
+                        continue
+
+    @property
+    def pivot_caches(self):
+        """
+        Get PivotCache objects
+        """
+        d = {}
+        for c in self.caches:
+            cache = get_rel(self.archive, self.rels, id=c.id, cls=CacheDefinition)
+            if cache.deps:
+                records = get_rel(self.archive, cache.deps, cache.id, RecordList)
+                cache.records = records
+            d[c.cacheId] = cache
+        return d
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/__init__.py
new file mode 100644
index 00000000..ea20d0d1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/__init__.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from .alignment import Alignment
+from .borders import Border, Side
+from .colors import Color
+from .fills import PatternFill, GradientFill, Fill
+from .fonts import Font, DEFAULT_FONT
+from .numbers import NumberFormatDescriptor, is_date_format, is_builtin
+from .protection import Protection
+from .named_styles import NamedStyle
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/alignment.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/alignment.py
new file mode 100644
index 00000000..a727f673
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/alignment.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+
+from openpyxl.descriptors import Bool, MinMax, Min, Alias, NoneSet
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+horizontal_alignments = (
+    "general", "left", "center", "right", "fill", "justify", "centerContinuous",
+    "distributed", )
+vertical_aligments = (
+    "top", "center", "bottom", "justify", "distributed",
+)
+
+class Alignment(Serialisable):
+    """Alignment options for use in styles."""
+
+    tagname = "alignment"
+
+    horizontal = NoneSet(values=horizontal_alignments)
+    vertical = NoneSet(values=vertical_aligments)
+    textRotation = NoneSet(values=range(181))
+    textRotation.values.add(255)
+    text_rotation = Alias('textRotation')
+    wrapText = Bool(allow_none=True)
+    wrap_text = Alias('wrapText')
+    shrinkToFit = Bool(allow_none=True)
+    shrink_to_fit = Alias('shrinkToFit')
+    indent = MinMax(min=0, max=255)
+    relativeIndent = MinMax(min=-255, max=255)
+    justifyLastLine = Bool(allow_none=True)
+    readingOrder = Min(min=0)
+
+    def __init__(self, horizontal=None, vertical=None,
+                 textRotation=0, wrapText=None, shrinkToFit=None, indent=0, relativeIndent=0,
+                 justifyLastLine=None, readingOrder=0, text_rotation=None,
+                 wrap_text=None, shrink_to_fit=None, mergeCell=None):
+        self.horizontal = horizontal
+        self.vertical = vertical
+        self.indent = indent
+        self.relativeIndent = relativeIndent
+        self.justifyLastLine = justifyLastLine
+        self.readingOrder = readingOrder
+        if text_rotation is not None:
+            textRotation = text_rotation
+        if textRotation is not None:
+            self.textRotation = int(textRotation)
+        if wrap_text is not None:
+            wrapText = wrap_text
+        self.wrapText = wrapText
+        if shrink_to_fit is not None:
+            shrinkToFit = shrink_to_fit
+        self.shrinkToFit = shrinkToFit
+        # mergeCell is vestigial
+
+
+    def __iter__(self):
+        for attr in self.__attrs__:
+            value = getattr(self, attr)
+            if value is not None and value != 0:
+                yield attr, safe_string(value)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/borders.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/borders.py
new file mode 100644
index 00000000..f9fce814
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/borders.py
@@ -0,0 +1,103 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+from openpyxl.descriptors import (
+    NoneSet,
+    Typed,
+    Bool,
+    Alias,
+    Sequence,
+    Integer,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+from .colors import ColorDescriptor
+
+
+BORDER_NONE = None
+BORDER_DASHDOT = 'dashDot'
+BORDER_DASHDOTDOT = 'dashDotDot'
+BORDER_DASHED = 'dashed'
+BORDER_DOTTED = 'dotted'
+BORDER_DOUBLE = 'double'
+BORDER_HAIR = 'hair'
+BORDER_MEDIUM = 'medium'
+BORDER_MEDIUMDASHDOT = 'mediumDashDot'
+BORDER_MEDIUMDASHDOTDOT = 'mediumDashDotDot'
+BORDER_MEDIUMDASHED = 'mediumDashed'
+BORDER_SLANTDASHDOT = 'slantDashDot'
+BORDER_THICK = 'thick'
+BORDER_THIN = 'thin'
+
+
+class Side(Serialisable):
+
+    """Border options for use in styles.
+    Caution: if you do not specify a border_style, other attributes will
+    have no effect !"""
+
+
+    color = ColorDescriptor(allow_none=True)
+    style = NoneSet(values=('dashDot','dashDotDot', 'dashed','dotted',
+                            'double','hair', 'medium', 'mediumDashDot', 'mediumDashDotDot',
+                            'mediumDashed', 'slantDashDot', 'thick', 'thin')
+                    )
+    border_style = Alias('style')
+
+    def __init__(self, style=None, color=None, border_style=None):
+        if border_style is not None:
+            style = border_style
+        self.style = style
+        self.color = color
+
+
+class Border(Serialisable):
+    """Border positioning for use in styles."""
+
+    tagname = "border"
+
+    __elements__ = ('start', 'end', 'left', 'right', 'top', 'bottom',
+                    'diagonal', 'vertical', 'horizontal')
+
+    # child elements
+    start = Typed(expected_type=Side, allow_none=True)
+    end = Typed(expected_type=Side, allow_none=True)
+    left = Typed(expected_type=Side, allow_none=True)
+    right = Typed(expected_type=Side, allow_none=True)
+    top = Typed(expected_type=Side, allow_none=True)
+    bottom = Typed(expected_type=Side, allow_none=True)
+    diagonal = Typed(expected_type=Side, allow_none=True)
+    vertical = Typed(expected_type=Side, allow_none=True)
+    horizontal = Typed(expected_type=Side, allow_none=True)
+    # attributes
+    outline = Bool()
+    diagonalUp = Bool()
+    diagonalDown = Bool()
+
+    def __init__(self, left=None, right=None, top=None,
+                 bottom=None, diagonal=None, diagonal_direction=None,
+                 vertical=None, horizontal=None, diagonalUp=False, diagonalDown=False,
+                 outline=True, start=None, end=None):
+        self.left = left
+        self.right = right
+        self.top = top
+        self.bottom = bottom
+        self.diagonal = diagonal
+        self.vertical = vertical
+        self.horizontal = horizontal
+        self.diagonal_direction = diagonal_direction
+        self.diagonalUp = diagonalUp
+        self.diagonalDown = diagonalDown
+        self.outline = outline
+        self.start = start
+        self.end = end
+
+    def __iter__(self):
+        for attr in self.__attrs__:
+            value = getattr(self, attr)
+            if value and attr != "outline":
+                yield attr, safe_string(value)
+            elif attr == "outline" and not value:
+                yield attr, safe_string(value)
+
+DEFAULT_BORDER = Border(left=Side(), right=Side(), top=Side(), bottom=Side(), diagonal=Side())
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/builtins.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/builtins.py
new file mode 100644
index 00000000..7095eb32
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/builtins.py
@@ -0,0 +1,1397 @@
+# Copyright (c) 2010-2024 openpyxl
+
+# Builtins styles as defined in Part 4 Annex G.2
+
+from .named_styles import NamedStyle
+from openpyxl.xml.functions import fromstring
+
+
+normal = """
+  <namedStyle builtinId="0" name="Normal">
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+comma = """
+  <namedStyle builtinId="3" name="Comma">
+    <alignment/>
+    <number_format>_-* #,##0.00\\ _$_-;\\-* #,##0.00\\ _$_-;_-* "-"??\\ _$_-;_-@_-</number_format>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+comma_0 = """
+  <namedStyle builtinId="6" name="Comma [0]">
+    <alignment/>
+    <number_format>_-* #,##0\\ _$_-;\\-* #,##0\\ _$_-;_-* "-"\\ _$_-;_-@_-</number_format>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+currency = """
+  <namedStyle builtinId="4" name="Currency">
+    <alignment/>
+    <number_format>_-* #,##0.00\\ "$"_-;\\-* #,##0.00\\ "$"_-;_-* "-"??\\ "$"_-;_-@_-</number_format>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+currency_0 = """
+  <namedStyle builtinId="7" name="Currency [0]">
+    <alignment/>
+    <number_format>_-* #,##0\\ "$"_-;\\-* #,##0\\ "$"_-;_-* "-"\\ "$"_-;_-@_-</number_format>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+percent = """
+  <namedStyle builtinId="5" name="Percent">
+    <alignment/>
+    <number_format>0%</number_format>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+hyperlink = """
+  <namedStyle builtinId="8" name="Hyperlink" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="10"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+followed_hyperlink = """
+  <namedStyle builtinId="9" name="Followed Hyperlink" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="11"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+title = """
+  <namedStyle builtinId="15" name="Title">
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Cambria"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="3"/>
+      <sz val="18"/>
+      <scheme val="major"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+headline_1 = """
+  <namedStyle builtinId="16" name="Headline 1" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom style="thick">
+        <color theme="4"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="3"/>
+      <sz val="15"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+headline_2 = """
+  <namedStyle builtinId="17" name="Headline 2" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom style="thick">
+        <color theme="4" tint="0.5"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="3"/>
+      <sz val="13"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+headline_3 = """
+   <namedStyle builtinId="18" name="Headline 3" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom style="medium">
+        <color theme="4" tint="0.4"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="3"/>
+      <sz val="11"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+
+"""
+
+headline_4 = """
+  <namedStyle builtinId="19" name="Headline 4">
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="3"/>
+      <sz val="11"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+good = """
+  <namedStyle builtinId="26" name="Good" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFC6EFCE"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FF006100"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+bad = """
+  <namedStyle builtinId="27" name="Bad" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFFFC7CE"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FF9C0006"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+neutral = """
+  <namedStyle builtinId="28" name="Neutral" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFFFEB9C"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FF9C6500"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+input = """
+  <namedStyle builtinId="20" name="Input" >
+    <alignment/>
+    <border>
+      <left style="thin">
+        <color rgb="FF7F7F7F"/>
+      </left>
+      <right style="thin">
+        <color rgb="FF7F7F7F"/>
+      </right>
+      <top style="thin">
+        <color rgb="FF7F7F7F"/>
+      </top>
+      <bottom style="thin">
+        <color rgb="FF7F7F7F"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFFFCC99"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FF3F3F76"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+output = """
+  <namedStyle builtinId="21" name="Output" >
+    <alignment/>
+    <border>
+      <left style="thin">
+        <color rgb="FF3F3F3F"/>
+      </left>
+      <right style="thin">
+        <color rgb="FF3F3F3F"/>
+      </right>
+      <top style="thin">
+        <color rgb="FF3F3F3F"/>
+      </top>
+      <bottom style="thin">
+        <color rgb="FF3F3F3F"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFF2F2F2"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color rgb="FF3F3F3F"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+calculation = """
+  <namedStyle builtinId="22" name="Calculation" >
+    <alignment/>
+    <border>
+      <left style="thin">
+        <color rgb="FF7F7F7F"/>
+      </left>
+      <right style="thin">
+        <color rgb="FF7F7F7F"/>
+      </right>
+      <top style="thin">
+        <color rgb="FF7F7F7F"/>
+      </top>
+      <bottom style="thin">
+        <color rgb="FF7F7F7F"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFF2F2F2"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color rgb="FFFA7D00"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+linked_cell = """
+  <namedStyle builtinId="24" name="Linked Cell" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom style="double">
+        <color rgb="FFFF8001"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FFFA7D00"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+check_cell = """
+  <namedStyle builtinId="23" name="Check Cell" >
+    <alignment/>
+    <border>
+      <left style="double">
+        <color rgb="FF3F3F3F"/>
+      </left>
+      <right style="double">
+        <color rgb="FF3F3F3F"/>
+      </right>
+      <top style="double">
+        <color rgb="FF3F3F3F"/>
+      </top>
+      <bottom style="double">
+        <color rgb="FF3F3F3F"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFA5A5A5"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+warning = """
+  <namedStyle builtinId="11" name="Warning Text" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color rgb="FFFF0000"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+note = """
+  <namedStyle builtinId="10" name="Note" >
+    <alignment/>
+    <border>
+      <left style="thin">
+        <color rgb="FFB2B2B2"/>
+      </left>
+      <right style="thin">
+        <color rgb="FFB2B2B2"/>
+      </right>
+      <top style="thin">
+        <color rgb="FFB2B2B2"/>
+      </top>
+      <bottom style="thin">
+        <color rgb="FFB2B2B2"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor rgb="FFFFFFCC"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+explanatory = """
+  <namedStyle builtinId="53" name="Explanatory Text" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <i val="1"/>
+      <color rgb="FF7F7F7F"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+total = """
+  <namedStyle builtinId="25" name="Total" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top style="thin">
+        <color theme="4"/>
+      </top>
+      <bottom style="double">
+        <color theme="4"/>
+      </bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <b val="1"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_1 = """
+  <namedStyle builtinId="29" name="Accent1" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="4"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_1_20 = """
+  <namedStyle builtinId="30" name="20 % - Accent1" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="4" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_1_40 = """
+  <namedStyle builtinId="31" name="40 % - Accent1" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="4" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_1_60 = """
+  <namedStyle builtinId="32" name="60 % - Accent1" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="4" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_2 = """<namedStyle builtinId="33" name="Accent2" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="5"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_2_20 = """
+  <namedStyle builtinId="34" name="20 % - Accent2" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="5" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_2_40 = """
+<namedStyle builtinId="35" name="40 % - Accent2" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="5" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_2_60 = """
+<namedStyle builtinId="36" name="60 % - Accent2" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="5" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_3 = """
+<namedStyle builtinId="37" name="Accent3" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="6"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_3_20 = """
+  <namedStyle builtinId="38" name="20 % - Accent3" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="6" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>"""
+
+accent_3_40 = """
+  <namedStyle builtinId="39" name="40 % - Accent3" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="6" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+accent_3_60 = """
+  <namedStyle builtinId="40" name="60 % - Accent3" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="6" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+accent_4 = """
+  <namedStyle builtinId="41" name="Accent4" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="7"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_4_20 = """
+  <namedStyle builtinId="42" name="20 % - Accent4" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="7" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_4_40 = """
+  <namedStyle builtinId="43" name="40 % - Accent4" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="7" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_4_60 = """
+<namedStyle builtinId="44" name="60 % - Accent4" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="7" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_5 = """
+  <namedStyle builtinId="45" name="Accent5" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="8"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_5_20 = """
+  <namedStyle builtinId="46" name="20 % - Accent5" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="8" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_5_40 = """
+  <namedStyle builtinId="47" name="40 % - Accent5" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="8" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_5_60 = """
+  <namedStyle builtinId="48" name="60 % - Accent5" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="8" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_6 = """
+  <namedStyle builtinId="49" name="Accent6" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="9"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_6_20 = """
+  <namedStyle builtinId="50" name="20 % - Accent6" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="9" tint="0.7999816888943144"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_6_40 = """
+  <namedStyle builtinId="51" name="40 % - Accent6" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="9" tint="0.5999938962981048"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="1"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+accent_6_60 = """
+  <namedStyle builtinId="52" name="60 % - Accent6" >
+    <alignment/>
+    <border>
+      <left/>
+      <right/>
+      <top/>
+      <bottom/>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill patternType="solid">
+        <fgColor theme="9" tint="0.3999755851924192"/>
+        <bgColor indexed="65"/>
+      </patternFill>
+    </fill>
+    <font>
+      <name val="Calibri"/>
+      <family val="2"/>
+      <color theme="0"/>
+      <sz val="12"/>
+      <scheme val="minor"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+pandas_highlight = """
+  <namedStyle hidden="0" name="Pandas">
+    <alignment horizontal="center"/>
+    <border>
+      <left style="thin"><color rgb="00000000"/></left>
+      <right style="thin"><color rgb="00000000"/></right>
+      <top style="thin"><color rgb="00000000"/></top>
+      <bottom style="thin"><color rgb="00000000"/></bottom>
+      <diagonal/>
+    </border>
+    <fill>
+      <patternFill/>
+    </fill>
+    <font>
+      <b val="1"/>
+    </font>
+    <protection hidden="0" locked="1"/>
+  </namedStyle>
+"""
+
+styles = dict(
+    [
+        ('Normal', NamedStyle.from_tree(fromstring(normal))),
+        ('Comma', NamedStyle.from_tree(fromstring(comma))),
+        ('Currency', NamedStyle.from_tree(fromstring(currency))),
+        ('Percent', NamedStyle.from_tree(fromstring(percent))),
+        ('Comma [0]', NamedStyle.from_tree(fromstring(comma_0))),
+        ('Currency [0]', NamedStyle.from_tree(fromstring(currency_0))),
+        ('Hyperlink', NamedStyle.from_tree(fromstring(hyperlink))),
+        ('Followed Hyperlink', NamedStyle.from_tree(fromstring(followed_hyperlink))),
+        ('Note', NamedStyle.from_tree(fromstring(note))),
+        ('Warning Text', NamedStyle.from_tree(fromstring(warning))),
+        ('Title', NamedStyle.from_tree(fromstring(title))),
+        ('Headline 1', NamedStyle.from_tree(fromstring(headline_1))),
+        ('Headline 2', NamedStyle.from_tree(fromstring(headline_2))),
+        ('Headline 3', NamedStyle.from_tree(fromstring(headline_3))),
+        ('Headline 4', NamedStyle.from_tree(fromstring(headline_4))),
+        ('Input', NamedStyle.from_tree(fromstring(input))),
+        ('Output', NamedStyle.from_tree(fromstring(output))),
+        ('Calculation',NamedStyle.from_tree(fromstring(calculation))),
+        ('Check Cell', NamedStyle.from_tree(fromstring(check_cell))),
+        ('Linked Cell', NamedStyle.from_tree(fromstring(linked_cell))),
+        ('Total', NamedStyle.from_tree(fromstring(total))),
+        ('Good', NamedStyle.from_tree(fromstring(good))),
+        ('Bad', NamedStyle.from_tree(fromstring(bad))),
+        ('Neutral', NamedStyle.from_tree(fromstring(neutral))),
+        ('Accent1', NamedStyle.from_tree(fromstring(accent_1))),
+        ('20 % - Accent1', NamedStyle.from_tree(fromstring(accent_1_20))),
+        ('40 % - Accent1', NamedStyle.from_tree(fromstring(accent_1_40))),
+        ('60 % - Accent1', NamedStyle.from_tree(fromstring(accent_1_60))),
+        ('Accent2', NamedStyle.from_tree(fromstring(accent_2))),
+        ('20 % - Accent2', NamedStyle.from_tree(fromstring(accent_2_20))),
+        ('40 % - Accent2', NamedStyle.from_tree(fromstring(accent_2_40))),
+        ('60 % - Accent2', NamedStyle.from_tree(fromstring(accent_2_60))),
+        ('Accent3', NamedStyle.from_tree(fromstring(accent_3))),
+        ('20 % - Accent3', NamedStyle.from_tree(fromstring(accent_3_20))),
+        ('40 % - Accent3', NamedStyle.from_tree(fromstring(accent_3_40))),
+        ('60 % - Accent3', NamedStyle.from_tree(fromstring(accent_3_60))),
+        ('Accent4', NamedStyle.from_tree(fromstring(accent_4))),
+        ('20 % - Accent4', NamedStyle.from_tree(fromstring(accent_4_20))),
+        ('40 % - Accent4', NamedStyle.from_tree(fromstring(accent_4_40))),
+        ('60 % - Accent4', NamedStyle.from_tree(fromstring(accent_4_60))),
+        ('Accent5', NamedStyle.from_tree(fromstring(accent_5))),
+        ('20 % - Accent5', NamedStyle.from_tree(fromstring(accent_5_20))),
+        ('40 % - Accent5', NamedStyle.from_tree(fromstring(accent_5_40))),
+        ('60 % - Accent5', NamedStyle.from_tree(fromstring(accent_5_60))),
+        ('Accent6', NamedStyle.from_tree(fromstring(accent_6))),
+        ('20 % - Accent6', NamedStyle.from_tree(fromstring(accent_6_20))),
+        ('40 % - Accent6', NamedStyle.from_tree(fromstring(accent_6_40))),
+        ('60 % - Accent6', NamedStyle.from_tree(fromstring(accent_6_60))),
+        ('Explanatory Text', NamedStyle.from_tree(fromstring(explanatory))),
+        ('Pandas', NamedStyle.from_tree(fromstring(pandas_highlight)))
+    ]
+)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/cell_style.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/cell_style.py
new file mode 100644
index 00000000..51091aa5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/cell_style.py
@@ -0,0 +1,206 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from array import array
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Float,
+    Bool,
+    Integer,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.utils.indexed_list import IndexedList
+
+
+from .alignment import Alignment
+from .protection import Protection
+
+
+class ArrayDescriptor:
+
+    def __init__(self, key):
+        self.key = key
+
+    def __get__(self, instance, cls):
+        return instance[self.key]
+
+    def __set__(self, instance, value):
+        instance[self.key] = value
+
+
+class StyleArray(array):
+    """
+    Simplified named tuple with an array
+    """
+
+    __slots__ = ()
+    tagname = 'xf'
+
+    fontId = ArrayDescriptor(0)
+    fillId = ArrayDescriptor(1)
+    borderId = ArrayDescriptor(2)
+    numFmtId = ArrayDescriptor(3)
+    protectionId = ArrayDescriptor(4)
+    alignmentId = ArrayDescriptor(5)
+    pivotButton = ArrayDescriptor(6)
+    quotePrefix = ArrayDescriptor(7)
+    xfId = ArrayDescriptor(8)
+
+
+    def __new__(cls, args=[0]*9):
+        return array.__new__(cls, 'i', args)
+
+
+    def __hash__(self):
+        return hash(tuple(self))
+
+
+    def __copy__(self):
+        return StyleArray((self))
+
+
+    def __deepcopy__(self, memo):
+        return StyleArray((self))
+
+
+class CellStyle(Serialisable):
+
+    tagname = "xf"
+
+    numFmtId = Integer()
+    fontId = Integer()
+    fillId = Integer()
+    borderId = Integer()
+    xfId = Integer(allow_none=True)
+    quotePrefix = Bool(allow_none=True)
+    pivotButton = Bool(allow_none=True)
+    applyNumberFormat = Bool(allow_none=True)
+    applyFont = Bool(allow_none=True)
+    applyFill = Bool(allow_none=True)
+    applyBorder = Bool(allow_none=True)
+    applyAlignment = Bool(allow_none=True)
+    applyProtection = Bool(allow_none=True)
+    alignment = Typed(expected_type=Alignment, allow_none=True)
+    protection = Typed(expected_type=Protection, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('alignment', 'protection')
+    __attrs__ = ("numFmtId", "fontId", "fillId", "borderId",
+                 "applyAlignment", "applyProtection", "pivotButton", "quotePrefix", "xfId")
+
+    def __init__(self,
+                 numFmtId=0,
+                 fontId=0,
+                 fillId=0,
+                 borderId=0,
+                 xfId=None,
+                 quotePrefix=None,
+                 pivotButton=None,
+                 applyNumberFormat=None,
+                 applyFont=None,
+                 applyFill=None,
+                 applyBorder=None,
+                 applyAlignment=None,
+                 applyProtection=None,
+                 alignment=None,
+                 protection=None,
+                 extLst=None,
+                ):
+        self.numFmtId = numFmtId
+        self.fontId = fontId
+        self.fillId = fillId
+        self.borderId = borderId
+        self.xfId = xfId
+        self.quotePrefix = quotePrefix
+        self.pivotButton = pivotButton
+        self.applyNumberFormat = applyNumberFormat
+        self.applyFont = applyFont
+        self.applyFill = applyFill
+        self.applyBorder = applyBorder
+        self.alignment = alignment
+        self.protection = protection
+
+
+    def to_array(self):
+        """
+        Convert to StyleArray
+        """
+        style = StyleArray()
+        for k in ("fontId", "fillId", "borderId", "numFmtId", "pivotButton",
+                  "quotePrefix", "xfId"):
+            v = getattr(self, k, 0)
+            if v is not None:
+                setattr(style, k, v)
+        return style
+
+
+    @classmethod
+    def from_array(cls, style):
+        """
+        Convert from StyleArray
+        """
+        return cls(numFmtId=style.numFmtId, fontId=style.fontId,
+                   fillId=style.fillId, borderId=style.borderId, xfId=style.xfId,
+                   quotePrefix=style.quotePrefix, pivotButton=style.pivotButton,)
+
+
+    @property
+    def applyProtection(self):
+        return self.protection is not None or None
+
+
+    @property
+    def applyAlignment(self):
+        return self.alignment is not None or None
+
+
+class CellStyleList(Serialisable):
+
+    tagname = "cellXfs"
+
+    __attrs__ = ("count",)
+
+    count = Integer(allow_none=True)
+    xf = Sequence(expected_type=CellStyle)
+    alignment = Sequence(expected_type=Alignment)
+    protection = Sequence(expected_type=Protection)
+
+    __elements__ = ('xf',)
+
+    def __init__(self,
+                 count=None,
+                 xf=(),
+                ):
+        self.xf = xf
+
+
+    @property
+    def count(self):
+        return len(self.xf)
+
+
+    def __getitem__(self, idx):
+        try:
+            return self.xf[idx]
+        except IndexError:
+            print((f"{idx} is out of range"))
+        return self.xf[idx]
+
+
+    def _to_array(self):
+        """
+        Extract protection and alignments, convert to style array
+        """
+        self.prots = IndexedList([Protection()])
+        self.alignments = IndexedList([Alignment()])
+        styles = [] # allow duplicates
+        for xf in self.xf:
+            style = xf.to_array()
+            if xf.alignment is not None:
+                style.alignmentId = self.alignments.add(xf.alignment)
+            if xf.protection is not None:
+                style.protectionId = self.prots.add(xf.protection)
+            styles.append(style)
+        return IndexedList(styles)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/colors.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/colors.py
new file mode 100644
index 00000000..6fa7476d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/colors.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import re
+from openpyxl.compat import safe_string
+from openpyxl.descriptors import (
+    String,
+    Bool,
+    MinMax,
+    Integer,
+    Typed,
+)
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.descriptors.serialisable import Serialisable
+
+# Default Color Index as per 18.8.27 of ECMA Part 4
+COLOR_INDEX = (
+    '00000000', '00FFFFFF', '00FF0000', '0000FF00', '000000FF', #0-4
+    '00FFFF00', '00FF00FF', '0000FFFF', '00000000', '00FFFFFF', #5-9
+    '00FF0000', '0000FF00', '000000FF', '00FFFF00', '00FF00FF', #10-14
+    '0000FFFF', '00800000', '00008000', '00000080', '00808000', #15-19
+    '00800080', '00008080', '00C0C0C0', '00808080', '009999FF', #20-24
+    '00993366', '00FFFFCC', '00CCFFFF', '00660066', '00FF8080', #25-29
+    '000066CC', '00CCCCFF', '00000080', '00FF00FF', '00FFFF00', #30-34
+    '0000FFFF', '00800080', '00800000', '00008080', '000000FF', #35-39
+    '0000CCFF', '00CCFFFF', '00CCFFCC', '00FFFF99', '0099CCFF', #40-44
+    '00FF99CC', '00CC99FF', '00FFCC99', '003366FF', '0033CCCC', #45-49
+    '0099CC00', '00FFCC00', '00FF9900', '00FF6600', '00666699', #50-54
+    '00969696', '00003366', '00339966', '00003300', '00333300', #55-59
+    '00993300', '00993366', '00333399', '00333333',  #60-63
+)
+# indices 64 and 65 are reserved for the system foreground and background colours respectively
+
+# Will remove these definitions in a future release
+BLACK = COLOR_INDEX[0]
+WHITE = COLOR_INDEX[1]
+#RED = COLOR_INDEX[2]
+#DARKRED = COLOR_INDEX[8]
+BLUE = COLOR_INDEX[4]
+#DARKBLUE = COLOR_INDEX[12]
+#GREEN = COLOR_INDEX[3]
+#DARKGREEN = COLOR_INDEX[9]
+#YELLOW = COLOR_INDEX[5]
+#DARKYELLOW = COLOR_INDEX[19]
+
+
+aRGB_REGEX = re.compile("^([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6})$")
+
+
+class RGB(Typed):
+    """
+    Descriptor for aRGB values
+    If not supplied alpha is 00
+    """
+
+    expected_type = str
+
+    def __set__(self, instance, value):
+        if not self.allow_none:
+            m = aRGB_REGEX.match(value)
+            if m is None:
+                raise ValueError("Colors must be aRGB hex values")
+            if len(value) == 6:
+                value = "00" + value
+        super().__set__(instance, value)
+
+
+class Color(Serialisable):
+    """Named colors for use in styles."""
+
+    tagname = "color"
+
+    rgb = RGB()
+    indexed = Integer()
+    auto = Bool()
+    theme = Integer()
+    tint = MinMax(min=-1, max=1, expected_type=float)
+    type = String()
+
+
+    def __init__(self, rgb=BLACK, indexed=None, auto=None, theme=None, tint=0.0, index=None, type='rgb'):
+        if index is not None:
+            indexed = index
+        if indexed is not None:
+            self.type = 'indexed'
+            self.indexed = indexed
+        elif theme is not None:
+            self.type = 'theme'
+            self.theme = theme
+        elif auto is not None:
+            self.type = 'auto'
+            self.auto = auto
+        else:
+            self.rgb = rgb
+            self.type = 'rgb'
+        self.tint = tint
+
+    @property
+    def value(self):
+        return getattr(self, self.type)
+
+    @value.setter
+    def value(self, value):
+        setattr(self, self.type, value)
+
+    def __iter__(self):
+        attrs = [(self.type, self.value)]
+        if self.tint != 0:
+            attrs.append(('tint', self.tint))
+        for k, v in attrs:
+            yield k, safe_string(v)
+
+    @property
+    def index(self):
+        # legacy
+        return self.value
+
+
+    def __add__(self, other):
+        """
+        Adding colours is undefined behaviour best do nothing
+        """
+        if not isinstance(other, Color):
+            return super().__add__(other)
+        return self
+
+
+class ColorDescriptor(Typed):
+
+    expected_type = Color
+
+    def __set__(self, instance, value):
+        if isinstance(value, str):
+            value = Color(rgb=value)
+        super().__set__(instance, value)
+
+
+class RgbColor(Serialisable):
+
+    tagname = "rgbColor"
+
+    rgb = RGB()
+
+    def __init__(self,
+                 rgb=None,
+                ):
+        self.rgb = rgb
+
+
+class ColorList(Serialisable):
+
+    tagname = "colors"
+
+    indexedColors = NestedSequence(expected_type=RgbColor)
+    mruColors = NestedSequence(expected_type=Color)
+
+    __elements__ = ('indexedColors', 'mruColors')
+
+    def __init__(self,
+                 indexedColors=(),
+                 mruColors=(),
+                ):
+        self.indexedColors = indexedColors
+        self.mruColors = mruColors
+
+
+    def __bool__(self):
+        return bool(self.indexedColors) or bool(self.mruColors)
+
+
+    @property
+    def index(self):
+        return [val.rgb for val in self.indexedColors]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/differential.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/differential.py
new file mode 100644
index 00000000..109577e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/differential.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    Alias,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.styles import (
+    Font,
+    Fill,
+    Border,
+    Alignment,
+    Protection,
+    )
+from .numbers import NumberFormat
+
+
+class DifferentialStyle(Serialisable):
+
+    tagname = "dxf"
+
+    __elements__ = ("font", "numFmt", "fill", "alignment", "border", "protection")
+
+    font = Typed(expected_type=Font, allow_none=True)
+    numFmt = Typed(expected_type=NumberFormat, allow_none=True)
+    fill = Typed(expected_type=Fill, allow_none=True)
+    alignment = Typed(expected_type=Alignment, allow_none=True)
+    border = Typed(expected_type=Border, allow_none=True)
+    protection = Typed(expected_type=Protection, allow_none=True)
+
+    def __init__(self,
+                 font=None,
+                 numFmt=None,
+                 fill=None,
+                 alignment=None,
+                 border=None,
+                 protection=None,
+                 extLst=None,
+                ):
+        self.font = font
+        self.numFmt = numFmt
+        self.fill = fill
+        self.alignment = alignment
+        self.border = border
+        self.protection = protection
+        self.extLst = extLst
+
+
+class DifferentialStyleList(Serialisable):
+    """
+    Dedupable container for differential styles.
+    """
+
+    tagname = "dxfs"
+
+    dxf = Sequence(expected_type=DifferentialStyle)
+    styles = Alias("dxf")
+    __attrs__ = ("count",)
+
+
+    def __init__(self, dxf=(), count=None):
+        self.dxf = dxf
+
+
+    def append(self, dxf):
+        """
+        Check to see whether style already exists and append it if does not.
+        """
+        if not isinstance(dxf, DifferentialStyle):
+            raise TypeError('expected ' + str(DifferentialStyle))
+        if dxf in self.styles:
+            return
+        self.styles.append(dxf)
+
+
+    def add(self, dxf):
+        """
+        Add a differential style and return its index
+        """
+        self.append(dxf)
+        return self.styles.index(dxf)
+
+
+    def __bool__(self):
+        return bool(self.styles)
+
+
+    def __getitem__(self, idx):
+        return self.styles[idx]
+
+
+    @property
+    def count(self):
+        return len(self.dxf)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/fills.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/fills.py
new file mode 100644
index 00000000..7071abd6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/fills.py
@@ -0,0 +1,224 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Float,
+    Set,
+    Alias,
+    NoneSet,
+    Sequence,
+    Integer,
+    MinMax,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.compat import safe_string
+
+from .colors import ColorDescriptor, Color
+
+from openpyxl.xml.functions import Element, localname
+from openpyxl.xml.constants import SHEET_MAIN_NS
+
+
+FILL_NONE = 'none'
+FILL_SOLID = 'solid'
+FILL_PATTERN_DARKDOWN = 'darkDown'
+FILL_PATTERN_DARKGRAY = 'darkGray'
+FILL_PATTERN_DARKGRID = 'darkGrid'
+FILL_PATTERN_DARKHORIZONTAL = 'darkHorizontal'
+FILL_PATTERN_DARKTRELLIS = 'darkTrellis'
+FILL_PATTERN_DARKUP = 'darkUp'
+FILL_PATTERN_DARKVERTICAL = 'darkVertical'
+FILL_PATTERN_GRAY0625 = 'gray0625'
+FILL_PATTERN_GRAY125 = 'gray125'
+FILL_PATTERN_LIGHTDOWN = 'lightDown'
+FILL_PATTERN_LIGHTGRAY = 'lightGray'
+FILL_PATTERN_LIGHTGRID = 'lightGrid'
+FILL_PATTERN_LIGHTHORIZONTAL = 'lightHorizontal'
+FILL_PATTERN_LIGHTTRELLIS = 'lightTrellis'
+FILL_PATTERN_LIGHTUP = 'lightUp'
+FILL_PATTERN_LIGHTVERTICAL = 'lightVertical'
+FILL_PATTERN_MEDIUMGRAY = 'mediumGray'
+
+fills = (FILL_SOLID, FILL_PATTERN_DARKDOWN, FILL_PATTERN_DARKGRAY,
+         FILL_PATTERN_DARKGRID, FILL_PATTERN_DARKHORIZONTAL, FILL_PATTERN_DARKTRELLIS,
+         FILL_PATTERN_DARKUP, FILL_PATTERN_DARKVERTICAL, FILL_PATTERN_GRAY0625,
+         FILL_PATTERN_GRAY125, FILL_PATTERN_LIGHTDOWN, FILL_PATTERN_LIGHTGRAY,
+         FILL_PATTERN_LIGHTGRID, FILL_PATTERN_LIGHTHORIZONTAL,
+         FILL_PATTERN_LIGHTTRELLIS, FILL_PATTERN_LIGHTUP, FILL_PATTERN_LIGHTVERTICAL,
+         FILL_PATTERN_MEDIUMGRAY)
+
+
+class Fill(Serialisable):
+
+    """Base class"""
+
+    tagname = "fill"
+
+    @classmethod
+    def from_tree(cls, el):
+        children = [c for c in el]
+        if not children:
+            return
+        child = children[0]
+        if "patternFill" in child.tag:
+            return PatternFill._from_tree(child)
+        return super(Fill, GradientFill).from_tree(child)
+
+
+class PatternFill(Fill):
+    """Area fill patterns for use in styles.
+    Caution: if you do not specify a fill_type, other attributes will have
+    no effect !"""
+
+    tagname = "patternFill"
+
+    __elements__ = ('fgColor', 'bgColor')
+
+    patternType = NoneSet(values=fills)
+    fill_type = Alias("patternType")
+    fgColor = ColorDescriptor()
+    start_color = Alias("fgColor")
+    bgColor = ColorDescriptor()
+    end_color = Alias("bgColor")
+
+    def __init__(self, patternType=None, fgColor=Color(), bgColor=Color(),
+                 fill_type=None, start_color=None, end_color=None):
+        if fill_type is not None:
+            patternType = fill_type
+        self.patternType = patternType
+        if start_color is not None:
+            fgColor = start_color
+        self.fgColor = fgColor
+        if end_color is not None:
+            bgColor = end_color
+        self.bgColor = bgColor
+
+    @classmethod
+    def _from_tree(cls, el):
+        attrib = dict(el.attrib)
+        for child in el:
+            desc = localname(child)
+            attrib[desc] = Color.from_tree(child)
+        return cls(**attrib)
+
+
+    def to_tree(self, tagname=None, idx=None):
+        parent = Element("fill")
+        el = Element(self.tagname)
+        if self.patternType is not None:
+            el.set('patternType', self.patternType)
+        for c in self.__elements__:
+            value = getattr(self, c)
+            if value != Color():
+                el.append(value.to_tree(c))
+        parent.append(el)
+        return parent
+
+
+DEFAULT_EMPTY_FILL = PatternFill()
+DEFAULT_GRAY_FILL = PatternFill(patternType='gray125')
+
+
+class Stop(Serialisable):
+
+    tagname = "stop"
+
+    position = MinMax(min=0, max=1)
+    color = ColorDescriptor()
+
+    def __init__(self, color, position):
+        self.position = position
+        self.color = color
+
+
+def _assign_position(values):
+    """
+    Automatically assign positions if a list of colours is provided.
+
+    It is not permitted to mix colours and stops
+    """
+    n_values = len(values)
+    n_stops = sum(isinstance(value, Stop) for value in values)
+
+    if n_stops == 0:
+        interval = 1
+        if n_values > 2:
+            interval = 1 / (n_values - 1)
+        values = [Stop(value, i * interval)
+                  for i, value in enumerate(values)]
+
+    elif n_stops < n_values:
+        raise ValueError('Cannot interpret mix of Stops and Colors in GradientFill')
+
+    pos = set()
+    for stop in values:
+        if stop.position in pos:
+            raise ValueError("Duplicate position {0}".format(stop.position))
+        pos.add(stop.position)
+
+    return values
+
+
+class StopList(Sequence):
+
+    expected_type = Stop
+
+    def __set__(self, obj, values):
+        values = _assign_position(values)
+        super().__set__(obj, values)
+
+
+class GradientFill(Fill):
+    """Fill areas with gradient
+
+    Two types of gradient fill are supported:
+
+        - A type='linear' gradient interpolates colours between
+          a set of specified Stops, across the length of an area.
+          The gradient is left-to-right by default, but this
+          orientation can be modified with the degree
+          attribute.  A list of Colors can be provided instead
+          and they will be positioned with equal distance between them.
+
+        - A type='path' gradient applies a linear gradient from each
+          edge of the area. Attributes top, right, bottom, left specify
+          the extent of fill from the respective borders. Thus top="0.2"
+          will fill the top 20% of the cell.
+
+    """
+
+    tagname = "gradientFill"
+
+    type = Set(values=('linear', 'path'))
+    fill_type = Alias("type")
+    degree = Float()
+    left = Float()
+    right = Float()
+    top = Float()
+    bottom = Float()
+    stop = StopList()
+
+
+    def __init__(self, type="linear", degree=0, left=0, right=0, top=0,
+                 bottom=0, stop=()):
+        self.degree = degree
+        self.left = left
+        self.right = right
+        self.top = top
+        self.bottom = bottom
+        self.stop = stop
+        self.type = type
+
+
+    def __iter__(self):
+        for attr in self.__attrs__:
+            value = getattr(self, attr)
+            if value:
+                yield attr, safe_string(value)
+
+
+    def to_tree(self, tagname=None, namespace=None, idx=None):
+        parent = Element("fill")
+        el = super().to_tree()
+        parent.append(el)
+        return parent
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/fonts.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/fonts.py
new file mode 100644
index 00000000..06e343fc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/fonts.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors import (
+    Alias,
+    Sequence,
+    Integer
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+from openpyxl.descriptors.nested import (
+    NestedValue,
+    NestedBool,
+    NestedNoneSet,
+    NestedMinMax,
+    NestedString,
+    NestedInteger,
+    NestedFloat,
+)
+from .colors import ColorDescriptor, Color, BLACK
+
+from openpyxl.compat import safe_string
+from openpyxl.xml.functions import Element, SubElement
+from openpyxl.xml.constants import SHEET_MAIN_NS
+
+
+def _no_value(tagname, value, namespace=None):
+    if value:
+        return Element(tagname, val=safe_string(value))
+
+
+class Font(Serialisable):
+    """Font options used in styles."""
+
+    UNDERLINE_DOUBLE = 'double'
+    UNDERLINE_DOUBLE_ACCOUNTING = 'doubleAccounting'
+    UNDERLINE_SINGLE = 'single'
+    UNDERLINE_SINGLE_ACCOUNTING = 'singleAccounting'
+
+    name = NestedString(allow_none=True)
+    charset = NestedInteger(allow_none=True)
+    family = NestedMinMax(min=0, max=14, allow_none=True)
+    sz = NestedFloat(allow_none=True)
+    size = Alias("sz")
+    b = NestedBool(to_tree=_no_value)
+    bold = Alias("b")
+    i = NestedBool(to_tree=_no_value)
+    italic = Alias("i")
+    strike = NestedBool(allow_none=True)
+    strikethrough = Alias("strike")
+    outline = NestedBool(allow_none=True)
+    shadow = NestedBool(allow_none=True)
+    condense = NestedBool(allow_none=True)
+    extend = NestedBool(allow_none=True)
+    u = NestedNoneSet(values=('single', 'double', 'singleAccounting',
+                             'doubleAccounting'))
+    underline = Alias("u")
+    vertAlign = NestedNoneSet(values=('superscript', 'subscript', 'baseline'))
+    color = ColorDescriptor(allow_none=True)
+    scheme = NestedNoneSet(values=("major", "minor"))
+
+    tagname = "font"
+
+    __elements__ = ('name', 'charset', 'family', 'b', 'i', 'strike', 'outline',
+                  'shadow', 'condense', 'color', 'extend', 'sz', 'u', 'vertAlign',
+                  'scheme')
+
+
+    def __init__(self, name=None, sz=None, b=None, i=None, charset=None,
+                 u=None, strike=None, color=None, scheme=None, family=None, size=None,
+                 bold=None, italic=None, strikethrough=None, underline=None,
+                 vertAlign=None, outline=None, shadow=None, condense=None,
+                 extend=None):
+        self.name = name
+        self.family = family
+        if size is not None:
+            sz = size
+        self.sz = sz
+        if bold is not None:
+            b = bold
+        self.b = b
+        if italic is not None:
+            i = italic
+        self.i = i
+        if underline is not None:
+            u = underline
+        self.u = u
+        if strikethrough is not None:
+            strike = strikethrough
+        self.strike = strike
+        self.color = color
+        self.vertAlign = vertAlign
+        self.charset = charset
+        self.outline = outline
+        self.shadow = shadow
+        self.condense = condense
+        self.extend = extend
+        self.scheme = scheme
+
+
+    @classmethod
+    def from_tree(cls, node):
+        """
+        Set default value for underline if child element is present
+        """
+        underline = node.find("{%s}u" % SHEET_MAIN_NS)
+        if underline is not None and underline.get('val') is None:
+            underline.set("val", "single")
+        return super().from_tree(node)
+
+
+DEFAULT_FONT = Font(name="Calibri", sz=11, family=2, b=False, i=False,
+                    color=Color(theme=1), scheme="minor")
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/named_styles.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/named_styles.py
new file mode 100644
index 00000000..221d333b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/named_styles.py
@@ -0,0 +1,282 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    Bool,
+    String,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.serialisable import Serialisable
+
+from .fills import PatternFill, Fill
+from .fonts import Font
+from .borders import Border
+from .alignment import Alignment
+from .protection import Protection
+from .numbers import (
+    NumberFormatDescriptor,
+    BUILTIN_FORMATS_MAX_SIZE,
+    BUILTIN_FORMATS_REVERSE,
+)
+from .cell_style import (
+    StyleArray,
+    CellStyle,
+)
+
+
+class NamedStyle(Serialisable):
+
+    """
+    Named and editable styles
+    """
+
+    font = Typed(expected_type=Font)
+    fill = Typed(expected_type=Fill)
+    border = Typed(expected_type=Border)
+    alignment = Typed(expected_type=Alignment)
+    number_format = NumberFormatDescriptor()
+    protection = Typed(expected_type=Protection)
+    builtinId = Integer(allow_none=True)
+    hidden = Bool(allow_none=True)
+    name = String()
+    _wb = None
+    _style = StyleArray()
+
+
+    def __init__(self,
+                 name="Normal",
+                 font=None,
+                 fill=None,
+                 border=None,
+                 alignment=None,
+                 number_format=None,
+                 protection=None,
+                 builtinId=None,
+                 hidden=False,
+                 ):
+        self.name = name
+        self.font = font or Font()
+        self.fill = fill or PatternFill()
+        self.border = border or Border()
+        self.alignment = alignment or Alignment()
+        self.number_format = number_format
+        self.protection = protection or Protection()
+        self.builtinId = builtinId
+        self.hidden = hidden
+        self._wb = None
+        self._style = StyleArray()
+
+
+    def __setattr__(self, attr, value):
+        super().__setattr__(attr, value)
+        if getattr(self, '_wb', None) and attr in (
+           'font', 'fill', 'border', 'alignment', 'number_format', 'protection',
+            ):
+            self._recalculate()
+
+
+    def __iter__(self):
+        for key in ('name', 'builtinId', 'hidden', 'xfId'):
+            value = getattr(self, key, None)
+            if value is not None:
+                yield key, safe_string(value)
+
+
+    def bind(self, wb):
+        """
+        Bind a named style to a workbook
+        """
+        self._wb = wb
+        self._recalculate()
+
+
+    def _recalculate(self):
+        self._style.fontId =  self._wb._fonts.add(self.font)
+        self._style.borderId = self._wb._borders.add(self.border)
+        self._style.fillId =  self._wb._fills.add(self.fill)
+        self._style.protectionId = self._wb._protections.add(self.protection)
+        self._style.alignmentId = self._wb._alignments.add(self.alignment)
+        fmt = self.number_format
+        if fmt in BUILTIN_FORMATS_REVERSE:
+            fmt = BUILTIN_FORMATS_REVERSE[fmt]
+        else:
+            fmt = self._wb._number_formats.add(self.number_format) + (
+                  BUILTIN_FORMATS_MAX_SIZE)
+        self._style.numFmtId = fmt
+
+
+    def as_tuple(self):
+        """Return a style array representing the current style"""
+        return self._style
+
+
+    def as_xf(self):
+        """
+        Return equivalent XfStyle
+        """
+        xf = CellStyle.from_array(self._style)
+        xf.xfId = None
+        xf.pivotButton = None
+        xf.quotePrefix = None
+        if self.alignment != Alignment():
+            xf.alignment = self.alignment
+        if self.protection != Protection():
+            xf.protection = self.protection
+        return xf
+
+
+    def as_name(self):
+        """
+        Return relevant named style
+
+        """
+        named = _NamedCellStyle(
+            name=self.name,
+            builtinId=self.builtinId,
+            hidden=self.hidden,
+            xfId=self._style.xfId
+        )
+        return named
+
+
+class NamedStyleList(list):
+    """
+    Named styles are editable and can be applied to multiple objects
+
+    As only the index is stored in referencing objects the order mus
+    be preserved.
+
+    Returns a list of NamedStyles
+    """
+
+    def __init__(self, iterable=()):
+        """
+        Allow a list of named styles to be passed in and index them.
+        """
+
+        for idx, s in enumerate(iterable, len(self)):
+            s._style.xfId = idx
+        super().__init__(iterable)
+
+
+    @property
+    def names(self):
+        return [s.name for s in self]
+
+
+    def __getitem__(self, key):
+        if isinstance(key, int):
+            return super().__getitem__(key)
+
+
+        for idx, name in enumerate(self.names):
+            if name == key:
+                return self[idx]
+
+        raise KeyError("No named style with the name{0} exists".format(key))
+
+    def append(self, style):
+        if not isinstance(style, NamedStyle):
+            raise TypeError("""Only NamedStyle instances can be added""")
+        elif style.name in self.names: # hotspot
+            raise ValueError("""Style {0} exists already""".format(style.name))
+        style._style.xfId = (len(self))
+        super().append(style)
+
+
+class _NamedCellStyle(Serialisable):
+
+    """
+    Pointer-based representation of named styles in XML
+    xfId refers to the corresponding CellStyleXfs
+
+    Not used in client code.
+    """
+
+    tagname = "cellStyle"
+
+    name = String()
+    xfId = Integer()
+    builtinId = Integer(allow_none=True)
+    iLevel = Integer(allow_none=True)
+    hidden = Bool(allow_none=True)
+    customBuiltin = Bool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+
+    def __init__(self,
+                 name=None,
+                 xfId=None,
+                 builtinId=None,
+                 iLevel=None,
+                 hidden=None,
+                 customBuiltin=None,
+                 extLst=None,
+                ):
+        self.name = name
+        self.xfId = xfId
+        self.builtinId = builtinId
+        self.iLevel = iLevel
+        self.hidden = hidden
+        self.customBuiltin = customBuiltin
+
+
+class _NamedCellStyleList(Serialisable):
+    """
+    Container for named cell style objects
+
+    Not used in client code
+    """
+
+    tagname = "cellStyles"
+
+    count = Integer(allow_none=True)
+    cellStyle = Sequence(expected_type=_NamedCellStyle)
+
+    __attrs__ = ("count",)
+
+    def __init__(self,
+                 count=None,
+                 cellStyle=(),
+                ):
+        self.cellStyle = cellStyle
+
+
+    @property
+    def count(self):
+        return len(self.cellStyle)
+
+
+    def remove_duplicates(self):
+        """
+        Some applications contain duplicate definitions either by name or
+        referenced style.
+
+        As the references are 0-based indices, styles are sorted by
+        index.
+
+        Returns a list of style references with duplicates removed
+        """
+
+        def sort_fn(v):
+            return v.xfId
+
+        styles = []
+        names = set()
+        ids = set()
+
+        for ns in sorted(self.cellStyle, key=sort_fn):
+            if ns.xfId in ids or ns.name in names: # skip duplicates
+                continue
+            ids.add(ns.xfId)
+            names.add(ns.name)
+
+            styles.append(ns)
+
+        return styles
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/numbers.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/numbers.py
new file mode 100644
index 00000000..b548cc7c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/numbers.py
@@ -0,0 +1,200 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import re
+
+from openpyxl.descriptors import (
+    String,
+    Sequence,
+    Integer,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+BUILTIN_FORMATS = {
+    0: 'General',
+    1: '0',
+    2: '0.00',
+    3: '#,##0',
+    4: '#,##0.00',
+    5: '"$"#,##0_);("$"#,##0)',
+    6: '"$"#,##0_);[Red]("$"#,##0)',
+    7: '"$"#,##0.00_);("$"#,##0.00)',
+    8: '"$"#,##0.00_);[Red]("$"#,##0.00)',
+    9: '0%',
+    10: '0.00%',
+    11: '0.00E+00',
+    12: '# ?/?',
+    13: '# ??/??',
+    14: 'mm-dd-yy',
+    15: 'd-mmm-yy',
+    16: 'd-mmm',
+    17: 'mmm-yy',
+    18: 'h:mm AM/PM',
+    19: 'h:mm:ss AM/PM',
+    20: 'h:mm',
+    21: 'h:mm:ss',
+    22: 'm/d/yy h:mm',
+
+    37: '#,##0_);(#,##0)',
+    38: '#,##0_);[Red](#,##0)',
+    39: '#,##0.00_);(#,##0.00)',
+    40: '#,##0.00_);[Red](#,##0.00)',
+
+    41: r'_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)',
+    42: r'_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)',
+    43: r'_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)',
+
+    44: r'_("$"* #,##0.00_)_("$"* \(#,##0.00\)_("$"* "-"??_)_(@_)',
+    45: 'mm:ss',
+    46: '[h]:mm:ss',
+    47: 'mmss.0',
+    48: '##0.0E+0',
+    49: '@', }
+
+BUILTIN_FORMATS_MAX_SIZE = 164
+BUILTIN_FORMATS_REVERSE = dict(
+        [(value, key) for key, value in BUILTIN_FORMATS.items()])
+
+FORMAT_GENERAL = BUILTIN_FORMATS[0]
+FORMAT_TEXT = BUILTIN_FORMATS[49]
+FORMAT_NUMBER = BUILTIN_FORMATS[1]
+FORMAT_NUMBER_00 = BUILTIN_FORMATS[2]
+FORMAT_NUMBER_COMMA_SEPARATED1 = BUILTIN_FORMATS[4]
+FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-'
+FORMAT_PERCENTAGE = BUILTIN_FORMATS[9]
+FORMAT_PERCENTAGE_00 = BUILTIN_FORMATS[10]
+FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'
+FORMAT_DATE_YYMMDD = 'yy-mm-dd'
+FORMAT_DATE_DDMMYY = 'dd/mm/yy'
+FORMAT_DATE_DMYSLASH = 'd/m/y'
+FORMAT_DATE_DMYMINUS = 'd-m-y'
+FORMAT_DATE_DMMINUS = 'd-m'
+FORMAT_DATE_MYMINUS = 'm-y'
+FORMAT_DATE_XLSX14 = BUILTIN_FORMATS[14]
+FORMAT_DATE_XLSX15 = BUILTIN_FORMATS[15]
+FORMAT_DATE_XLSX16 = BUILTIN_FORMATS[16]
+FORMAT_DATE_XLSX17 = BUILTIN_FORMATS[17]
+FORMAT_DATE_XLSX22 = BUILTIN_FORMATS[22]
+FORMAT_DATE_DATETIME = 'yyyy-mm-dd h:mm:ss'
+FORMAT_DATE_TIME1 = BUILTIN_FORMATS[18]
+FORMAT_DATE_TIME2 = BUILTIN_FORMATS[19]
+FORMAT_DATE_TIME3 = BUILTIN_FORMATS[20]
+FORMAT_DATE_TIME4 = BUILTIN_FORMATS[21]
+FORMAT_DATE_TIME5 = BUILTIN_FORMATS[45]
+FORMAT_DATE_TIME6 = BUILTIN_FORMATS[21]
+FORMAT_DATE_TIME7 = 'i:s.S'
+FORMAT_DATE_TIME8 = 'h:mm:ss@'
+FORMAT_DATE_TIMEDELTA = '[hh]:mm:ss'
+FORMAT_DATE_YYMMDDSLASH = 'yy/mm/dd@'
+FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'
+FORMAT_CURRENCY_USD = '$#,##0_-'
+FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-'
+
+
+COLORS = r"\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\]"
+LITERAL_GROUP = r'".*?"' # anything in quotes
+LOCALE_GROUP = r'\[(?!hh?\]|mm?\]|ss?\])[^\]]*\]' # anything in square brackets, except hours or minutes or seconds
+STRIP_RE = re.compile(f"{LITERAL_GROUP}|{LOCALE_GROUP}")
+TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\](:ss(\.0*)?)?|\[ss?\](\.0*)?', re.I)
+
+
+# Spec 18.8.31 numFmts
+# +ve;-ve;zero;text
+
+def is_date_format(fmt):
+    if fmt is None:
+        return False
+    fmt = fmt.split(";")[0] # only look at the first format
+    fmt = STRIP_RE.sub("", fmt) # ignore some formats
+    return re.search(r"(?<![_\\])[dmhysDMHYS]", fmt) is not None
+
+
+def is_timedelta_format(fmt):
+    if fmt is None:
+        return False
+    fmt = fmt.split(";")[0] # only look at the first format
+    return TIMEDELTA_RE.search(fmt) is not None
+
+
+def is_datetime(fmt):
+    """
+    Return date, time or datetime
+    """
+    if not is_date_format(fmt):
+        return
+
+    DATE = TIME = False
+
+    if any((x in fmt for x in 'dy')):
+        DATE = True
+    if any((x in fmt for x in 'hs')):
+        TIME = True
+
+    if DATE and TIME:
+        return "datetime"
+    if DATE:
+        return "date"
+    return "time"
+
+
+def is_builtin(fmt):
+    return fmt in BUILTIN_FORMATS.values()
+
+
+def builtin_format_code(index):
+    """Return one of the standard format codes by index."""
+    try:
+        fmt = BUILTIN_FORMATS[index]
+    except KeyError:
+        fmt = None
+    return fmt
+
+
+def builtin_format_id(fmt):
+    """Return the id of a standard style."""
+    return BUILTIN_FORMATS_REVERSE.get(fmt)
+
+
+class NumberFormatDescriptor(String):
+
+    def __set__(self, instance, value):
+        if value is None:
+            value = FORMAT_GENERAL
+        super().__set__(instance, value)
+
+
+class NumberFormat(Serialisable):
+
+    numFmtId = Integer()
+    formatCode = String()
+
+    def __init__(self,
+                 numFmtId=None,
+                 formatCode=None,
+                ):
+        self.numFmtId = numFmtId
+        self.formatCode = formatCode
+
+
+class NumberFormatList(Serialisable):
+
+    count = Integer(allow_none=True)
+    numFmt = Sequence(expected_type=NumberFormat)
+
+    __elements__ = ('numFmt',)
+    __attrs__ = ("count",)
+
+    def __init__(self,
+                 count=None,
+                 numFmt=(),
+                ):
+        self.numFmt = numFmt
+
+
+    @property
+    def count(self):
+        return len(self.numFmt)
+
+
+    def __getitem__(self, idx):
+        return self.numFmt[idx]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/protection.py
new file mode 100644
index 00000000..7c9238ce
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/protection.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import Bool
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+class Protection(Serialisable):
+    """Protection options for use in styles."""
+
+    tagname = "protection"
+
+    locked = Bool()
+    hidden = Bool()
+
+    def __init__(self, locked=True, hidden=False):
+        self.locked = locked
+        self.hidden = hidden
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/proxy.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/proxy.py
new file mode 100644
index 00000000..bee780cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/proxy.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from copy import copy
+
+from openpyxl.compat import deprecated
+
+
+class StyleProxy:
+    """
+    Proxy formatting objects so that they cannot be altered
+    """
+
+    __slots__ = ('__target')
+
+    def __init__(self, target):
+        self.__target = target
+
+
+    def __repr__(self):
+        return repr(self.__target)
+
+
+    def __getattr__(self, attr):
+        return getattr(self.__target, attr)
+
+
+    def __setattr__(self, attr, value):
+        if attr != "_StyleProxy__target":
+            raise AttributeError("Style objects are immutable and cannot be changed."
+                                 "Reassign the style with a copy")
+        super().__setattr__(attr, value)
+
+
+    def __copy__(self):
+        """
+        Return a copy of the proxied object.
+        """
+        return copy(self.__target)
+
+
+    def __add__(self, other):
+        """
+        Add proxied object to another instance and return the combined object
+        """
+        return self.__target + other
+
+
+    @deprecated("Use copy(obj) or cell.obj = cell.obj + other")
+    def copy(self, **kw):
+        """Return a copy of the proxied object. Keyword args will be passed through"""
+        cp = copy(self.__target)
+        for k, v in kw.items():
+            setattr(cp, k, v)
+        return cp
+
+
+    def __eq__(self, other):
+        return self.__target == other
+
+
+    def __ne__(self, other):
+        return not self == other
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/styleable.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/styleable.py
new file mode 100644
index 00000000..2703096d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/styleable.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from copy import copy
+
+from .numbers import (
+    BUILTIN_FORMATS,
+    BUILTIN_FORMATS_MAX_SIZE,
+    BUILTIN_FORMATS_REVERSE,
+)
+from .proxy import StyleProxy
+from .cell_style import StyleArray
+from .named_styles import NamedStyle
+from .builtins import styles
+
+
+class StyleDescriptor:
+
+    def __init__(self, collection, key):
+        self.collection = collection
+        self.key = key
+
+    def __set__(self, instance, value):
+        coll = getattr(instance.parent.parent, self.collection)
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        setattr(instance._style, self.key, coll.add(value))
+
+
+    def __get__(self, instance, cls):
+        coll = getattr(instance.parent.parent, self.collection)
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        idx =  getattr(instance._style, self.key)
+        return StyleProxy(coll[idx])
+
+
+class NumberFormatDescriptor:
+
+    key = "numFmtId"
+    collection = '_number_formats'
+
+    def __set__(self, instance, value):
+        coll = getattr(instance.parent.parent, self.collection)
+        if value in BUILTIN_FORMATS_REVERSE:
+            idx = BUILTIN_FORMATS_REVERSE[value]
+        else:
+            idx = coll.add(value) + BUILTIN_FORMATS_MAX_SIZE
+
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        setattr(instance._style, self.key, idx)
+
+
+    def __get__(self, instance, cls):
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        idx = getattr(instance._style, self.key)
+        if idx < BUILTIN_FORMATS_MAX_SIZE:
+            return BUILTIN_FORMATS.get(idx, "General")
+        coll = getattr(instance.parent.parent, self.collection)
+        return coll[idx - BUILTIN_FORMATS_MAX_SIZE]
+
+
+class NamedStyleDescriptor:
+
+    key = "xfId"
+    collection = "_named_styles"
+
+
+    def __set__(self, instance, value):
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        coll = getattr(instance.parent.parent, self.collection)
+        if isinstance(value, NamedStyle):
+            style = value
+            if style not in coll:
+                instance.parent.parent.add_named_style(style)
+        elif value not in coll.names:
+            if value in styles: # is it builtin?
+                style = styles[value]
+                if style not in coll:
+                    instance.parent.parent.add_named_style(style)
+            else:
+                raise ValueError("{0} is not a known style".format(value))
+        else:
+            style = coll[value]
+        instance._style = copy(style.as_tuple())
+
+
+    def __get__(self, instance, cls):
+        if not getattr(instance, "_style"):
+            instance._style = StyleArray()
+        idx = getattr(instance._style, self.key)
+        coll = getattr(instance.parent.parent, self.collection)
+        return coll.names[idx]
+
+
+class StyleArrayDescriptor:
+
+    def __init__(self, key):
+        self.key = key
+
+    def __set__(self, instance, value):
+        if instance._style is None:
+            instance._style = StyleArray()
+        setattr(instance._style, self.key, value)
+
+
+    def __get__(self, instance, cls):
+        if instance._style is None:
+            return False
+        return bool(getattr(instance._style, self.key))
+
+
+class StyleableObject:
+    """
+    Base class for styleble objects implementing proxy and lookup functions
+    """
+
+    font = StyleDescriptor('_fonts', "fontId")
+    fill = StyleDescriptor('_fills', "fillId")
+    border = StyleDescriptor('_borders', "borderId")
+    number_format = NumberFormatDescriptor()
+    protection = StyleDescriptor('_protections', "protectionId")
+    alignment = StyleDescriptor('_alignments', "alignmentId")
+    style = NamedStyleDescriptor()
+    quotePrefix = StyleArrayDescriptor('quotePrefix')
+    pivotButton = StyleArrayDescriptor('pivotButton')
+
+    __slots__ = ('parent', '_style')
+
+    def __init__(self, sheet, style_array=None):
+        self.parent = sheet
+        if style_array is not None:
+            style_array = StyleArray(style_array)
+        self._style = style_array
+
+
+    @property
+    def style_id(self):
+        if self._style is None:
+            self._style = StyleArray()
+        return self.parent.parent._cell_styles.add(self._style)
+
+
+    @property
+    def has_style(self):
+        if self._style is None:
+            return False
+        return any(self._style)
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/stylesheet.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/stylesheet.py
new file mode 100644
index 00000000..dfaf875d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/stylesheet.py
@@ -0,0 +1,274 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from warnings import warn
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+)
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.utils.indexed_list import IndexedList
+from openpyxl.xml.constants import ARC_STYLE, SHEET_MAIN_NS
+from openpyxl.xml.functions import fromstring
+
+from .builtins import styles
+from .colors import ColorList
+from .differential import DifferentialStyle
+from .table import TableStyleList
+from .borders import Border
+from .fills import Fill
+from .fonts import Font
+from .numbers import (
+    NumberFormatList,
+    BUILTIN_FORMATS,
+    BUILTIN_FORMATS_MAX_SIZE,
+    BUILTIN_FORMATS_REVERSE,
+    is_date_format,
+    is_timedelta_format,
+    builtin_format_code
+)
+from .named_styles import (
+    _NamedCellStyleList,
+    NamedStyleList,
+    NamedStyle,
+)
+from .cell_style import CellStyle, CellStyleList
+
+
+class Stylesheet(Serialisable):
+
+    tagname = "styleSheet"
+
+    numFmts = Typed(expected_type=NumberFormatList)
+    fonts = NestedSequence(expected_type=Font, count=True)
+    fills = NestedSequence(expected_type=Fill, count=True)
+    borders = NestedSequence(expected_type=Border, count=True)
+    cellStyleXfs = Typed(expected_type=CellStyleList)
+    cellXfs = Typed(expected_type=CellStyleList)
+    cellStyles = Typed(expected_type=_NamedCellStyleList)
+    dxfs = NestedSequence(expected_type=DifferentialStyle, count=True)
+    tableStyles = Typed(expected_type=TableStyleList, allow_none=True)
+    colors = Typed(expected_type=ColorList, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('numFmts', 'fonts', 'fills', 'borders', 'cellStyleXfs',
+                    'cellXfs', 'cellStyles', 'dxfs', 'tableStyles', 'colors')
+
+    def __init__(self,
+                 numFmts=None,
+                 fonts=(),
+                 fills=(),
+                 borders=(),
+                 cellStyleXfs=None,
+                 cellXfs=None,
+                 cellStyles=None,
+                 dxfs=(),
+                 tableStyles=None,
+                 colors=None,
+                 extLst=None,
+                ):
+        if numFmts is None:
+            numFmts = NumberFormatList()
+        self.numFmts = numFmts
+        self.number_formats = IndexedList()
+        self.fonts = fonts
+        self.fills = fills
+        self.borders = borders
+        if cellStyleXfs is None:
+            cellStyleXfs = CellStyleList()
+        self.cellStyleXfs = cellStyleXfs
+        if cellXfs is None:
+            cellXfs = CellStyleList()
+        self.cellXfs = cellXfs
+        if cellStyles is None:
+            cellStyles = _NamedCellStyleList()
+        self.cellStyles = cellStyles
+
+        self.dxfs = dxfs
+        self.tableStyles = tableStyles
+        self.colors = colors
+
+        self.cell_styles = self.cellXfs._to_array()
+        self.alignments = self.cellXfs.alignments
+        self.protections = self.cellXfs.prots
+        self._normalise_numbers()
+        self.named_styles = self._merge_named_styles()
+
+
+    @classmethod
+    def from_tree(cls, node):
+        # strip all attribs
+        attrs = dict(node.attrib)
+        for k in attrs:
+            del node.attrib[k]
+        return super().from_tree(node)
+
+
+    def _merge_named_styles(self):
+        """
+        Merge named style names "cellStyles" with their associated styles
+        "cellStyleXfs"
+        """
+        style_refs = self.cellStyles.remove_duplicates()
+        from_ref = [self._expand_named_style(style_ref) for style_ref in style_refs]
+
+        return NamedStyleList(from_ref)
+
+
+    def _expand_named_style(self, style_ref):
+        """
+        Expand a named style reference element to a
+        named style object by binding the relevant
+        objects from the stylesheet
+        """
+        xf = self.cellStyleXfs[style_ref.xfId]
+        named_style = NamedStyle(
+            name=style_ref.name,
+            hidden=style_ref.hidden,
+            builtinId=style_ref.builtinId,
+        )
+
+        named_style.font = self.fonts[xf.fontId]
+        named_style.fill = self.fills[xf.fillId]
+        named_style.border = self.borders[xf.borderId]
+        if xf.numFmtId < BUILTIN_FORMATS_MAX_SIZE:
+            formats = BUILTIN_FORMATS
+        else:
+            formats = self.custom_formats
+
+        if xf.numFmtId in formats:
+            named_style.number_format = formats[xf.numFmtId]
+        if xf.alignment:
+            named_style.alignment = xf.alignment
+        if xf.protection:
+            named_style.protection = xf.protection
+
+        return named_style
+
+
+    def _split_named_styles(self, wb):
+        """
+        Convert NamedStyle into separate CellStyle and Xf objects
+
+        """
+        for  style in wb._named_styles:
+            self.cellStyles.cellStyle.append(style.as_name())
+            self.cellStyleXfs.xf.append(style.as_xf())
+
+
+    @property
+    def custom_formats(self):
+        return dict([(n.numFmtId, n.formatCode) for n in self.numFmts.numFmt])
+
+
+    def _normalise_numbers(self):
+        """
+        Rebase custom numFmtIds with a floor of 164 when reading stylesheet
+        And index datetime formats
+        """
+        date_formats = set()
+        timedelta_formats = set()
+        custom = self.custom_formats
+        formats = self.number_formats
+        for idx, style in enumerate(self.cell_styles):
+            if style.numFmtId in custom:
+                fmt = custom[style.numFmtId]
+                if fmt in BUILTIN_FORMATS_REVERSE: # remove builtins
+                    style.numFmtId = BUILTIN_FORMATS_REVERSE[fmt]
+                else:
+                    style.numFmtId = formats.add(fmt) + BUILTIN_FORMATS_MAX_SIZE
+            else:
+                fmt = builtin_format_code(style.numFmtId)
+            if is_date_format(fmt):
+                # Create an index of which styles refer to datetimes
+                date_formats.add(idx)
+            if is_timedelta_format(fmt):
+                # Create an index of which styles refer to timedeltas
+                timedelta_formats.add(idx)
+        self.date_formats = date_formats
+        self.timedelta_formats = timedelta_formats
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        tree = super().to_tree(tagname, idx, namespace)
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+def apply_stylesheet(archive, wb):
+    """
+    Add styles to workbook if present
+    """
+    try:
+        src = archive.read(ARC_STYLE)
+    except KeyError:
+        return wb
+
+    node = fromstring(src)
+    stylesheet = Stylesheet.from_tree(node)
+
+    if stylesheet.cell_styles:
+
+        wb._borders = IndexedList(stylesheet.borders)
+        wb._fonts = IndexedList(stylesheet.fonts)
+        wb._fills = IndexedList(stylesheet.fills)
+        wb._differential_styles.styles = stylesheet.dxfs
+        wb._number_formats = stylesheet.number_formats
+        wb._protections = stylesheet.protections
+        wb._alignments = stylesheet.alignments
+        wb._table_styles = stylesheet.tableStyles
+
+        # need to overwrite openpyxl defaults in case workbook has different ones
+        wb._cell_styles = stylesheet.cell_styles
+        wb._named_styles = stylesheet.named_styles
+        wb._date_formats = stylesheet.date_formats
+        wb._timedelta_formats = stylesheet.timedelta_formats
+
+        for ns in wb._named_styles:
+            ns.bind(wb)
+
+    else:
+        warn("Workbook contains no stylesheet, using openpyxl's defaults")
+
+    if not wb._named_styles:
+        normal = styles['Normal']
+        wb.add_named_style(normal)
+        warn("Workbook contains no default style, apply openpyxl's default")
+
+    if stylesheet.colors is not None:
+        wb._colors = stylesheet.colors.index
+
+
+def write_stylesheet(wb):
+    stylesheet = Stylesheet()
+    stylesheet.fonts = wb._fonts
+    stylesheet.fills = wb._fills
+    stylesheet.borders = wb._borders
+    stylesheet.dxfs = wb._differential_styles.styles
+    stylesheet.colors = ColorList(indexedColors=wb._colors)
+
+    from .numbers import NumberFormat
+    fmts = []
+    for idx, code in enumerate(wb._number_formats, BUILTIN_FORMATS_MAX_SIZE):
+        fmt = NumberFormat(idx, code)
+        fmts.append(fmt)
+
+    stylesheet.numFmts.numFmt = fmts
+
+    xfs = []
+    for style in wb._cell_styles:
+        xf = CellStyle.from_array(style)
+
+        if style.alignmentId:
+            xf.alignment = wb._alignments[style.alignmentId]
+
+        if style.protectionId:
+            xf.protection = wb._protections[style.protectionId]
+        xfs.append(xf)
+    stylesheet.cellXfs = CellStyleList(xf=xfs)
+
+    stylesheet._split_named_styles(wb)
+    stylesheet.tableStyles = wb._table_styles
+
+    return stylesheet.to_tree()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/styles/table.py b/.venv/lib/python3.12/site-packages/openpyxl/styles/table.py
new file mode 100644
index 00000000..18307198
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/styles/table.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Float,
+    Bool,
+    Set,
+    Integer,
+    NoneSet,
+    String,
+    Sequence
+)
+
+from .colors import Color
+
+
+class TableStyleElement(Serialisable):
+
+    tagname = "tableStyleElement"
+
+    type = Set(values=(['wholeTable', 'headerRow', 'totalRow', 'firstColumn',
+                        'lastColumn', 'firstRowStripe', 'secondRowStripe', 'firstColumnStripe',
+                        'secondColumnStripe', 'firstHeaderCell', 'lastHeaderCell',
+                        'firstTotalCell', 'lastTotalCell', 'firstSubtotalColumn',
+                        'secondSubtotalColumn', 'thirdSubtotalColumn', 'firstSubtotalRow',
+                        'secondSubtotalRow', 'thirdSubtotalRow', 'blankRow',
+                        'firstColumnSubheading', 'secondColumnSubheading',
+                        'thirdColumnSubheading', 'firstRowSubheading', 'secondRowSubheading',
+                        'thirdRowSubheading', 'pageFieldLabels', 'pageFieldValues']))
+    size = Integer(allow_none=True)
+    dxfId = Integer(allow_none=True)
+
+    def __init__(self,
+                 type=None,
+                 size=None,
+                 dxfId=None,
+                ):
+        self.type = type
+        self.size = size
+        self.dxfId = dxfId
+
+
+class TableStyle(Serialisable):
+
+    tagname = "tableStyle"
+
+    name = String()
+    pivot = Bool(allow_none=True)
+    table = Bool(allow_none=True)
+    count = Integer(allow_none=True)
+    tableStyleElement = Sequence(expected_type=TableStyleElement, allow_none=True)
+
+    __elements__ = ('tableStyleElement',)
+
+    def __init__(self,
+                 name=None,
+                 pivot=None,
+                 table=None,
+                 count=None,
+                 tableStyleElement=(),
+                ):
+        self.name = name
+        self.pivot = pivot
+        self.table = table
+        self.count = count
+        self.tableStyleElement = tableStyleElement
+
+
+class TableStyleList(Serialisable):
+
+    tagname = "tableStyles"
+
+    defaultTableStyle = String(allow_none=True)
+    defaultPivotStyle = String(allow_none=True)
+    tableStyle = Sequence(expected_type=TableStyle, allow_none=True)
+
+    __elements__ = ('tableStyle',)
+    __attrs__ = ("count", "defaultTableStyle", "defaultPivotStyle")
+
+    def __init__(self,
+                 count=None,
+                 defaultTableStyle="TableStyleMedium9",
+                 defaultPivotStyle="PivotStyleLight16",
+                 tableStyle=(),
+                ):
+        self.defaultTableStyle = defaultTableStyle
+        self.defaultPivotStyle = defaultPivotStyle
+        self.tableStyle = tableStyle
+
+
+    @property
+    def count(self):
+        return len(self.tableStyle)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
new file mode 100644
index 00000000..f6132636
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/__init__.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from .cell import (
+    absolute_coordinate,
+    cols_from_range,
+    column_index_from_string,
+    coordinate_to_tuple,
+    get_column_letter,
+    get_column_interval,
+    quote_sheetname,
+    range_boundaries,
+    range_to_tuple,
+    rows_from_range,
+)
+
+from .formulas import FORMULAE
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py
new file mode 100644
index 00000000..20cbd1c4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/bound_dictionary.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from collections import defaultdict
+
+
+class BoundDictionary(defaultdict):
+    """
+    A default dictionary where elements are tightly coupled.
+
+    The factory method is responsible for binding the parent object to the child.
+
+    If a reference attribute is assigned then child objects will have the key assigned to this.
+
+    Otherwise it's just a defaultdict.
+    """
+
+    def __init__(self, reference=None, *args, **kw):
+        self.reference = reference
+        super().__init__(*args, **kw)
+
+
+    def __getitem__(self, key):
+        value = super().__getitem__(key)
+        if self.reference is not None:
+            setattr(value, self.reference, key)
+        return value
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
new file mode 100644
index 00000000..f1ccc7d2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/cell.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Collection of utilities used within the package and also available for client code
+"""
+from functools import lru_cache
+from itertools import chain, product
+from string import ascii_uppercase, digits
+import re
+
+from .exceptions import CellCoordinatesException
+
+# constants
+COORD_RE = re.compile(r'^[$]?([A-Za-z]{1,3})[$]?(\d+)$')
+COL_RANGE = """[A-Z]{1,3}:[A-Z]{1,3}:"""
+ROW_RANGE = r"""\d+:\d+:"""
+RANGE_EXPR = r"""
+[$]?(?P<min_col>[A-Za-z]{1,3})?
+[$]?(?P<min_row>\d+)?
+(:[$]?(?P<max_col>[A-Za-z]{1,3})?
+[$]?(?P<max_row>\d+)?)?
+"""
+ABSOLUTE_RE = re.compile('^' + RANGE_EXPR +'$', re.VERBOSE)
+SHEET_TITLE = r"""
+(('(?P<quoted>([^']|'')*)')|(?P<notquoted>[^'^ ^!]*))!"""
+SHEETRANGE_RE = re.compile("""{0}(?P<cells>{1})(?=,?)""".format(
+    SHEET_TITLE, RANGE_EXPR), re.VERBOSE)
+
+
+def get_column_interval(start, end):
+    """
+    Given the start and end columns, return all the columns in the series.
+
+    The start and end columns can be either column letters or 1-based
+    indexes.
+    """
+    if isinstance(start, str):
+        start = column_index_from_string(start)
+    if isinstance(end, str):
+        end = column_index_from_string(end)
+    return [get_column_letter(x) for x in range(start, end + 1)]
+
+
+def coordinate_from_string(coord_string):
+    """Convert a coordinate string like 'B12' to a tuple ('B', 12)"""
+    match = COORD_RE.match(coord_string)
+    if not match:
+        msg = f"Invalid cell coordinates ({coord_string})"
+        raise CellCoordinatesException(msg)
+    column, row = match.groups()
+    row = int(row)
+    if not row:
+        msg = f"There is no row 0 ({coord_string})"
+        raise CellCoordinatesException(msg)
+    return column, row
+
+
+def absolute_coordinate(coord_string):
+    """Convert a coordinate to an absolute coordinate string (B12 -> $B$12)"""
+    m = ABSOLUTE_RE.match(coord_string)
+    if not m:
+        raise ValueError(f"{coord_string} is not a valid coordinate range")
+
+    d = m.groupdict('')
+    for k, v in d.items():
+        if v:
+            d[k] = f"${v}"
+
+    if d['max_col'] or d['max_row']:
+        fmt = "{min_col}{min_row}:{max_col}{max_row}"
+    else:
+        fmt = "{min_col}{min_row}"
+    return fmt.format(**d)
+
+
+__decimal_to_alpha = [""] + list(ascii_uppercase)
+
+@lru_cache(maxsize=None)
+def get_column_letter(col_idx):
+    """
+    Convert decimal column position to its ASCII (base 26) form.
+
+    Because column indices are 1-based, strides are actually pow(26, n) + 26
+    Hence, a correction is applied between pow(26, n) and pow(26, 2) + 26 to
+    prevent and additional column letter being prepended
+
+    "A" == 1 == pow(26, 0)
+    "Z" == 26 == pow(26, 0) + 26 // decimal equivalent 10
+    "AA" == 27 == pow(26, 1) + 1
+    "ZZ" == 702 == pow(26, 2) + 26 // decimal equivalent 100
+    """
+
+    if not 1 <= col_idx <= 18278:
+        raise ValueError("Invalid column index {0}".format(col_idx))
+
+    result = []
+
+    if col_idx < 26:
+        return __decimal_to_alpha[col_idx]
+
+    while col_idx:
+        col_idx, remainder = divmod(col_idx, 26)
+        result.insert(0, __decimal_to_alpha[remainder])
+        if not remainder:
+            col_idx -= 1
+            result.insert(0, "Z")
+
+    return "".join(result)
+
+
+__alpha_to_decimal = {letter:pos for pos, letter in enumerate(ascii_uppercase, 1)}
+__powers = (1, 26, 676)
+
+@lru_cache(maxsize=None)
+def column_index_from_string(col):
+    """
+    Convert ASCII column name (base 26) to decimal with 1-based index
+
+    Characters represent descending multiples of powers of 26
+
+    "AFZ" == 26 * pow(26, 0) + 6 * pow(26, 1) + 1 * pow(26, 2)
+    """
+    error_msg = f"'{col}' is not a valid column name. Column names are from A to ZZZ"
+    if len(col) > 3:
+        raise ValueError(error_msg)
+    idx = 0
+    col = reversed(col.upper())
+    for letter, power in zip(col, __powers):
+        try:
+            pos = __alpha_to_decimal[letter]
+        except KeyError:
+            raise ValueError(error_msg)
+        idx += pos * power
+    if not 0 < idx < 18279:
+        raise ValueError(error_msg)
+    return idx
+
+
+def range_boundaries(range_string):
+    """
+    Convert a range string into a tuple of boundaries:
+    (min_col, min_row, max_col, max_row)
+    Cell coordinates will be converted into a range with the cell at both end
+    """
+    msg = "{0} is not a valid coordinate or range".format(range_string)
+    m = ABSOLUTE_RE.match(range_string)
+    if not m:
+        raise ValueError(msg)
+
+    min_col, min_row, sep, max_col, max_row = m.groups()
+
+    if sep:
+        cols = min_col, max_col
+        rows = min_row, max_row
+
+        if not (
+            all(cols + rows) or
+            all(cols) and not any(rows) or
+            all(rows) and not any(cols)
+        ):
+            raise ValueError(msg)
+
+    if min_col is not None:
+        min_col = column_index_from_string(min_col)
+
+    if min_row is not None:
+        min_row = int(min_row)
+
+    if max_col is not None:
+        max_col = column_index_from_string(max_col)
+    else:
+        max_col = min_col
+
+    if max_row is not None:
+        max_row = int(max_row)
+    else:
+        max_row = min_row
+
+    return min_col, min_row, max_col, max_row
+
+
+def rows_from_range(range_string):
+    """
+    Get individual addresses for every cell in a range.
+    Yields one row at a time.
+    """
+    min_col, min_row, max_col, max_row = range_boundaries(range_string)
+    rows = range(min_row, max_row + 1)
+    cols = [get_column_letter(col) for col in range(min_col, max_col + 1)]
+    for row in rows:
+        yield tuple('{0}{1}'.format(col, row) for col in cols)
+
+
+def cols_from_range(range_string):
+    """
+    Get individual addresses for every cell in a range.
+    Yields one row at a time.
+    """
+    min_col, min_row, max_col, max_row = range_boundaries(range_string)
+    rows = range(min_row, max_row+1)
+    cols = (get_column_letter(col) for col in range(min_col, max_col+1))
+    for col in cols:
+        yield tuple('{0}{1}'.format(col, row) for row in rows)
+
+
+def coordinate_to_tuple(coordinate):
+    """
+    Convert an Excel style coordinate to (row, column) tuple
+    """
+    for idx, c in enumerate(coordinate):
+        if c in digits:
+            break
+    col = coordinate[:idx]
+    row = coordinate[idx:]
+    return int(row), column_index_from_string(col)
+
+
+def range_to_tuple(range_string):
+    """
+    Convert a worksheet range to the sheetname and maximum and minimum
+    coordinate indices
+    """
+    m = SHEETRANGE_RE.match(range_string)
+    if m is None:
+        raise ValueError("Value must be of the form sheetname!A1:E4")
+    sheetname = m.group("quoted") or m.group("notquoted")
+    cells = m.group("cells")
+    boundaries = range_boundaries(cells)
+    return sheetname, boundaries
+
+
+def quote_sheetname(sheetname):
+    """
+    Add quotes around sheetnames if they contain spaces.
+    """
+    if "'" in sheetname:
+        sheetname = sheetname.replace("'", "''")
+
+    sheetname = u"'{0}'".format(sheetname)
+    return sheetname
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py
new file mode 100644
index 00000000..f56a4887
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/dataframe.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from itertools import accumulate
+import operator
+import numpy
+from openpyxl.compat.product import prod
+
+
+def dataframe_to_rows(df, index=True, header=True):
+    """
+    Convert a Pandas dataframe into something suitable for passing into a worksheet.
+    If index is True then the index will be included, starting one row below the header.
+    If header is True then column headers will be included starting one column to the right.
+    Formatting should be done by client code.
+    """
+    from pandas import Timestamp
+
+    if header:
+        if df.columns.nlevels > 1:
+            rows = expand_index(df.columns, header)
+        else:
+            rows = [list(df.columns.values)]
+        for row in rows:
+            n = []
+            for v in row:
+                if isinstance(v, numpy.datetime64):
+                    v = Timestamp(v)
+                n.append(v)
+            row = n
+            if index:
+                row = [None]*df.index.nlevels + row
+            yield row
+
+    if index:
+        yield df.index.names
+
+    expanded = ([v] for v in df.index)
+    if df.index.nlevels > 1:
+        expanded = expand_index(df.index)
+
+    # Using the expanded index is preferable to df.itertuples(index=True) so that we have 'None' inserted where applicable
+    for (df_index, row) in zip(expanded, df.itertuples(index=False)):
+        row = list(row)
+        if index:
+            row = df_index + row
+        yield row
+
+
+def expand_index(index, header=False):
+    """
+    Expand axis or column Multiindex
+    For columns use header = True
+    For axes use header = False (default)
+    """
+
+    # For each element of the index, zip the members with the previous row
+    # If the 2 elements of the zipped list do not match, we can insert the new value into the row
+    # or if an earlier member was different, all later members should be added to the row
+    values = list(index.values)
+    previous_value = [None] * len(values[0])
+    result = []
+
+    for value in values:
+        row = [None] * len(value)
+
+        # Once there's a difference in member of an index with the prior index, we need to store all subsequent members in the row
+        prior_change = False
+        for idx, (current_index_member, previous_index_member) in enumerate(zip(value, previous_value)):
+
+            if current_index_member != previous_index_member or prior_change:
+                row[idx] = current_index_member
+                prior_change = True
+
+        previous_value = value
+
+        # If this is for a row index, we're already returning a row so just yield
+        if not header:
+            yield row
+        else:
+            result.append(row)
+
+    # If it's for a header, we need to transpose to get it in row order
+    # Example: result = [['A', 'A'], [None, 'B']] -> [['A', None], ['A', 'B']]
+    if header:
+        result = numpy.array(result).transpose().tolist()
+        for row in result:
+            yield row
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
new file mode 100644
index 00000000..bf7e5006
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/datetime.py
@@ -0,0 +1,140 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Manage Excel date weirdness."""
+
+# Python stdlib imports
+import datetime
+from math import isnan
+import re
+
+
+# constants
+MAC_EPOCH = datetime.datetime(1904, 1, 1)
+WINDOWS_EPOCH = datetime.datetime(1899, 12, 30)
+CALENDAR_WINDOWS_1900 = 2415018.5   # Julian date of WINDOWS_EPOCH
+CALENDAR_MAC_1904 = 2416480.5       # Julian date of MAC_EPOCH
+CALENDAR_WINDOWS_1900 = WINDOWS_EPOCH
+CALENDAR_MAC_1904 = MAC_EPOCH
+SECS_PER_DAY = 86400
+
+ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+ISO_REGEX = re.compile(r'''
+(?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
+(?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
+                                       re.VERBOSE)
+ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')
+
+
+def to_ISO8601(dt):
+    """Convert from a datetime to a timestamp string."""
+    if hasattr(dt, "microsecond") and dt.microsecond:
+        return dt.isoformat(timespec="milliseconds")
+    return dt.isoformat()
+
+
+def from_ISO8601(formatted_string):
+    """Convert from a timestamp string to a datetime object. According to
+    18.17.4 in the specification the following ISO 8601 formats are
+    supported.
+
+    Dates B.1.1 and B.2.1
+    Times B.1.2 and B.2.2
+    Datetimes B.1.3 and B.2.3
+
+    There is no concept of timedeltas in the specification, but Excel
+    writes them (in strict OOXML mode), so these are also understood.
+    """
+    if not formatted_string:
+        return None
+
+    match = ISO_REGEX.match(formatted_string)
+    if match and any(match.groups()):
+        parts = match.groupdict(0)
+        for key in ["year", "month", "day", "hour", "minute", "second"]:
+            if parts[key]:
+                parts[key] = int(parts[key])
+
+        if parts["microsecond"]:
+            parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
+
+        if not parts["date"]:
+            dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
+        elif not parts["time"]:
+            dt = datetime.date(parts['year'], parts['month'], parts['day'])
+        else:
+            del parts["time"]
+            del parts["date"]
+            dt = datetime.datetime(**parts)
+        return dt
+
+    match = ISO_DURATION.match(formatted_string)
+    if match and any(match.groups()):
+        parts = match.groupdict(0)
+        for key, val in parts.items():
+            if val:
+                parts[key] = float(val)
+        return datetime.timedelta(**parts)
+
+    raise ValueError("Invalid datetime value {}".format(formatted_string))
+
+
+def to_excel(dt, epoch=WINDOWS_EPOCH):
+    """Convert Python datetime to Excel serial"""
+    if isinstance(dt, datetime.time):
+        return time_to_days(dt)
+    if isinstance(dt, datetime.timedelta):
+        return timedelta_to_days(dt)
+    if isnan(dt.year):  # Pandas supports Not a Date
+        return
+
+    if not hasattr(dt, "date"):
+        dt = datetime.datetime.combine(dt, datetime.time())
+
+    # rebase on epoch and adjust for < 1900-03-01
+    days = (dt - epoch).days
+    if 0 < days <= 60 and epoch == WINDOWS_EPOCH:
+        days -= 1
+    return days + time_to_days(dt)
+
+
+def from_excel(value, epoch=WINDOWS_EPOCH, timedelta=False):
+    """Convert Excel serial to Python datetime"""
+    if value is None:
+        return
+
+    if timedelta:
+        td = datetime.timedelta(days=value)
+        if td.microseconds:
+            # round to millisecond precision
+            td = datetime.timedelta(seconds=td.total_seconds() // 1,
+                                    microseconds=round(td.microseconds, -3))
+        return td
+
+    day, fraction = divmod(value, 1)
+    diff = datetime.timedelta(milliseconds=round(fraction * SECS_PER_DAY * 1000))
+    if 0 <= value < 1 and diff.days == 0:
+        return days_to_time(diff)
+    if 0 < value < 60 and epoch == WINDOWS_EPOCH:
+        day += 1
+    return epoch + datetime.timedelta(days=day) + diff
+
+
+def time_to_days(value):
+    """Convert a time value to fractions of day"""
+    return (
+        (value.hour * 3600)
+        + (value.minute * 60)
+        + value.second
+        + value.microsecond / 10**6
+        ) / SECS_PER_DAY
+
+
+def timedelta_to_days(value):
+    """Convert a timedelta value to fractions of a day"""
+    return value.total_seconds() / SECS_PER_DAY
+
+
+def days_to_time(value):
+    mins, seconds = divmod(value.seconds, 60)
+    hours, mins = divmod(mins, 60)
+    return datetime.time(hours, mins, seconds, value.microseconds)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
new file mode 100644
index 00000000..a8985343
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/escape.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+OOXML has non-standard escaping for characters < \031
+"""
+
+import re
+
+
+def escape(value):
+    r"""
+    Convert ASCII < 31 to OOXML: \n == _x + hex(ord(\n)) + _
+    """
+
+    CHAR_REGEX = re.compile(r"[\001-\031]")
+
+    def _sub(match):
+        """
+        Callback to escape chars
+        """
+        return "_x{:0>4x}_".format(ord(match.group(0)))
+
+    return CHAR_REGEX.sub(_sub, value)
+
+
+def unescape(value):
+    r"""
+    Convert escaped strings to ASCIII: _x000a_ == \n
+    """
+
+
+    ESCAPED_REGEX = re.compile("_x([0-9A-Fa-f]{4})_")
+
+    def _sub(match):
+        """
+        Callback to unescape chars
+        """
+        return chr(int(match.group(1), 16))
+
+    if "_x" in value:
+        value = ESCAPED_REGEX.sub(_sub, value)
+
+    return value
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py
new file mode 100644
index 00000000..7b05f742
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/exceptions.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""Definitions for openpyxl shared exception classes."""
+
+
+class CellCoordinatesException(Exception):
+    """Error for converting between numeric and A1-style cell references."""
+
+
+class IllegalCharacterError(Exception):
+    """The data submitted which cannot be used directly in Excel files. It
+    must be removed or escaped."""
+
+
+class NamedRangeException(Exception):
+    """Error for badly formatted named ranges."""
+
+
+class SheetTitleException(Exception):
+    """Error for bad sheet names."""
+
+
+class InvalidFileException(Exception):
+    """Error for trying to open a non-ooxml file."""
+
+
+class ReadOnlyWorkbookException(Exception):
+    """Error for trying to modify a read-only workbook"""
+
+
+class WorkbookAlreadySaved(Exception):
+    """Error when attempting to perform operations on a dump workbook
+    while it has already been dumped once"""
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
new file mode 100644
index 00000000..aab9961b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/formulas.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+List of builtin formulae
+"""
+
+FORMULAE = ("CUBEKPIMEMBER", "CUBEMEMBER", "CUBEMEMBERPROPERTY", "CUBERANKEDMEMBER", "CUBESET", "CUBESETCOUNT", "CUBEVALUE", "DAVERAGE", "DCOUNT", "DCOUNTA", "DGET", "DMAX", "DMIN", "DPRODUCT", "DSTDEV", "DSTDEVP", "DSUM", "DVAR", "DVARP", "DATE", "DATEDIF", "DATEVALUE", "DAY", "DAYS360", "EDATE", "EOMONTH", "HOUR", "MINUTE", "MONTH", "NETWORKDAYS", "NETWORKDAYS.INTL", "NOW", "SECOND", "TIME", "TIMEVALUE", "TODAY", "WEEKDAY", "WEEKNUM", "WORKDAY", "WORKDAY.INTL", "YEAR", "YEARFRAC", "BESSELI", "BESSELJ", "BESSELK", "BESSELY", "BIN2DEC", "BIN2HEX", "BIN2OCT", "COMPLEX", "CONVERT", "DEC2BIN", "DEC2HEX", "DEC2OCT", "DELTA", "ERF", "ERFC", "GESTEP", "HEX2BIN", "HEX2DEC", "HEX2OCT", "IMABS", "IMAGINARY", "IMARGUMENT", "IMCONJUGATE", "IMCOS", "IMDIV", "IMEXP", "IMLN", "IMLOG10", "IMLOG2", "IMPOWER", "IMPRODUCT", "IMREAL", "IMSIN", "IMSQRT", "IMSUB", "IMSUM", "OCT2BIN", "OCT2DEC", "OCT2HEX", "ACCRINT", "ACCRINTM", "AMORDEGRC", "AMORLINC", "COUPDAYBS", "COUPDAYS", "COUPDAYSNC", "COUPNCD", "COUPNUM", "COUPPCD", "CUMIPMT", "CUMPRINC", "DB", "DDB", "DISC", "DOLLARDE", "DOLLARFR", "DURATION", "EFFECT", "FV", "FVSCHEDULE", "INTRATE", "IPMT", "IRR", "ISPMT", "MDURATION", "MIRR", "NOMINAL", "NPER", "NPV", "ODDFPRICE", "ODDFYIELD", "ODDLPRICE", "ODDLYIELD", "PMT", "PPMT", "PRICE", "PRICEDISC", "PRICEMAT", "PV", "RATE", "RECEIVED", "SLN", "SYD", "TBILLEQ", "TBILLPRICE", "TBILLYIELD", "VDB", "XIRR", "XNPV", "YIELD", "YIELDDISC", "YIELDMAT", "CELL", "ERROR.TYPE", "INFO", "ISBLANK", "ISERR", "ISERROR", "ISEVEN", "ISLOGICAL", "ISNA", "ISNONTEXT", "ISNUMBER", "ISODD", "ISREF", "ISTEXT", "N", "NA", "TYPE", "AND", "FALSE", "IF", "IFERROR", "NOT", "OR", "TRUE", "ADDRESS", "AREAS", "CHOOSE", "COLUMN", "COLUMNS", "GETPIVOTDATA", "HLOOKUP", "HYPERLINK", "INDEX", "INDIRECT", "LOOKUP", "MATCH", "OFFSET", "ROW", "ROWS", "RTD", "TRANSPOSE", "VLOOKUP", "ABS", "ACOS", "ACOSH", "ASIN", "ASINH", "ATAN", "ATAN2", "ATANH", "CEILING", "COMBIN", "COS", "COSH", "DEGREES", "ECMA.CEILING", "EVEN", "EXP", "FACT", "FACTDOUBLE", "FLOOR", "GCD", "INT", "ISO.CEILING", "LCM", "LN", "LOG", "LOG10", "MDETERM", "MINVERSE", "MMULT", "MOD", "MROUND", "MULTINOMIAL", "ODD", "PI", "POWER", "PRODUCT", "QUOTIENT", "RADIANS", "RAND", "RANDBETWEEN", "ROMAN", "ROUND", "ROUNDDOWN", "ROUNDUP", "SERIESSUM", "SIGN", "SIN", "SINH", "SQRT", "SQRTPI", "SUBTOTAL", "SUM", "SUMIF", "SUMIFS", "SUMPRODUCT", "SUMSQ", "SUMX2MY2", "SUMX2PY2", "SUMXMY2", "TAN", "TANH", "TRUNC", "AVEDEV", "AVERAGE", "AVERAGEA", "AVERAGEIF", "AVERAGEIFS", "BETADIST", "BETAINV", "BINOMDIST", "CHIDIST", "CHIINV", "CHITEST", "CONFIDENCE", "CORREL", "COUNT", "COUNTA", "COUNTBLANK", "COUNTIF", "COUNTIFS", "COVAR", "CRITBINOM", "DEVSQ", "EXPONDIST", "FDIST", "FINV", "FISHER", "FISHERINV", "FORECAST", "FREQUENCY", "FTEST", "GAMMADIST", "GAMMAINV", "GAMMALN", "GEOMEAN", "GROWTH", "HARMEAN", "HYPGEOMDIST", "INTERCEPT", "KURT", "LARGE", "LINEST", "LOGEST", "LOGINV", "LOGNORMDIST", "MAX", "MAXA", "MEDIAN", "MIN", "MINA", "MODE", "NEGBINOMDIST", "NORMDIST", "NORMINV", "NORMSDIST", "NORMSINV", "PEARSON", "PERCENTILE", "PERCENTRANK", "PERMUT", "POISSON", "PROB", "QUARTILE", "RANK", "RSQ", "SKEW", "SLOPE", "SMALL", "STANDARDIZE", "STDEV", "STDEVA", "STDEVP", "STDEVPA", "STEYX", "TDIST", "TINV", "TREND", "TRIMMEAN", "TTEST", "VAR", "VARA", "VARP", "VARPA", "WEIBULL", "ZTEST", "ASC", "BAHTTEXT", "CHAR", "CLEAN", "CODE", "CONCATENATE", "DOLLAR", "EXACT", "FIND", "FINDB", "FIXED", "JIS", "LEFT", "LEFTB", "LEN", "LENB", "LOWER", "MID", "MIDB", "PHONETIC", "PROPER", "REPLACE", "REPLACEB", "REPT", "RIGHT", "RIGHTB", "SEARCH", "SEARCHB", "SUBSTITUTE", "T", "TEXT", "TRIM", "UPPER", "VALUE")
+
+FORMULAE = frozenset(FORMULAE)
+
+
+from openpyxl.formula import Tokenizer
+
+
+def validate(formula):
+    """
+    Utility function for checking whether a formula is syntactically correct
+    """
+    assert formula.startswith("=")
+    formula = Tokenizer(formula)
+    for t in formula.items:
+        if t.type == "FUNC" and t.subtype == "OPEN":
+            if not t.value.startswith("_xlfn.") and t.value[:-1] not in FORMULAE:
+                raise ValueError(f"Unknown function {t.value} in {formula.formula}. The function may need a prefix")
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py
new file mode 100644
index 00000000..753acf09
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/indexed_list.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+class IndexedList(list):
+    """
+    List with optimised access by value
+    Based on Alex Martelli's recipe
+
+    http://code.activestate.com/recipes/52303-the-auxiliary-dictionary-idiom-for-sequences-with-/
+    """
+
+    _dict = {}
+
+    def __init__(self, iterable=None):
+        self.clean = True
+        self._dict = {}
+        if iterable is not None:
+            self.clean = False
+            for idx, val in enumerate(iterable):
+                self._dict[val] = idx
+                list.append(self, val)
+
+    def _rebuild_dict(self):
+        self._dict = {}
+        idx = 0
+        for value in self:
+            if value not in self._dict:
+                self._dict[value] = idx
+                idx += 1
+        self.clean = True
+
+    def __contains__(self, value):
+        if not self.clean:
+            self._rebuild_dict()
+        return value in self._dict
+
+    def index(self, value):
+        if value in self:
+            return self._dict[value]
+        raise ValueError
+
+    def append(self, value):
+        if value not in self._dict:
+            self._dict[value] = len(self)
+            list.append(self, value)
+
+    def add(self, value):
+        self.append(value)
+        return self._dict[value]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py
new file mode 100644
index 00000000..aff02a2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/inference.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+Type inference functions
+"""
+import datetime
+import re
+
+from openpyxl.styles import numbers
+
+PERCENT_REGEX = re.compile(r'^(?P<number>\-?[0-9]*\.?[0-9]*\s?)\%$')
+TIME_REGEX = re.compile(r"""
+^(?: # HH:MM and HH:MM:SS
+(?P<hour>[0-1]{0,1}[0-9]{2}):
+(?P<minute>[0-5][0-9]):?
+(?P<second>[0-5][0-9])?$)
+|
+^(?: # MM:SS.
+([0-5][0-9]):
+([0-5][0-9])?\.
+(?P<microsecond>\d{1,6}))
+""", re.VERBOSE)
+NUMBER_REGEX = re.compile(r'^-?([\d]|[\d]+\.[\d]*|\.[\d]+|[1-9][\d]+\.?[\d]*)((E|e)[-+]?[\d]+)?$')
+
+
+def cast_numeric(value):
+    """Explicitly convert a string to a numeric value"""
+    if NUMBER_REGEX.match(value):
+        try:
+            return int(value)
+        except ValueError:
+            return float(value)
+
+
+def cast_percentage(value):
+    """Explicitly convert a string to numeric value and format as a
+    percentage"""
+    match = PERCENT_REGEX.match(value)
+    if match:
+        return float(match.group('number')) / 100
+
+
+
+def cast_time(value):
+    """Explicitly convert a string to a number and format as datetime or
+    time"""
+    match = TIME_REGEX.match(value)
+    if match:
+        if match.group("microsecond") is not None:
+            value = value[:12]
+            pattern = "%M:%S.%f"
+            #fmt = numbers.FORMAT_DATE_TIME5
+        elif match.group('second') is None:
+            #fmt = numbers.FORMAT_DATE_TIME3
+            pattern = "%H:%M"
+        else:
+            pattern = "%H:%M:%S"
+            #fmt = numbers.FORMAT_DATE_TIME6
+        value = datetime.datetime.strptime(value, pattern)
+        return value.time()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py
new file mode 100644
index 00000000..cc7707ee
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/protection.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+def hash_password(plaintext_password=''):
+    """
+    Create a password hash from a given string for protecting a worksheet
+    only. This will not work for encrypting a workbook.
+
+    This method is based on the algorithm provided by
+    Daniel Rentz of OpenOffice and the PEAR package
+    Spreadsheet_Excel_Writer by Xavier Noguer <xnoguer@rezebra.com>.
+    See also http://blogs.msdn.com/b/ericwhite/archive/2008/02/23/the-legacy-hashing-algorithm-in-open-xml.aspx
+    """
+    password = 0x0000
+    for idx, char in enumerate(plaintext_password, 1):
+        value = ord(char) << idx
+        rotated_bits = value >> 15
+        value &= 0x7fff
+        password ^= (value | rotated_bits)
+    password ^= len(plaintext_password)
+    password ^= 0xCE4B
+    return str(hex(password)).upper()[2:]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py b/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py
new file mode 100644
index 00000000..19f23c5b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/utils/units.py
@@ -0,0 +1,108 @@
+
+# Copyright (c) 2010-2024 openpyxl
+
+import math
+
+
+#constants
+
+DEFAULT_ROW_HEIGHT = 15.  # Default row height measured in point size.
+BASE_COL_WIDTH = 8 # in characters
+DEFAULT_COLUMN_WIDTH = BASE_COL_WIDTH + 5
+#  = baseColumnWidth + {margin padding (2 pixels on each side, totalling 4 pixels)} + {gridline (1pixel)}
+
+
+DEFAULT_LEFT_MARGIN = 0.7 # in inches, = right margin
+DEFAULT_TOP_MARGIN = 0.7874 # in inches = bottom margin
+DEFAULT_HEADER = 0.3 # in inches
+
+
+# Conversion functions
+"""
+From the ECMA Spec (4th Edition part 1)
+Page setup: "Left Page Margin in inches" p. 1647
+
+Docs from
+http://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
+
+See also http://msdn.microsoft.com/en-us/library/dd560821(v=office.12).aspx
+
+dxa: The main unit in OOXML is a twentieth of a point. Also called twips.
+pt: point. In Excel there are 72 points to an inch
+hp: half-points are used to specify font sizes. A font-size of 12pt equals 24 half points
+pct: Half-points are used to specify font sizes. A font-size of 12pt equals 24 half points
+
+EMU: English Metric Unit, EMUs are used for coordinates in vector-based
+drawings and embedded pictures. One inch equates to 914400 EMUs and a
+centimeter is 360000. For bitmaps the default resolution is 96 dpi (known as
+PixelsPerInch in Excel). Spec p. 1122
+
+For radial geometry Excel uses integer units of 1/60000th of a degree.
+"""
+
+
+
+def inch_to_dxa(value):
+    """1 inch = 72 * 20 dxa"""
+    return int(value * 20 * 72)
+
+def dxa_to_inch(value):
+    return value / 72 / 20
+
+
+def dxa_to_cm(value):
+    return 2.54 * dxa_to_inch(value)
+
+def cm_to_dxa(value):
+    emu = cm_to_EMU(value)
+    inch = EMU_to_inch(emu)
+    return inch_to_dxa(inch)
+
+
+def pixels_to_EMU(value):
+    """1 pixel = 9525 EMUs"""
+    return int(value * 9525)
+
+def EMU_to_pixels(value):
+    return round(value / 9525)
+
+
+def cm_to_EMU(value):
+    """1 cm = 360000 EMUs"""
+    return int(value * 360000)
+
+def EMU_to_cm(value):
+    return round(value / 360000, 4)
+
+
+def inch_to_EMU(value):
+    """1 inch = 914400 EMUs"""
+    return int(value * 914400)
+
+def EMU_to_inch(value):
+    return round(value / 914400, 4)
+
+
+def pixels_to_points(value, dpi=96):
+    """96 dpi, 72i"""
+    return value * 72 / dpi
+
+
+def points_to_pixels(value, dpi=96):
+    return int(math.ceil(value * dpi / 72))
+
+
+def degrees_to_angle(value):
+    """1 degree = 60000 angles"""
+    return int(round(value * 60000))
+
+
+def angle_to_degrees(value):
+    return round(value / 60000, 2)
+
+
+def short_color(color):
+    """ format a color to its short size """
+    if len(color) > 6:
+        return color[2:]
+    return color
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/__init__.py
new file mode 100644
index 00000000..8ae4d80d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/__init__.py
@@ -0,0 +1,4 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from .workbook import Workbook
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/_writer.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/_writer.py
new file mode 100644
index 00000000..1aa6aacf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/_writer.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Write the workbook global settings to the archive."""
+
+from openpyxl.utils import quote_sheetname
+from openpyxl.xml.constants import (
+    ARC_APP,
+    ARC_CORE,
+    ARC_CUSTOM,
+    ARC_WORKBOOK,
+    PKG_REL_NS,
+    CUSTOMUI_NS,
+    ARC_ROOT_RELS,
+)
+from openpyxl.xml.functions import tostring, fromstring
+
+from openpyxl.packaging.relationship import Relationship, RelationshipList
+from openpyxl.workbook.defined_name import (
+    DefinedName,
+    DefinedNameList,
+)
+from openpyxl.workbook.external_reference import ExternalReference
+from openpyxl.packaging.workbook import ChildSheet, WorkbookPackage, PivotCache
+from openpyxl.workbook.properties import WorkbookProperties
+from openpyxl.utils.datetime import CALENDAR_MAC_1904
+
+
+def get_active_sheet(wb):
+    """
+    Return the index of the active sheet.
+    If the sheet set to active is hidden return the next visible sheet or None
+    """
+    visible_sheets = [idx for idx, sheet in enumerate(wb._sheets) if sheet.sheet_state == "visible"]
+    if not visible_sheets:
+        raise IndexError("At least one sheet must be visible")
+
+    idx = wb._active_sheet_index
+    sheet = wb.active
+    if sheet and sheet.sheet_state == "visible":
+        return idx
+
+    for idx in visible_sheets[idx:]:
+        wb.active = idx
+        return idx
+
+    return None
+
+
+class WorkbookWriter:
+
+    def __init__(self, wb):
+        self.wb = wb
+        self.rels = RelationshipList()
+        self.package = WorkbookPackage()
+        self.package.workbookProtection = wb.security
+        self.package.calcPr = wb.calculation
+
+
+    def write_properties(self):
+
+        props = WorkbookProperties() # needs a mapping to the workbook for preservation
+        if self.wb.code_name is not None:
+            props.codeName = self.wb.code_name
+        if self.wb.excel_base_date == CALENDAR_MAC_1904:
+            props.date1904 = True
+        self.package.workbookPr = props
+
+
+    def write_worksheets(self):
+        for idx, sheet in enumerate(self.wb._sheets, 1):
+            sheet_node = ChildSheet(name=sheet.title, sheetId=idx, id="rId{0}".format(idx))
+            rel = Relationship(type=sheet._rel_type, Target=sheet.path)
+            self.rels.append(rel)
+
+            if not sheet.sheet_state == 'visible':
+                if len(self.wb._sheets) == 1:
+                    raise ValueError("The only worksheet of a workbook cannot be hidden")
+                sheet_node.state = sheet.sheet_state
+            self.package.sheets.append(sheet_node)
+
+
+    def write_refs(self):
+        for link in self.wb._external_links:
+            # need to match a counter with a workbook's relations
+            rId = len(self.wb.rels) + 1
+            rel = Relationship(type=link._rel_type, Target=link.path)
+            self.rels.append(rel)
+            ext = ExternalReference(id=rel.id)
+            self.package.externalReferences.append(ext)
+
+
+    def write_names(self):
+        defined_names = list(self.wb.defined_names.values())
+
+        for idx, sheet in enumerate(self.wb.worksheets):
+            quoted = quote_sheetname(sheet.title)
+
+            # local names
+            if sheet.defined_names:
+                names = sheet.defined_names.values()
+                for n in names:
+                    n.localSheetId = idx
+                defined_names.extend(names)
+
+            if sheet.auto_filter:
+                name = DefinedName(name='_FilterDatabase', localSheetId=idx, hidden=True)
+                name.value = f"{quoted}!{sheet.auto_filter}"
+                defined_names.append(name)
+
+            if sheet.print_titles:
+                name = DefinedName(name="Print_Titles", localSheetId=idx)
+                name.value = sheet.print_titles
+                defined_names.append(name)
+
+            if sheet.print_area:
+                name = DefinedName(name="Print_Area", localSheetId=idx)
+                name.value = sheet.print_area
+                defined_names.append(name)
+
+        self.package.definedNames = DefinedNameList(definedName=defined_names)
+
+
+    def write_pivots(self):
+        pivot_caches = set()
+        for pivot in self.wb._pivots:
+            if pivot.cache not in pivot_caches:
+                pivot_caches.add(pivot.cache)
+                c = PivotCache(cacheId=pivot.cacheId)
+                self.package.pivotCaches.append(c)
+                rel = Relationship(Type=pivot.cache.rel_type, Target=pivot.cache.path)
+                self.rels.append(rel)
+                c.id = rel.id
+        #self.wb._pivots = [] # reset
+
+
+    def write_views(self):
+        active = get_active_sheet(self.wb)
+        if self.wb.views:
+            self.wb.views[0].activeTab = active
+        self.package.bookViews = self.wb.views
+
+
+    def write(self):
+        """Write the core workbook xml."""
+
+        self.write_properties()
+        self.write_worksheets()
+        self.write_names()
+        self.write_pivots()
+        self.write_views()
+        self.write_refs()
+
+        return tostring(self.package.to_tree())
+
+
+    def write_rels(self):
+        """Write the workbook relationships xml."""
+
+        styles =  Relationship(type='styles', Target='styles.xml')
+        self.rels.append(styles)
+
+        theme =  Relationship(type='theme', Target='theme/theme1.xml')
+        self.rels.append(theme)
+
+        if self.wb.vba_archive:
+            vba =  Relationship(type='', Target='vbaProject.bin')
+            vba.Type ='http://schemas.microsoft.com/office/2006/relationships/vbaProject'
+            self.rels.append(vba)
+
+        return tostring(self.rels.to_tree())
+
+
+    def write_root_rels(self):
+        """Write the package relationships"""
+
+        rels = RelationshipList()
+
+        rel = Relationship(type="officeDocument", Target=ARC_WORKBOOK)
+        rels.append(rel)
+        rel = Relationship(Type=f"{PKG_REL_NS}/metadata/core-properties", Target=ARC_CORE)
+        rels.append(rel)
+
+        rel = Relationship(type="extended-properties", Target=ARC_APP)
+        rels.append(rel)
+
+        if len(self.wb.custom_doc_props) >= 1:
+            rel = Relationship(type="custom-properties", Target=ARC_CUSTOM)
+            rels.append(rel)
+
+        if self.wb.vba_archive is not None:
+            # See if there was a customUI relation and reuse it
+            xml = fromstring(self.wb.vba_archive.read(ARC_ROOT_RELS))
+            root_rels = RelationshipList.from_tree(xml)
+            for rel in root_rels.find(CUSTOMUI_NS):
+                rels.append(rel)
+
+        return tostring(rels.to_tree())
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/child.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/child.py
new file mode 100644
index 00000000..19dd29fb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/child.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import re
+import warnings
+
+from openpyxl.worksheet.header_footer import HeaderFooter
+
+"""
+Base class for worksheets, chartsheets, etc. that can be added to workbooks
+"""
+
+INVALID_TITLE_REGEX = re.compile(r'[\\*?:/\[\]]')
+
+
+def avoid_duplicate_name(names, value):
+    """
+    Naive check to see whether name already exists.
+    If name does exist suggest a name using an incrementer
+    Duplicates are case insensitive
+    """
+    # Check for an absolute match in which case we need to find an alternative
+    match = [n for n in names if n.lower() == value.lower()]
+    if match:
+        names = u",".join(names)
+        sheet_title_regex = re.compile(f'(?P<title>{re.escape(value)})(?P<count>\\d*),?', re.I)
+        matches = sheet_title_regex.findall(names)
+        if matches:
+            # use name, but append with the next highest integer
+            counts = [int(idx) for (t, idx) in matches if idx.isdigit()]
+            highest = 0
+            if counts:
+                highest = max(counts)
+            value = u"{0}{1}".format(value, highest + 1)
+    return value
+
+
+class _WorkbookChild:
+
+    __title = ""
+    _id = None
+    _path = "{0}"
+    _parent = None
+    _default_title = "Sheet"
+
+    def __init__(self, parent=None, title=None):
+        self._parent = parent
+        self.title = title or self._default_title
+        self.HeaderFooter = HeaderFooter()
+
+
+    def __repr__(self):
+        return '<{0} "{1}">'.format(self.__class__.__name__, self.title)
+
+
+    @property
+    def parent(self):
+        return self._parent
+
+
+    @property
+    def encoding(self):
+        return self._parent.encoding
+
+
+    @property
+    def title(self):
+        return self.__title
+
+
+    @title.setter
+    def title(self, value):
+        """
+        Set a sheet title, ensuring it is valid.
+        Limited to 31 characters, no special characters.
+        Duplicate titles will be incremented numerically
+        """
+        if not self._parent:
+            return
+
+        if not value:
+            raise ValueError("Title must have at least one character")
+
+        if hasattr(value, "decode"):
+            if not isinstance(value, str):
+                try:
+                    value = value.decode("ascii")
+                except UnicodeDecodeError:
+                    raise ValueError("Worksheet titles must be str")
+
+        m = INVALID_TITLE_REGEX.search(value)
+        if m:
+            msg = "Invalid character {0} found in sheet title".format(m.group(0))
+            raise ValueError(msg)
+
+        if self.title is not None and self.title != value:
+            value = avoid_duplicate_name(self.parent.sheetnames, value)
+
+        if len(value) > 31:
+            warnings.warn("Title is more than 31 characters. Some applications may not be able to read the file")
+
+        self.__title = value
+
+
+    @property
+    def oddHeader(self):
+        return self.HeaderFooter.oddHeader
+
+
+    @oddHeader.setter
+    def oddHeader(self, value):
+        self.HeaderFooter.oddHeader = value
+
+
+    @property
+    def oddFooter(self):
+        return self.HeaderFooter.oddFooter
+
+
+    @oddFooter.setter
+    def oddFooter(self, value):
+        self.HeaderFooter.oddFooter = value
+
+
+    @property
+    def evenHeader(self):
+        return self.HeaderFooter.evenHeader
+
+
+    @evenHeader.setter
+    def evenHeader(self, value):
+        self.HeaderFooter.evenHeader = value
+
+
+    @property
+    def evenFooter(self):
+        return self.HeaderFooter.evenFooter
+
+
+    @evenFooter.setter
+    def evenFooter(self, value):
+        self.HeaderFooter.evenFooter = value
+
+
+    @property
+    def firstHeader(self):
+        return self.HeaderFooter.firstHeader
+
+
+    @firstHeader.setter
+    def firstHeader(self, value):
+        self.HeaderFooter.firstHeader = value
+
+
+    @property
+    def firstFooter(self):
+        return self.HeaderFooter.firstFooter
+
+
+    @firstFooter.setter
+    def firstFooter(self, value):
+        self.HeaderFooter.firstFooter = value
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/defined_name.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/defined_name.py
new file mode 100644
index 00000000..15f0bd30
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/defined_name.py
@@ -0,0 +1,189 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from collections import defaultdict
+import re
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    String,
+    Integer,
+    Bool,
+    Sequence,
+    Descriptor,
+)
+from openpyxl.compat import safe_string
+from openpyxl.formula import Tokenizer
+from openpyxl.utils.cell import SHEETRANGE_RE
+
+RESERVED = frozenset(["Print_Area", "Print_Titles", "Criteria",
+                      "_FilterDatabase", "Extract", "Consolidate_Area",
+                      "Sheet_Title"])
+
+_names = "|".join(RESERVED)
+RESERVED_REGEX = re.compile(r"^_xlnm\.(?P<name>{0})".format(_names))
+
+
+class DefinedName(Serialisable):
+
+    tagname = "definedName"
+
+    name = String() # unique per workbook/worksheet
+    comment = String(allow_none=True)
+    customMenu = String(allow_none=True)
+    description = String(allow_none=True)
+    help = String(allow_none=True)
+    statusBar = String(allow_none=True)
+    localSheetId = Integer(allow_none=True)
+    hidden = Bool(allow_none=True)
+    function = Bool(allow_none=True)
+    vbProcedure = Bool(allow_none=True)
+    xlm = Bool(allow_none=True)
+    functionGroupId = Integer(allow_none=True)
+    shortcutKey = String(allow_none=True)
+    publishToServer = Bool(allow_none=True)
+    workbookParameter = Bool(allow_none=True)
+    attr_text = Descriptor()
+    value = Alias("attr_text")
+
+
+    def __init__(self,
+                 name=None,
+                 comment=None,
+                 customMenu=None,
+                 description=None,
+                 help=None,
+                 statusBar=None,
+                 localSheetId=None,
+                 hidden=None,
+                 function=None,
+                 vbProcedure=None,
+                 xlm=None,
+                 functionGroupId=None,
+                 shortcutKey=None,
+                 publishToServer=None,
+                 workbookParameter=None,
+                 attr_text=None
+                ):
+        self.name = name
+        self.comment = comment
+        self.customMenu = customMenu
+        self.description = description
+        self.help = help
+        self.statusBar = statusBar
+        self.localSheetId = localSheetId
+        self.hidden = hidden
+        self.function = function
+        self.vbProcedure = vbProcedure
+        self.xlm = xlm
+        self.functionGroupId = functionGroupId
+        self.shortcutKey = shortcutKey
+        self.publishToServer = publishToServer
+        self.workbookParameter = workbookParameter
+        self.attr_text = attr_text
+
+
+    @property
+    def type(self):
+        tok = Tokenizer("=" + self.value)
+        parsed = tok.items[0]
+        if parsed.type == "OPERAND":
+            return parsed.subtype
+        return parsed.type
+
+
+    @property
+    def destinations(self):
+        if self.type == "RANGE":
+            tok = Tokenizer("=" + self.value)
+            for part in tok.items:
+                if part.subtype == "RANGE":
+                    m = SHEETRANGE_RE.match(part.value)
+                    sheetname = m.group('notquoted') or m.group('quoted')
+                    yield sheetname, m.group('cells')
+
+
+    @property
+    def is_reserved(self):
+        m = RESERVED_REGEX.match(self.name)
+        if m:
+            return m.group("name")
+
+
+    @property
+    def is_external(self):
+        return re.compile(r"^\[\d+\].*").match(self.value) is not None
+
+
+    def __iter__(self):
+        for key in self.__attrs__:
+            if key == "attr_text":
+                continue
+            v = getattr(self, key)
+            if v is not None:
+                if v in RESERVED:
+                    v = "_xlnm." + v
+                yield key, safe_string(v)
+
+
+class DefinedNameDict(dict):
+
+    """
+    Utility class for storing defined names.
+    Allows access by name and separation of global and scoped names
+    """
+
+    def __setitem__(self, key, value):
+        if not isinstance(value, DefinedName):
+            raise TypeError("Value must be a an instance of DefinedName")
+        elif value.name != key:
+            raise ValueError("Key must be the same as the name")
+        super().__setitem__(key, value)
+
+
+    def add(self, value):
+        """
+        Add names without worrying about key and name matching.
+        """
+        self[value.name] = value
+
+
+class DefinedNameList(Serialisable):
+
+    tagname = "definedNames"
+
+    definedName = Sequence(expected_type=DefinedName)
+
+
+    def __init__(self, definedName=()):
+        self.definedName = definedName
+
+
+    def by_sheet(self):
+        """
+        Break names down into sheet locals and globals
+        """
+        names = defaultdict(DefinedNameDict)
+        for defn in self.definedName:
+            if defn.localSheetId is None:
+                if defn.name in ("_xlnm.Print_Titles", "_xlnm.Print_Area", "_xlnm._FilterDatabase"):
+                    continue
+                names["global"][defn.name] = defn
+            else:
+                sheet = int(defn.localSheetId)
+                names[sheet][defn.name] = defn
+        return names
+
+
+    def _duplicate(self, defn):
+        """
+        Check for whether DefinedName with the same name and scope already
+        exists
+        """
+        for d in self.definedName:
+            if d.name == defn.name and d.localSheetId == defn.localSheetId:
+                return True
+
+
+    def __len__(self):
+        return len(self.definedName)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/__init__.py
new file mode 100644
index 00000000..c3cb6211
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from .external import ExternalLink
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/external.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/external.py
new file mode 100644
index 00000000..7e2e5b20
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_link/external.py
@@ -0,0 +1,190 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Bool,
+    Integer,
+    NoneSet,
+    Sequence,
+)
+from openpyxl.descriptors.excel import Relation
+from openpyxl.descriptors.nested import NestedText
+from openpyxl.descriptors.sequence import NestedSequence, ValueSequence
+
+from openpyxl.packaging.relationship import (
+    Relationship,
+    get_rels_path,
+    get_dependents
+    )
+from openpyxl.xml.constants import SHEET_MAIN_NS
+from openpyxl.xml.functions import fromstring
+
+
+"""Manage links to external Workbooks"""
+
+
+class ExternalCell(Serialisable):
+
+    r = String()
+    t = NoneSet(values=(['b', 'd', 'n', 'e', 's', 'str', 'inlineStr']))
+    vm = Integer(allow_none=True)
+    v = NestedText(allow_none=True, expected_type=str)
+
+    def __init__(self,
+                 r=None,
+                 t=None,
+                 vm=None,
+                 v=None,
+                ):
+        self.r = r
+        self.t = t
+        self.vm = vm
+        self.v = v
+
+
+class ExternalRow(Serialisable):
+
+    r = Integer()
+    cell = Sequence(expected_type=ExternalCell)
+
+    __elements__ = ('cell',)
+
+    def __init__(self,
+                 r=(),
+                 cell=None,
+                ):
+        self.r = r
+        self.cell = cell
+
+
+class ExternalSheetData(Serialisable):
+
+    sheetId = Integer()
+    refreshError = Bool(allow_none=True)
+    row = Sequence(expected_type=ExternalRow)
+
+    __elements__ = ('row',)
+
+    def __init__(self,
+                 sheetId=None,
+                 refreshError=None,
+                 row=(),
+                ):
+        self.sheetId = sheetId
+        self.refreshError = refreshError
+        self.row = row
+
+
+class ExternalSheetDataSet(Serialisable):
+
+    sheetData = Sequence(expected_type=ExternalSheetData, )
+
+    __elements__ = ('sheetData',)
+
+    def __init__(self,
+                 sheetData=None,
+                ):
+        self.sheetData = sheetData
+
+
+class ExternalSheetNames(Serialisable):
+
+    sheetName = ValueSequence(expected_type=str)
+
+    __elements__ = ('sheetName',)
+
+    def __init__(self,
+                 sheetName=(),
+                ):
+        self.sheetName = sheetName
+
+
+class ExternalDefinedName(Serialisable):
+
+    tagname = "definedName"
+
+    name = String()
+    refersTo = String(allow_none=True)
+    sheetId = Integer(allow_none=True)
+
+    def __init__(self,
+                 name=None,
+                 refersTo=None,
+                 sheetId=None,
+                ):
+        self.name = name
+        self.refersTo = refersTo
+        self.sheetId = sheetId
+
+
+class ExternalBook(Serialisable):
+
+    tagname = "externalBook"
+
+    sheetNames = Typed(expected_type=ExternalSheetNames, allow_none=True)
+    definedNames = NestedSequence(expected_type=ExternalDefinedName)
+    sheetDataSet = Typed(expected_type=ExternalSheetDataSet, allow_none=True)
+    id = Relation()
+
+    __elements__ = ('sheetNames', 'definedNames', 'sheetDataSet')
+
+    def __init__(self,
+                 sheetNames=None,
+                 definedNames=(),
+                 sheetDataSet=None,
+                 id=None,
+                ):
+        self.sheetNames = sheetNames
+        self.definedNames = definedNames
+        self.sheetDataSet = sheetDataSet
+        self.id = id
+
+
+class ExternalLink(Serialisable):
+
+    tagname = "externalLink"
+
+    _id = None
+    _path = "/xl/externalLinks/externalLink{0}.xml"
+    _rel_type = "externalLink"
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"
+
+    externalBook = Typed(expected_type=ExternalBook, allow_none=True)
+    file_link = Typed(expected_type=Relationship, allow_none=True) # link to external file
+
+    __elements__ = ('externalBook', )
+
+    def __init__(self,
+                 externalBook=None,
+                 ddeLink=None,
+                 oleLink=None,
+                 extLst=None,
+                ):
+        self.externalBook = externalBook
+        # ignore other items for the moment.
+
+
+    def to_tree(self):
+        node = super().to_tree()
+        node.set("xmlns", SHEET_MAIN_NS)
+        return node
+
+
+    @property
+    def path(self):
+        return self._path.format(self._id)
+
+
+def read_external_link(archive, book_path):
+    src = archive.read(book_path)
+    node = fromstring(src)
+    book = ExternalLink.from_tree(node)
+
+    link_path = get_rels_path(book_path)
+    deps = get_dependents(archive, link_path)
+    book.file_link = deps[0]
+
+    return book
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_reference.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_reference.py
new file mode 100644
index 00000000..f05802da
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/external_reference.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence
+)
+from openpyxl.descriptors.excel import (
+    Relation,
+)
+
+class ExternalReference(Serialisable):
+
+    tagname = "externalReference"
+
+    id = Relation()
+
+    def __init__(self, id):
+        self.id = id
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/function_group.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/function_group.py
new file mode 100644
index 00000000..5d7e8557
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/function_group.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    String,
+    Integer,
+)
+
+class FunctionGroup(Serialisable):
+
+    tagname = "functionGroup"
+
+    name = String()
+
+    def __init__(self,
+                 name=None,
+                ):
+        self.name = name
+
+
+class FunctionGroupList(Serialisable):
+
+    tagname = "functionGroups"
+
+    builtInGroupCount = Integer(allow_none=True)
+    functionGroup = Sequence(expected_type=FunctionGroup, allow_none=True)
+
+    __elements__ = ('functionGroup',)
+
+    def __init__(self,
+                 builtInGroupCount=16,
+                 functionGroup=(),
+                ):
+        self.builtInGroupCount = builtInGroupCount
+        self.functionGroup = functionGroup
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/properties.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/properties.py
new file mode 100644
index 00000000..bdc9d614
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/properties.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    String,
+    Float,
+    Integer,
+    Bool,
+    NoneSet,
+    Set,
+)
+
+from openpyxl.descriptors.excel import Guid
+
+
+class WorkbookProperties(Serialisable):
+
+    tagname = "workbookPr"
+
+    date1904 = Bool(allow_none=True)
+    dateCompatibility = Bool(allow_none=True)
+    showObjects = NoneSet(values=(['all', 'placeholders']))
+    showBorderUnselectedTables = Bool(allow_none=True)
+    filterPrivacy = Bool(allow_none=True)
+    promptedSolutions = Bool(allow_none=True)
+    showInkAnnotation = Bool(allow_none=True)
+    backupFile = Bool(allow_none=True)
+    saveExternalLinkValues = Bool(allow_none=True)
+    updateLinks = NoneSet(values=(['userSet', 'never', 'always']))
+    codeName = String(allow_none=True)
+    hidePivotFieldList = Bool(allow_none=True)
+    showPivotChartFilter = Bool(allow_none=True)
+    allowRefreshQuery = Bool(allow_none=True)
+    publishItems = Bool(allow_none=True)
+    checkCompatibility = Bool(allow_none=True)
+    autoCompressPictures = Bool(allow_none=True)
+    refreshAllConnections = Bool(allow_none=True)
+    defaultThemeVersion = Integer(allow_none=True)
+
+    def __init__(self,
+                 date1904=None,
+                 dateCompatibility=None,
+                 showObjects=None,
+                 showBorderUnselectedTables=None,
+                 filterPrivacy=None,
+                 promptedSolutions=None,
+                 showInkAnnotation=None,
+                 backupFile=None,
+                 saveExternalLinkValues=None,
+                 updateLinks=None,
+                 codeName=None,
+                 hidePivotFieldList=None,
+                 showPivotChartFilter=None,
+                 allowRefreshQuery=None,
+                 publishItems=None,
+                 checkCompatibility=None,
+                 autoCompressPictures=None,
+                 refreshAllConnections=None,
+                 defaultThemeVersion=None,
+                ):
+        self.date1904 = date1904
+        self.dateCompatibility = dateCompatibility
+        self.showObjects = showObjects
+        self.showBorderUnselectedTables = showBorderUnselectedTables
+        self.filterPrivacy = filterPrivacy
+        self.promptedSolutions = promptedSolutions
+        self.showInkAnnotation = showInkAnnotation
+        self.backupFile = backupFile
+        self.saveExternalLinkValues = saveExternalLinkValues
+        self.updateLinks = updateLinks
+        self.codeName = codeName
+        self.hidePivotFieldList = hidePivotFieldList
+        self.showPivotChartFilter = showPivotChartFilter
+        self.allowRefreshQuery = allowRefreshQuery
+        self.publishItems = publishItems
+        self.checkCompatibility = checkCompatibility
+        self.autoCompressPictures = autoCompressPictures
+        self.refreshAllConnections = refreshAllConnections
+        self.defaultThemeVersion = defaultThemeVersion
+
+
+class CalcProperties(Serialisable):
+
+    tagname = "calcPr"
+
+    calcId = Integer()
+    calcMode = NoneSet(values=(['manual', 'auto', 'autoNoTable']))
+    fullCalcOnLoad = Bool(allow_none=True)
+    refMode = NoneSet(values=(['A1', 'R1C1']))
+    iterate = Bool(allow_none=True)
+    iterateCount = Integer(allow_none=True)
+    iterateDelta = Float(allow_none=True)
+    fullPrecision = Bool(allow_none=True)
+    calcCompleted = Bool(allow_none=True)
+    calcOnSave = Bool(allow_none=True)
+    concurrentCalc = Bool(allow_none=True)
+    concurrentManualCount = Integer(allow_none=True)
+    forceFullCalc = Bool(allow_none=True)
+
+    def __init__(self,
+                 calcId=124519,
+                 calcMode=None,
+                 fullCalcOnLoad=True,
+                 refMode=None,
+                 iterate=None,
+                 iterateCount=None,
+                 iterateDelta=None,
+                 fullPrecision=None,
+                 calcCompleted=None,
+                 calcOnSave=None,
+                 concurrentCalc=None,
+                 concurrentManualCount=None,
+                 forceFullCalc=None,
+                ):
+        self.calcId = calcId
+        self.calcMode = calcMode
+        self.fullCalcOnLoad = fullCalcOnLoad
+        self.refMode = refMode
+        self.iterate = iterate
+        self.iterateCount = iterateCount
+        self.iterateDelta = iterateDelta
+        self.fullPrecision = fullPrecision
+        self.calcCompleted = calcCompleted
+        self.calcOnSave = calcOnSave
+        self.concurrentCalc = concurrentCalc
+        self.concurrentManualCount = concurrentManualCount
+        self.forceFullCalc = forceFullCalc
+
+
+class FileVersion(Serialisable):
+
+    tagname = "fileVersion"
+
+    appName = String(allow_none=True)
+    lastEdited = String(allow_none=True)
+    lowestEdited = String(allow_none=True)
+    rupBuild = String(allow_none=True)
+    codeName = Guid(allow_none=True)
+
+    def __init__(self,
+                 appName=None,
+                 lastEdited=None,
+                 lowestEdited=None,
+                 rupBuild=None,
+                 codeName=None,
+                ):
+        self.appName = appName
+        self.lastEdited = lastEdited
+        self.lowestEdited = lowestEdited
+        self.rupBuild = rupBuild
+        self.codeName = codeName
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/protection.py
new file mode 100644
index 00000000..d77d64bb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/protection.py
@@ -0,0 +1,163 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    String,
+    Float,
+    Integer,
+    Bool,
+    NoneSet,
+    Set,
+)
+from openpyxl.descriptors.excel import (
+    ExtensionList,
+    HexBinary,
+    Guid,
+    Relation,
+    Base64Binary,
+)
+from openpyxl.utils.protection import hash_password
+
+
+class WorkbookProtection(Serialisable):
+
+    _workbook_password, _revisions_password = None, None
+
+    tagname = "workbookPr"
+
+    workbook_password = Alias("workbookPassword")
+    workbookPasswordCharacterSet = String(allow_none=True)
+    revision_password = Alias("revisionsPassword")
+    revisionsPasswordCharacterSet = String(allow_none=True)
+    lockStructure = Bool(allow_none=True)
+    lock_structure = Alias("lockStructure")
+    lockWindows = Bool(allow_none=True)
+    lock_windows = Alias("lockWindows")
+    lockRevision = Bool(allow_none=True)
+    lock_revision = Alias("lockRevision")
+    revisionsAlgorithmName = String(allow_none=True)
+    revisionsHashValue = Base64Binary(allow_none=True)
+    revisionsSaltValue = Base64Binary(allow_none=True)
+    revisionsSpinCount = Integer(allow_none=True)
+    workbookAlgorithmName = String(allow_none=True)
+    workbookHashValue = Base64Binary(allow_none=True)
+    workbookSaltValue = Base64Binary(allow_none=True)
+    workbookSpinCount = Integer(allow_none=True)
+
+    __attrs__ = ('workbookPassword', 'workbookPasswordCharacterSet', 'revisionsPassword',
+                 'revisionsPasswordCharacterSet', 'lockStructure', 'lockWindows', 'lockRevision',
+                 'revisionsAlgorithmName', 'revisionsHashValue', 'revisionsSaltValue',
+                 'revisionsSpinCount', 'workbookAlgorithmName', 'workbookHashValue',
+                 'workbookSaltValue', 'workbookSpinCount')
+
+    def __init__(self,
+                 workbookPassword=None,
+                 workbookPasswordCharacterSet=None,
+                 revisionsPassword=None,
+                 revisionsPasswordCharacterSet=None,
+                 lockStructure=None,
+                 lockWindows=None,
+                 lockRevision=None,
+                 revisionsAlgorithmName=None,
+                 revisionsHashValue=None,
+                 revisionsSaltValue=None,
+                 revisionsSpinCount=None,
+                 workbookAlgorithmName=None,
+                 workbookHashValue=None,
+                 workbookSaltValue=None,
+                 workbookSpinCount=None,
+                ):
+        if workbookPassword is not None:
+            self.workbookPassword = workbookPassword
+        self.workbookPasswordCharacterSet = workbookPasswordCharacterSet
+        if revisionsPassword is not None:
+            self.revisionsPassword = revisionsPassword
+        self.revisionsPasswordCharacterSet = revisionsPasswordCharacterSet
+        self.lockStructure = lockStructure
+        self.lockWindows = lockWindows
+        self.lockRevision = lockRevision
+        self.revisionsAlgorithmName = revisionsAlgorithmName
+        self.revisionsHashValue = revisionsHashValue
+        self.revisionsSaltValue = revisionsSaltValue
+        self.revisionsSpinCount = revisionsSpinCount
+        self.workbookAlgorithmName = workbookAlgorithmName
+        self.workbookHashValue = workbookHashValue
+        self.workbookSaltValue = workbookSaltValue
+        self.workbookSpinCount = workbookSpinCount
+
+    def set_workbook_password(self, value='', already_hashed=False):
+        """Set a password on this workbook."""
+        if not already_hashed:
+            value = hash_password(value)
+        self._workbook_password = value
+
+    @property
+    def workbookPassword(self):
+        """Return the workbook password value, regardless of hash."""
+        return self._workbook_password
+
+    @workbookPassword.setter
+    def workbookPassword(self, value):
+        """Set a workbook password directly, forcing a hash step."""
+        self.set_workbook_password(value)
+
+    def set_revisions_password(self, value='', already_hashed=False):
+        """Set a revision password on this workbook."""
+        if not already_hashed:
+            value = hash_password(value)
+        self._revisions_password = value
+
+    @property
+    def revisionsPassword(self):
+        """Return the revisions password value, regardless of hash."""
+        return self._revisions_password
+
+    @revisionsPassword.setter
+    def revisionsPassword(self, value):
+        """Set a revisions password directly, forcing a hash step."""
+        self.set_revisions_password(value)
+
+    @classmethod
+    def from_tree(cls, node):
+        """Don't hash passwords when deserialising from XML"""
+        self = super().from_tree(node)
+        if self.workbookPassword:
+            self.set_workbook_password(node.get('workbookPassword'), already_hashed=True)
+        if self.revisionsPassword:
+            self.set_revisions_password(node.get('revisionsPassword'), already_hashed=True)
+        return self
+
+# Backwards compatibility
+DocumentSecurity = WorkbookProtection
+
+
+class FileSharing(Serialisable):
+
+    tagname = "fileSharing"
+
+    readOnlyRecommended = Bool(allow_none=True)
+    userName = String(allow_none=True)
+    reservationPassword = HexBinary(allow_none=True)
+    algorithmName = String(allow_none=True)
+    hashValue = Base64Binary(allow_none=True)
+    saltValue = Base64Binary(allow_none=True)
+    spinCount = Integer(allow_none=True)
+
+    def __init__(self,
+                 readOnlyRecommended=None,
+                 userName=None,
+                 reservationPassword=None,
+                 algorithmName=None,
+                 hashValue=None,
+                 saltValue=None,
+                 spinCount=None,
+                ):
+        self.readOnlyRecommended = readOnlyRecommended
+        self.userName = userName
+        self.reservationPassword = reservationPassword
+        self.algorithmName = algorithmName
+        self.hashValue = hashValue
+        self.saltValue = saltValue
+        self.spinCount = spinCount
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/smart_tags.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/smart_tags.py
new file mode 100644
index 00000000..873e98bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/smart_tags.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    String,
+    Bool,
+    NoneSet,
+
+)
+
+class SmartTag(Serialisable):
+
+    tagname = "smartTagType"
+
+    namespaceUri = String(allow_none=True)
+    name = String(allow_none=True)
+    url = String(allow_none=True)
+
+    def __init__(self,
+                 namespaceUri=None,
+                 name=None,
+                 url=None,
+                ):
+        self.namespaceUri = namespaceUri
+        self.name = name
+        self.url = url
+
+
+class SmartTagList(Serialisable):
+
+    tagname = "smartTagTypes"
+
+    smartTagType = Sequence(expected_type=SmartTag, allow_none=True)
+
+    __elements__ = ('smartTagType',)
+
+    def __init__(self,
+                 smartTagType=(),
+                ):
+        self.smartTagType = smartTagType
+
+
+class SmartTagProperties(Serialisable):
+
+    tagname = "smartTagPr"
+
+    embed = Bool(allow_none=True)
+    show = NoneSet(values=(['all', 'noIndicator']))
+
+    def __init__(self,
+                 embed=None,
+                 show=None,
+                ):
+        self.embed = embed
+        self.show = show
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/views.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/views.py
new file mode 100644
index 00000000..bcbf0267
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/views.py
@@ -0,0 +1,155 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    String,
+    Float,
+    Integer,
+    Bool,
+    NoneSet,
+    Set,
+)
+from openpyxl.descriptors.excel import (
+    ExtensionList,
+    Guid,
+)
+
+
+class BookView(Serialisable):
+
+    tagname = "workbookView"
+
+    visibility = NoneSet(values=(['visible', 'hidden', 'veryHidden']))
+    minimized = Bool(allow_none=True)
+    showHorizontalScroll = Bool(allow_none=True)
+    showVerticalScroll = Bool(allow_none=True)
+    showSheetTabs = Bool(allow_none=True)
+    xWindow = Integer(allow_none=True)
+    yWindow = Integer(allow_none=True)
+    windowWidth = Integer(allow_none=True)
+    windowHeight = Integer(allow_none=True)
+    tabRatio = Integer(allow_none=True)
+    firstSheet = Integer(allow_none=True)
+    activeTab = Integer(allow_none=True)
+    autoFilterDateGrouping = Bool(allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 visibility="visible",
+                 minimized=False,
+                 showHorizontalScroll=True,
+                 showVerticalScroll=True,
+                 showSheetTabs=True,
+                 xWindow=None,
+                 yWindow=None,
+                 windowWidth=None,
+                 windowHeight=None,
+                 tabRatio=600,
+                 firstSheet=0,
+                 activeTab=0,
+                 autoFilterDateGrouping=True,
+                 extLst=None,
+                ):
+        self.visibility = visibility
+        self.minimized = minimized
+        self.showHorizontalScroll = showHorizontalScroll
+        self.showVerticalScroll = showVerticalScroll
+        self.showSheetTabs = showSheetTabs
+        self.xWindow = xWindow
+        self.yWindow = yWindow
+        self.windowWidth = windowWidth
+        self.windowHeight = windowHeight
+        self.tabRatio = tabRatio
+        self.firstSheet = firstSheet
+        self.activeTab = activeTab
+        self.autoFilterDateGrouping = autoFilterDateGrouping
+
+
+class CustomWorkbookView(Serialisable):
+
+    tagname = "customWorkbookView"
+
+    name = String()
+    guid = Guid()
+    autoUpdate = Bool(allow_none=True)
+    mergeInterval = Integer(allow_none=True)
+    changesSavedWin = Bool(allow_none=True)
+    onlySync = Bool(allow_none=True)
+    personalView = Bool(allow_none=True)
+    includePrintSettings = Bool(allow_none=True)
+    includeHiddenRowCol = Bool(allow_none=True)
+    maximized = Bool(allow_none=True)
+    minimized = Bool(allow_none=True)
+    showHorizontalScroll = Bool(allow_none=True)
+    showVerticalScroll = Bool(allow_none=True)
+    showSheetTabs = Bool(allow_none=True)
+    xWindow = Integer(allow_none=True)
+    yWindow = Integer(allow_none=True)
+    windowWidth = Integer()
+    windowHeight = Integer()
+    tabRatio = Integer(allow_none=True)
+    activeSheetId = Integer()
+    showFormulaBar = Bool(allow_none=True)
+    showStatusbar = Bool(allow_none=True)
+    showComments = NoneSet(values=(['commNone', 'commIndicator',
+                                'commIndAndComment']))
+    showObjects = NoneSet(values=(['all', 'placeholders']))
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 name=None,
+                 guid=None,
+                 autoUpdate=None,
+                 mergeInterval=None,
+                 changesSavedWin=None,
+                 onlySync=None,
+                 personalView=None,
+                 includePrintSettings=None,
+                 includeHiddenRowCol=None,
+                 maximized=None,
+                 minimized=None,
+                 showHorizontalScroll=None,
+                 showVerticalScroll=None,
+                 showSheetTabs=None,
+                 xWindow=None,
+                 yWindow=None,
+                 windowWidth=None,
+                 windowHeight=None,
+                 tabRatio=None,
+                 activeSheetId=None,
+                 showFormulaBar=None,
+                 showStatusbar=None,
+                 showComments="commIndicator",
+                 showObjects="all",
+                 extLst=None,
+                ):
+        self.name = name
+        self.guid = guid
+        self.autoUpdate = autoUpdate
+        self.mergeInterval = mergeInterval
+        self.changesSavedWin = changesSavedWin
+        self.onlySync = onlySync
+        self.personalView = personalView
+        self.includePrintSettings = includePrintSettings
+        self.includeHiddenRowCol = includeHiddenRowCol
+        self.maximized = maximized
+        self.minimized = minimized
+        self.showHorizontalScroll = showHorizontalScroll
+        self.showVerticalScroll = showVerticalScroll
+        self.showSheetTabs = showSheetTabs
+        self.xWindow = xWindow
+        self.yWindow = yWindow
+        self.windowWidth = windowWidth
+        self.windowHeight = windowHeight
+        self.tabRatio = tabRatio
+        self.activeSheetId = activeSheetId
+        self.showFormulaBar = showFormulaBar
+        self.showStatusbar = showStatusbar
+        self.showComments = showComments
+        self.showObjects = showObjects
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/web.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/web.py
new file mode 100644
index 00000000..e30e761a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/web.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Sequence,
+    String,
+    Float,
+    Integer,
+    Bool,
+    NoneSet,
+)
+
+
+class WebPublishObject(Serialisable):
+
+    tagname = "webPublishingObject"
+
+    id = Integer()
+    divId = String()
+    sourceObject = String(allow_none=True)
+    destinationFile = String()
+    title = String(allow_none=True)
+    autoRepublish = Bool(allow_none=True)
+
+    def __init__(self,
+                 id=None,
+                 divId=None,
+                 sourceObject=None,
+                 destinationFile=None,
+                 title=None,
+                 autoRepublish=None,
+                ):
+        self.id = id
+        self.divId = divId
+        self.sourceObject = sourceObject
+        self.destinationFile = destinationFile
+        self.title = title
+        self.autoRepublish = autoRepublish
+
+
+class WebPublishObjectList(Serialisable):
+
+    tagname ="webPublishingObjects"
+
+    count = Integer(allow_none=True)
+    webPublishObject = Sequence(expected_type=WebPublishObject)
+
+    __elements__ = ('webPublishObject',)
+
+    def __init__(self,
+                 count=None,
+                 webPublishObject=(),
+                ):
+        self.webPublishObject = webPublishObject
+
+
+    @property
+    def count(self):
+        return len(self.webPublishObject)
+
+
+class WebPublishing(Serialisable):
+
+    tagname = "webPublishing"
+
+    css = Bool(allow_none=True)
+    thicket = Bool(allow_none=True)
+    longFileNames = Bool(allow_none=True)
+    vml = Bool(allow_none=True)
+    allowPng = Bool(allow_none=True)
+    targetScreenSize = NoneSet(values=(['544x376', '640x480', '720x512', '800x600',
+                                    '1024x768', '1152x882', '1152x900', '1280x1024', '1600x1200',
+                                    '1800x1440', '1920x1200']))
+    dpi = Integer(allow_none=True)
+    codePage = Integer(allow_none=True)
+    characterSet = String(allow_none=True)
+
+    def __init__(self,
+                 css=None,
+                 thicket=None,
+                 longFileNames=None,
+                 vml=None,
+                 allowPng=None,
+                 targetScreenSize='800x600',
+                 dpi=None,
+                 codePage=None,
+                 characterSet=None,
+                ):
+        self.css = css
+        self.thicket = thicket
+        self.longFileNames = longFileNames
+        self.vml = vml
+        self.allowPng = allowPng
+        self.targetScreenSize = targetScreenSize
+        self.dpi = dpi
+        self.codePage = codePage
+        self.characterSet = characterSet
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/workbook/workbook.py b/.venv/lib/python3.12/site-packages/openpyxl/workbook/workbook.py
new file mode 100644
index 00000000..b83ac442
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/workbook/workbook.py
@@ -0,0 +1,438 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Workbook is the top-level container for all document information."""
+from copy import copy
+
+from openpyxl.compat import deprecated
+from openpyxl.worksheet.worksheet import Worksheet
+from openpyxl.worksheet._read_only import ReadOnlyWorksheet
+from openpyxl.worksheet._write_only import WriteOnlyWorksheet
+from openpyxl.worksheet.copier import WorksheetCopy
+
+from openpyxl.utils import quote_sheetname
+from openpyxl.utils.indexed_list import IndexedList
+from openpyxl.utils.datetime  import WINDOWS_EPOCH, MAC_EPOCH
+from openpyxl.utils.exceptions import ReadOnlyWorkbookException
+
+from openpyxl.writer.excel import save_workbook
+
+from openpyxl.styles.cell_style import StyleArray
+from openpyxl.styles.named_styles import NamedStyle
+from openpyxl.styles.differential import DifferentialStyleList
+from openpyxl.styles.alignment import Alignment
+from openpyxl.styles.borders import DEFAULT_BORDER
+from openpyxl.styles.fills import DEFAULT_EMPTY_FILL, DEFAULT_GRAY_FILL
+from openpyxl.styles.fonts import DEFAULT_FONT
+from openpyxl.styles.protection import Protection
+from openpyxl.styles.colors import COLOR_INDEX
+from openpyxl.styles.named_styles import NamedStyleList
+from openpyxl.styles.table import TableStyleList
+
+from openpyxl.chartsheet import Chartsheet
+from .defined_name import DefinedName, DefinedNameDict
+from openpyxl.packaging.core import DocumentProperties
+from openpyxl.packaging.custom import CustomPropertyList
+from openpyxl.packaging.relationship import RelationshipList
+from .child import _WorkbookChild
+from .protection import DocumentSecurity
+from .properties import CalcProperties
+from .views import BookView
+
+
+from openpyxl.xml.constants import (
+    XLSM,
+    XLSX,
+    XLTM,
+    XLTX
+)
+
+INTEGER_TYPES = (int,)
+
+class Workbook:
+    """Workbook is the container for all other parts of the document."""
+
+    _read_only = False
+    _data_only = False
+    template = False
+    path = "/xl/workbook.xml"
+
+    def __init__(self,
+                 write_only=False,
+                 iso_dates=False,
+                 ):
+        self._sheets = []
+        self._pivots = []
+        self._active_sheet_index = 0
+        self.defined_names = DefinedNameDict()
+        self._external_links = []
+        self.properties = DocumentProperties()
+        self.custom_doc_props = CustomPropertyList()
+        self.security = DocumentSecurity()
+        self.__write_only = write_only
+        self.shared_strings = IndexedList()
+
+        self._setup_styles()
+
+        self.loaded_theme = None
+        self.vba_archive = None
+        self.is_template = False
+        self.code_name = None
+        self.epoch = WINDOWS_EPOCH
+        self.encoding = "utf-8"
+        self.iso_dates = iso_dates
+
+        if not self.write_only:
+            self._sheets.append(Worksheet(self))
+
+        self.rels = RelationshipList()
+        self.calculation = CalcProperties()
+        self.views = [BookView()]
+
+
+    def _setup_styles(self):
+        """Bootstrap styles"""
+
+        self._fonts = IndexedList()
+        self._fonts.add(DEFAULT_FONT)
+
+        self._alignments = IndexedList([Alignment()])
+
+        self._borders = IndexedList()
+        self._borders.add(DEFAULT_BORDER)
+
+        self._fills = IndexedList()
+        self._fills.add(DEFAULT_EMPTY_FILL)
+        self._fills.add(DEFAULT_GRAY_FILL)
+
+        self._number_formats = IndexedList()
+        self._date_formats = {}
+        self._timedelta_formats = {}
+
+        self._protections = IndexedList([Protection()])
+
+        self._colors = COLOR_INDEX
+        self._cell_styles = IndexedList([StyleArray()])
+        self._named_styles = NamedStyleList()
+        self.add_named_style(NamedStyle(font=copy(DEFAULT_FONT), border=copy(DEFAULT_BORDER), builtinId=0))
+        self._table_styles = TableStyleList()
+        self._differential_styles = DifferentialStyleList()
+
+
+    @property
+    def epoch(self):
+        if self._epoch == WINDOWS_EPOCH:
+            return WINDOWS_EPOCH
+        return MAC_EPOCH
+
+
+    @epoch.setter
+    def epoch(self, value):
+        if value not in (WINDOWS_EPOCH, MAC_EPOCH):
+            raise ValueError("The epoch must be either 1900 or 1904")
+        self._epoch = value
+
+
+    @property
+    def read_only(self):
+        return self._read_only
+
+    @property
+    def data_only(self):
+        return self._data_only
+
+    @property
+    def write_only(self):
+        return self.__write_only
+
+
+    @property
+    def excel_base_date(self):
+        return self.epoch
+
+    @property
+    def active(self):
+        """Get the currently active sheet or None
+
+        :type: :class:`openpyxl.worksheet.worksheet.Worksheet`
+        """
+        try:
+            return self._sheets[self._active_sheet_index]
+        except IndexError:
+            pass
+
+    @active.setter
+    def active(self, value):
+        """Set the active sheet"""
+        if not isinstance(value, (_WorkbookChild, INTEGER_TYPES)):
+            raise TypeError("Value must be either a worksheet, chartsheet or numerical index")
+        if isinstance(value, INTEGER_TYPES):
+            self._active_sheet_index = value
+            return
+            #if self._sheets and 0 <= value < len(self._sheets):
+                #value = self._sheets[value]
+            #else:
+                #raise ValueError("Sheet index is outside the range of possible values", value)
+        if value not in self._sheets:
+            raise ValueError("Worksheet is not in the workbook")
+        if value.sheet_state != "visible":
+            raise ValueError("Only visible sheets can be made active")
+
+        idx = self._sheets.index(value)
+        self._active_sheet_index = idx
+
+
+    def create_sheet(self, title=None, index=None):
+        """Create a worksheet (at an optional index).
+
+        :param title: optional title of the sheet
+        :type title: str
+        :param index: optional position at which the sheet will be inserted
+        :type index: int
+
+        """
+        if self.read_only:
+            raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
+
+        if self.write_only :
+            new_ws = WriteOnlyWorksheet(parent=self, title=title)
+        else:
+            new_ws = Worksheet(parent=self, title=title)
+
+        self._add_sheet(sheet=new_ws, index=index)
+        return new_ws
+
+
+    def _add_sheet(self, sheet, index=None):
+        """Add an worksheet (at an optional index)."""
+
+        if not isinstance(sheet, (Worksheet, WriteOnlyWorksheet, Chartsheet)):
+            raise TypeError("Cannot be added to a workbook")
+
+        if sheet.parent != self:
+            raise ValueError("You cannot add worksheets from another workbook.")
+
+        if index is None:
+            self._sheets.append(sheet)
+        else:
+            self._sheets.insert(index, sheet)
+
+
+    def move_sheet(self, sheet, offset=0):
+        """
+        Move a sheet or sheetname
+        """
+        if not isinstance(sheet, Worksheet):
+            sheet = self[sheet]
+        idx = self._sheets.index(sheet)
+        del self._sheets[idx]
+        new_pos = idx + offset
+        self._sheets.insert(new_pos, sheet)
+
+
+    def remove(self, worksheet):
+        """Remove `worksheet` from this workbook."""
+        idx = self._sheets.index(worksheet)
+        self._sheets.remove(worksheet)
+
+
+    @deprecated("Use wb.remove(worksheet) or del wb[sheetname]")
+    def remove_sheet(self, worksheet):
+        """Remove `worksheet` from this workbook."""
+        self.remove(worksheet)
+
+
+    def create_chartsheet(self, title=None, index=None):
+        if self.read_only:
+            raise ReadOnlyWorkbookException("Cannot create new sheet in a read-only workbook")
+        cs = Chartsheet(parent=self, title=title)
+
+        self._add_sheet(cs, index)
+        return cs
+
+
+    @deprecated("Use wb[sheetname]")
+    def get_sheet_by_name(self, name):
+        """Returns a worksheet by its name.
+
+        :param name: the name of the worksheet to look for
+        :type name: string
+
+        """
+        return self[name]
+
+    def __contains__(self, key):
+        return key in self.sheetnames
+
+
+    def index(self, worksheet):
+        """Return the index of a worksheet."""
+        return self.worksheets.index(worksheet)
+
+
+    @deprecated("Use wb.index(worksheet)")
+    def get_index(self, worksheet):
+        """Return the index of the worksheet."""
+        return self.index(worksheet)
+
+    def __getitem__(self, key):
+        """Returns a worksheet by its name.
+
+        :param name: the name of the worksheet to look for
+        :type name: string
+
+        """
+        for sheet in self.worksheets + self.chartsheets:
+            if sheet.title == key:
+                return sheet
+        raise KeyError("Worksheet {0} does not exist.".format(key))
+
+    def __delitem__(self, key):
+        sheet = self[key]
+        self.remove(sheet)
+
+    def __iter__(self):
+        return iter(self.worksheets)
+
+
+    @deprecated("Use wb.sheetnames")
+    def get_sheet_names(self):
+        return self.sheetnames
+
+    @property
+    def worksheets(self):
+        """A list of sheets in this workbook
+
+        :type: list of :class:`openpyxl.worksheet.worksheet.Worksheet`
+        """
+        return [s for s in self._sheets if isinstance(s, (Worksheet, ReadOnlyWorksheet, WriteOnlyWorksheet))]
+
+    @property
+    def chartsheets(self):
+        """A list of Chartsheets in this workbook
+
+        :type: list of :class:`openpyxl.chartsheet.chartsheet.Chartsheet`
+        """
+        return [s for s in self._sheets if isinstance(s, Chartsheet)]
+
+    @property
+    def sheetnames(self):
+        """Returns the list of the names of worksheets in this workbook.
+
+        Names are returned in the worksheets order.
+
+        :type: list of strings
+
+        """
+        return [s.title for s in self._sheets]
+
+
+    @deprecated("Assign scoped named ranges directly to worksheets or global ones to the workbook. Deprecated in 3.1")
+    def create_named_range(self, name, worksheet=None, value=None, scope=None):
+        """Create a new named_range on a worksheet
+
+        """
+        defn = DefinedName(name=name)
+        if worksheet is not None:
+            defn.value = "{0}!{1}".format(quote_sheetname(worksheet.title), value)
+        else:
+            defn.value = value
+
+        self.defined_names[name] = defn
+
+
+    def add_named_style(self, style):
+        """
+        Add a named style
+        """
+        self._named_styles.append(style)
+        style.bind(self)
+
+
+    @property
+    def named_styles(self):
+        """
+        List available named styles
+        """
+        return self._named_styles.names
+
+
+    @property
+    def mime_type(self):
+        """
+        The mime type is determined by whether a workbook is a template or
+        not and whether it contains macros or not. Excel requires the file
+        extension to match but openpyxl does not enforce this.
+
+        """
+        ct = self.template and XLTX or XLSX
+        if self.vba_archive:
+            ct = self.template and XLTM or XLSM
+        return ct
+
+
+    def save(self, filename):
+        """Save the current workbook under the given `filename`.
+        Use this function instead of using an `ExcelWriter`.
+
+        .. warning::
+            When creating your workbook using `write_only` set to True,
+            you will only be able to call this function once. Subsequent attempts to
+            modify or save the file will raise an :class:`openpyxl.shared.exc.WorkbookAlreadySaved` exception.
+        """
+        if self.read_only:
+            raise TypeError("""Workbook is read-only""")
+        if self.write_only and not self.worksheets:
+            self.create_sheet()
+        save_workbook(self, filename)
+
+
+    @property
+    def style_names(self):
+        """
+        List of named styles
+        """
+        return [s.name for s in self._named_styles]
+
+
+    def copy_worksheet(self, from_worksheet):
+        """Copy an existing worksheet in the current workbook
+
+        .. warning::
+            This function cannot copy worksheets between workbooks.
+            worksheets can only be copied within the workbook that they belong
+
+        :param from_worksheet: the worksheet to be copied from
+        :return: copy of the initial worksheet
+        """
+        if self.__write_only or self._read_only:
+            raise ValueError("Cannot copy worksheets in read-only or write-only mode")
+
+        new_title = u"{0} Copy".format(from_worksheet.title)
+        to_worksheet = self.create_sheet(title=new_title)
+        cp = WorksheetCopy(source_worksheet=from_worksheet, target_worksheet=to_worksheet)
+        cp.copy_worksheet()
+        return to_worksheet
+
+
+    def close(self):
+        """
+        Close workbook file if open. Only affects read-only and write-only modes.
+        """
+        if hasattr(self, '_archive'):
+            self._archive.close()
+
+
+    def _duplicate_name(self, name):
+        """
+        Check for duplicate name in defined name list and table list of each worksheet.
+        Names are not case sensitive.
+        """
+        name = name.lower()
+        for sheet in self.worksheets:
+            for t in sheet.tables:
+                if name == t.lower():
+                    return True
+
+        if name in self.defined_names:
+            return True
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/__init__.py
new file mode 100644
index 00000000..ab6cdead
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) 2010-2024 openpyxl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_read_only.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_read_only.py
new file mode 100644
index 00000000..95852f21
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_read_only.py
@@ -0,0 +1,190 @@
+# Copyright (c) 2010-2024 openpyxl
+
+""" Read worksheets on-demand
+"""
+
+from .worksheet import Worksheet
+from openpyxl.cell.read_only import ReadOnlyCell, EMPTY_CELL
+from openpyxl.utils import get_column_letter
+
+from ._reader import WorkSheetParser
+from openpyxl.workbook.defined_name import DefinedNameDict
+
+
+def read_dimension(source):
+    parser = WorkSheetParser(source, [])
+    return parser.parse_dimensions()
+
+
+class ReadOnlyWorksheet:
+
+    _min_column = 1
+    _min_row = 1
+    _max_column = _max_row = None
+
+    # from Standard Worksheet
+    # Methods from Worksheet
+    cell = Worksheet.cell
+    iter_rows = Worksheet.iter_rows
+    values = Worksheet.values
+    rows = Worksheet.rows
+    __getitem__ = Worksheet.__getitem__
+    __iter__ = Worksheet.__iter__
+
+
+    def __init__(self, parent_workbook, title, worksheet_path, shared_strings):
+        self.parent = parent_workbook
+        self.title = title
+        self.sheet_state = 'visible'
+        self._current_row = None
+        self._worksheet_path = worksheet_path
+        self._shared_strings = shared_strings
+        self._get_size()
+        self.defined_names = DefinedNameDict()
+
+
+    def _get_size(self):
+        src = self._get_source()
+        parser = WorkSheetParser(src, [])
+        dimensions = parser.parse_dimensions()
+        src.close()
+        if dimensions is not None:
+            self._min_column, self._min_row, self._max_column, self._max_row = dimensions
+
+
+    def _get_source(self):
+        """Parse xml source on demand, must close after use"""
+        return self.parent._archive.open(self._worksheet_path)
+
+
+    def _cells_by_row(self, min_col, min_row, max_col, max_row, values_only=False):
+        """
+        The source worksheet file may have columns or rows missing.
+        Missing cells will be created.
+        """
+        filler = EMPTY_CELL
+        if values_only:
+            filler = None
+
+        max_col = max_col or self.max_column
+        max_row = max_row or self.max_row
+        empty_row = []
+        if max_col is not None:
+            empty_row = (filler,) * (max_col + 1 - min_col)
+
+        counter = min_row
+        idx = 1
+        with self._get_source() as src:
+            parser = WorkSheetParser(src,
+                                     self._shared_strings,
+                                     data_only=self.parent.data_only,
+                                     epoch=self.parent.epoch,
+                                     date_formats=self.parent._date_formats,
+                                     timedelta_formats=self.parent._timedelta_formats)
+
+            for idx, row in parser.parse():
+                if max_row is not None and idx > max_row:
+                    break
+
+                # some rows are missing
+                for _ in range(counter, idx):
+                    counter += 1
+                    yield empty_row
+
+                # return cells from a row
+                if counter <= idx:
+                    row = self._get_row(row, min_col, max_col, values_only)
+                    counter += 1
+                    yield row
+
+        if max_row is not None and max_row < idx:
+            for _ in range(counter, max_row+1):
+                yield empty_row
+
+
+    def _get_row(self, row, min_col=1, max_col=None, values_only=False):
+        """
+        Make sure a row contains always the same number of cells or values
+        """
+        if not row and not max_col: # in case someone wants to force rows where there aren't any
+            return ()
+
+        max_col = max_col or  row[-1]['column']
+        row_width = max_col + 1 - min_col
+
+        new_row = [EMPTY_CELL] * row_width
+        if values_only:
+            new_row = [None] * row_width
+
+        for cell in row:
+            counter = cell['column']
+            if min_col <= counter <= max_col:
+                idx = counter - min_col # position in list of cells returned
+                new_row[idx] = cell['value']
+                if not values_only:
+                    new_row[idx] = ReadOnlyCell(self, **cell)
+
+        return tuple(new_row)
+
+
+    def _get_cell(self, row, column):
+        """Cells are returned by a generator which can be empty"""
+        for row in self._cells_by_row(column, row, column, row):
+            if row:
+                return row[0]
+        return EMPTY_CELL
+
+
+    def calculate_dimension(self, force=False):
+        if not all([self.max_column, self.max_row]):
+            if force:
+                self._calculate_dimension()
+            else:
+                raise ValueError("Worksheet is unsized, use calculate_dimension(force=True)")
+        return f"{get_column_letter(self.min_column)}{self.min_row}:{get_column_letter(self.max_column)}{self.max_row}"
+
+
+    def _calculate_dimension(self):
+        """
+        Loop through all the cells to get the size of a worksheet.
+        Do this only if it is explicitly requested.
+        """
+
+        max_col = 0
+        for r in self.rows:
+            if not r:
+                continue
+            cell = r[-1]
+            max_col = max(max_col, cell.column)
+
+        self._max_row = cell.row
+        self._max_column = max_col
+
+
+    def reset_dimensions(self):
+        """
+        Remove worksheet dimensions if these are incorrect in the worksheet source.
+        NB. This probably indicates a bug in the library or application that created
+        the workbook.
+        """
+        self._max_row = self._max_column = None
+
+
+    @property
+    def min_row(self):
+        return self._min_row
+
+
+    @property
+    def max_row(self):
+        return self._max_row
+
+
+    @property
+    def min_column(self):
+        return self._min_column
+
+
+    @property
+    def max_column(self):
+        return self._max_column
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_reader.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_reader.py
new file mode 100644
index 00000000..38240f03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_reader.py
@@ -0,0 +1,472 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Reader for a single worksheet."""
+from copy import copy
+from warnings import warn
+
+# compatibility imports
+from openpyxl.xml.functions import iterparse
+
+# package imports
+from openpyxl.cell import Cell, MergedCell
+from openpyxl.cell.text import Text
+from openpyxl.worksheet.dimensions import (
+    ColumnDimension,
+    RowDimension,
+    SheetFormatProperties,
+)
+
+from openpyxl.xml.constants import (
+    SHEET_MAIN_NS,
+    EXT_TYPES,
+)
+from openpyxl.formatting.formatting import ConditionalFormatting
+from openpyxl.formula.translate import Translator
+from openpyxl.utils import (
+    get_column_letter,
+    coordinate_to_tuple,
+    )
+from openpyxl.utils.datetime import from_excel, from_ISO8601, WINDOWS_EPOCH
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.cell.rich_text import CellRichText
+
+from .formula import DataTableFormula, ArrayFormula
+from .filters import AutoFilter
+from .header_footer import HeaderFooter
+from .hyperlink import HyperlinkList
+from .merge import MergeCells
+from .page import PageMargins, PrintOptions, PrintPageSetup
+from .pagebreak import RowBreak, ColBreak
+from .protection import SheetProtection
+from .scenario import ScenarioList
+from .views import SheetViewList
+from .datavalidation import DataValidationList
+from .table import TablePartList
+from .properties import WorksheetProperties
+from .dimensions import SheetDimension
+from .related import Related
+
+
+CELL_TAG = '{%s}c' % SHEET_MAIN_NS
+VALUE_TAG = '{%s}v' % SHEET_MAIN_NS
+FORMULA_TAG = '{%s}f' % SHEET_MAIN_NS
+MERGE_TAG = '{%s}mergeCells' % SHEET_MAIN_NS
+INLINE_STRING = "{%s}is" % SHEET_MAIN_NS
+COL_TAG = '{%s}col' % SHEET_MAIN_NS
+ROW_TAG = '{%s}row' % SHEET_MAIN_NS
+CF_TAG = '{%s}conditionalFormatting' % SHEET_MAIN_NS
+LEGACY_TAG = '{%s}legacyDrawing' % SHEET_MAIN_NS
+PROT_TAG = '{%s}sheetProtection' % SHEET_MAIN_NS
+EXT_TAG = "{%s}extLst" % SHEET_MAIN_NS
+HYPERLINK_TAG = "{%s}hyperlinks" % SHEET_MAIN_NS
+TABLE_TAG = "{%s}tableParts" % SHEET_MAIN_NS
+PRINT_TAG = '{%s}printOptions' % SHEET_MAIN_NS
+MARGINS_TAG = '{%s}pageMargins' % SHEET_MAIN_NS
+PAGE_TAG = '{%s}pageSetup' % SHEET_MAIN_NS
+HEADER_TAG = '{%s}headerFooter' % SHEET_MAIN_NS
+FILTER_TAG = '{%s}autoFilter' % SHEET_MAIN_NS
+VALIDATION_TAG = '{%s}dataValidations' % SHEET_MAIN_NS
+PROPERTIES_TAG = '{%s}sheetPr' % SHEET_MAIN_NS
+VIEWS_TAG = '{%s}sheetViews' % SHEET_MAIN_NS
+FORMAT_TAG = '{%s}sheetFormatPr' % SHEET_MAIN_NS
+ROW_BREAK_TAG = '{%s}rowBreaks' % SHEET_MAIN_NS
+COL_BREAK_TAG = '{%s}colBreaks' % SHEET_MAIN_NS
+SCENARIOS_TAG = '{%s}scenarios' % SHEET_MAIN_NS
+DATA_TAG = '{%s}sheetData' % SHEET_MAIN_NS
+DIMENSION_TAG = '{%s}dimension' % SHEET_MAIN_NS
+CUSTOM_VIEWS_TAG = '{%s}customSheetViews' % SHEET_MAIN_NS
+
+
+def _cast_number(value):
+    "Convert numbers as string to an int or float"
+    if "." in value or "E" in value or "e" in value:
+        return float(value)
+    return int(value)
+
+
+def parse_richtext_string(element):
+    """
+    Parse inline string and preserve rich text formatting
+    """
+    value = CellRichText.from_tree(element) or ""
+    if len(value) == 1 and isinstance(value[0], str):
+        value = value[0]
+    return value
+
+
+class WorkSheetParser:
+
+    def __init__(self, src, shared_strings, data_only=False,
+                 epoch=WINDOWS_EPOCH, date_formats=set(),
+                 timedelta_formats=set(), rich_text=False):
+        self.min_row = self.min_col = None
+        self.epoch = epoch
+        self.source = src
+        self.shared_strings = shared_strings
+        self.data_only = data_only
+        self.shared_formulae = {}
+        self.row_counter = self.col_counter = 0
+        self.tables = TablePartList()
+        self.date_formats = date_formats
+        self.timedelta_formats = timedelta_formats
+        self.row_dimensions = {}
+        self.column_dimensions = {}
+        self.number_formats = []
+        self.keep_vba = False
+        self.hyperlinks = HyperlinkList()
+        self.formatting = []
+        self.legacy_drawing = None
+        self.merged_cells = None
+        self.row_breaks = RowBreak()
+        self.col_breaks = ColBreak()
+        self.rich_text = rich_text
+
+
+    def parse(self):
+        dispatcher = {
+            COL_TAG: self.parse_column_dimensions,
+            PROT_TAG: self.parse_sheet_protection,
+            EXT_TAG: self.parse_extensions,
+            CF_TAG: self.parse_formatting,
+            LEGACY_TAG: self.parse_legacy,
+            ROW_BREAK_TAG: self.parse_row_breaks,
+            COL_BREAK_TAG: self.parse_col_breaks,
+            CUSTOM_VIEWS_TAG: self.parse_custom_views,
+                      }
+
+        properties = {
+            PRINT_TAG: ('print_options', PrintOptions),
+            MARGINS_TAG: ('page_margins', PageMargins),
+            PAGE_TAG: ('page_setup', PrintPageSetup),
+            HEADER_TAG: ('HeaderFooter', HeaderFooter),
+            FILTER_TAG: ('auto_filter', AutoFilter),
+            VALIDATION_TAG: ('data_validations', DataValidationList),
+            PROPERTIES_TAG: ('sheet_properties', WorksheetProperties),
+            VIEWS_TAG: ('views', SheetViewList),
+            FORMAT_TAG: ('sheet_format', SheetFormatProperties),
+            SCENARIOS_TAG: ('scenarios', ScenarioList),
+            TABLE_TAG: ('tables', TablePartList),
+            HYPERLINK_TAG: ('hyperlinks', HyperlinkList),
+            MERGE_TAG: ('merged_cells', MergeCells),
+
+        }
+
+        it = iterparse(self.source) # add a finaliser to close the source when this becomes possible
+
+        for _, element in it:
+            tag_name = element.tag
+            if tag_name in dispatcher:
+                dispatcher[tag_name](element)
+                element.clear()
+            elif tag_name in properties:
+                prop = properties[tag_name]
+                obj = prop[1].from_tree(element)
+                setattr(self, prop[0], obj)
+                element.clear()
+            elif tag_name == ROW_TAG:
+                row = self.parse_row(element)
+                element.clear()
+                yield row
+
+
+    def parse_dimensions(self):
+        """
+        Get worksheet dimensions if they are provided.
+        """
+        it = iterparse(self.source)
+
+        for _event, element in it:
+            if element.tag == DIMENSION_TAG:
+                dim = SheetDimension.from_tree(element)
+                return dim.boundaries
+
+            elif element.tag == DATA_TAG:
+                # Dimensions missing
+                break
+            element.clear()
+
+
+    def parse_cell(self, element):
+        data_type = element.get('t', 'n')
+        coordinate = element.get('r')
+        style_id = element.get('s', 0)
+        if style_id:
+            style_id = int(style_id)
+
+        if data_type == "inlineStr":
+            value = None
+        else:
+            value = element.findtext(VALUE_TAG, None) or None
+
+        if coordinate:
+            row, column = coordinate_to_tuple(coordinate)
+            self.col_counter = column
+        else:
+            self.col_counter += 1
+            row, column = self.row_counter, self.col_counter
+
+        if not self.data_only and element.find(FORMULA_TAG) is not None:
+            data_type = 'f'
+            value = self.parse_formula(element)
+
+        elif value is not None:
+            if data_type == 'n':
+                value = _cast_number(value)
+                if style_id in self.date_formats:
+                    data_type = 'd'
+                    try:
+                        value = from_excel(
+                            value, self.epoch, timedelta=style_id in self.timedelta_formats
+                        )
+                    except (OverflowError, ValueError):
+                        msg = f"""Cell {coordinate} is marked as a date but the serial value {value} is outside the limits for dates. The cell will be treated as an error."""
+                        warn(msg)
+                        data_type = "e"
+                        value = "#VALUE!"
+            elif data_type == 's':
+                value = self.shared_strings[int(value)]
+            elif data_type == 'b':
+                value = bool(int(value))
+            elif data_type == "str":
+                data_type = "s"
+            elif data_type == 'd':
+                value = from_ISO8601(value)
+
+        elif data_type == 'inlineStr':
+                child = element.find(INLINE_STRING)
+                if child is not None:
+                    data_type = 's'
+                    if self.rich_text:
+                        value = parse_richtext_string(child)
+                    else:
+                        value = Text.from_tree(child).content
+
+        return {'row':row, 'column':column, 'value':value, 'data_type':data_type, 'style_id':style_id}
+
+
+    def parse_formula(self, element):
+        """
+        possible formulae types: shared, array, datatable
+        """
+        formula = element.find(FORMULA_TAG)
+        formula_type = formula.get('t')
+        coordinate = element.get('r')
+        value = "="
+        if formula.text is not None:
+            value += formula.text
+
+        if formula_type == "array":
+            value = ArrayFormula(ref=formula.get('ref'), text=value)
+
+        elif formula_type == "shared":
+            idx = formula.get('si')
+            if idx in self.shared_formulae:
+                trans = self.shared_formulae[idx]
+                value = trans.translate_formula(coordinate)
+            elif value != "=":
+                self.shared_formulae[idx] = Translator(value, coordinate)
+
+        elif formula_type == "dataTable":
+            value = DataTableFormula(**formula.attrib)
+
+        return value
+
+
+    def parse_column_dimensions(self, col):
+        attrs = dict(col.attrib)
+        column = get_column_letter(int(attrs['min']))
+        attrs['index'] = column
+        self.column_dimensions[column] = attrs
+
+
+    def parse_row(self, row):
+        attrs = dict(row.attrib)
+
+        if "r" in attrs:
+            try:
+                self.row_counter = int(attrs['r'])
+            except ValueError:
+                val = float(attrs['r'])
+                if val.is_integer():
+                    self.row_counter = int(val)
+                else:
+                    raise ValueError(f"{attrs['r']} is not a valid row number")
+        else:
+            self.row_counter += 1
+        self.col_counter = 0
+
+        keys = {k for k in attrs if not k.startswith('{')}
+        if keys - {'r', 'spans'}:
+            # don't create dimension objects unless they have relevant information
+            self.row_dimensions[str(self.row_counter)] = attrs
+
+        cells = [self.parse_cell(el) for el in row]
+        return self.row_counter, cells
+
+
+    def parse_formatting(self, element):
+        try:
+            cf = ConditionalFormatting.from_tree(element)
+            self.formatting.append(cf)
+        except TypeError as e:
+            msg = f"Failed to load a conditional formatting rule. It will be discarded. Cause: {e}"
+            warn(msg)
+
+
+    def parse_sheet_protection(self, element):
+        protection = SheetProtection.from_tree(element)
+        password = element.get("password")
+        if password is not None:
+            protection.set_password(password, True)
+        self.protection = protection
+
+
+    def parse_extensions(self, element):
+        extLst = ExtensionList.from_tree(element)
+        for e in extLst.ext:
+            ext_type = EXT_TYPES.get(e.uri.upper(), "Unknown")
+            msg = "{0} extension is not supported and will be removed".format(ext_type)
+            warn(msg)
+
+
+    def parse_legacy(self, element):
+        obj = Related.from_tree(element)
+        self.legacy_drawing = obj.id
+
+
+    def parse_row_breaks(self, element):
+        brk = RowBreak.from_tree(element)
+        self.row_breaks = brk
+
+
+    def parse_col_breaks(self, element):
+        brk = ColBreak.from_tree(element)
+        self.col_breaks = brk
+
+
+    def parse_custom_views(self, element):
+        # clear page_breaks to avoid duplication which Excel doesn't like
+        # basically they're ignored in custom views
+        self.row_breaks = RowBreak()
+        self.col_breaks = ColBreak()
+
+
+class WorksheetReader:
+    """
+    Create a parser and apply it to a workbook
+    """
+
+    def __init__(self, ws, xml_source, shared_strings, data_only, rich_text):
+        self.ws = ws
+        self.parser = WorkSheetParser(xml_source, shared_strings,
+                data_only, ws.parent.epoch, ws.parent._date_formats,
+                ws.parent._timedelta_formats, rich_text)
+        self.tables = []
+
+
+    def bind_cells(self):
+        for idx, row in self.parser.parse():
+            for cell in row:
+                style = self.ws.parent._cell_styles[cell['style_id']]
+                c = Cell(self.ws, row=cell['row'], column=cell['column'], style_array=style)
+                c._value = cell['value']
+                c.data_type = cell['data_type']
+                self.ws._cells[(cell['row'], cell['column'])] = c
+
+        if self.ws._cells:
+            self.ws._current_row = self.ws.max_row # use cells not row dimensions
+
+
+    def bind_formatting(self):
+        for cf in self.parser.formatting:
+            for rule in cf.rules:
+                if rule.dxfId is not None:
+                    rule.dxf = self.ws.parent._differential_styles[rule.dxfId]
+                self.ws.conditional_formatting[cf] = rule
+
+
+    def bind_tables(self):
+        for t in self.parser.tables.tablePart:
+            rel = self.ws._rels.get(t.id)
+            self.tables.append(rel.Target)
+
+
+    def bind_merged_cells(self):
+        from openpyxl.worksheet.cell_range import MultiCellRange
+        from openpyxl.worksheet.merge import MergedCellRange
+        if not self.parser.merged_cells:
+            return
+
+        ranges = []
+        for cr in self.parser.merged_cells.mergeCell:
+            mcr = MergedCellRange(self.ws, cr.ref)
+            self.ws._clean_merge_range(mcr)
+            ranges.append(mcr)
+        self.ws.merged_cells = MultiCellRange(ranges)
+
+
+    def bind_hyperlinks(self):
+        for link in self.parser.hyperlinks.hyperlink:
+            if link.id:
+                rel = self.ws._rels.get(link.id)
+                link.target = rel.Target
+            if ":" in link.ref:
+                # range of cells
+                for row in self.ws[link.ref]:
+                    for cell in row:
+                        try:
+                            cell.hyperlink = copy(link)
+                        except AttributeError:
+                            pass
+            else:
+                cell = self.ws[link.ref]
+                if isinstance(cell, MergedCell):
+                    cell = self.normalize_merged_cell_link(cell.coordinate)
+                cell.hyperlink = link
+
+    def normalize_merged_cell_link(self, coord):
+        """
+        Returns the appropriate cell to which a hyperlink, which references a merged cell at the specified coordinates,
+        should be bound.
+        """
+        for rng in self.ws.merged_cells:
+            if coord in rng:
+                return self.ws.cell(*rng.top[0])
+
+    def bind_col_dimensions(self):
+        for col, cd in self.parser.column_dimensions.items():
+            if 'style' in cd:
+                key = int(cd['style'])
+                cd['style'] = self.ws.parent._cell_styles[key]
+            self.ws.column_dimensions[col] = ColumnDimension(self.ws, **cd)
+
+
+    def bind_row_dimensions(self):
+        for row, rd in self.parser.row_dimensions.items():
+            if 's' in rd:
+                key = int(rd['s'])
+                rd['s'] = self.ws.parent._cell_styles[key]
+            self.ws.row_dimensions[int(row)] = RowDimension(self.ws, **rd)
+
+
+    def bind_properties(self):
+        for k in ('print_options', 'page_margins', 'page_setup',
+                  'HeaderFooter', 'auto_filter', 'data_validations',
+                  'sheet_properties', 'views', 'sheet_format',
+                  'row_breaks', 'col_breaks', 'scenarios', 'legacy_drawing',
+                  'protection',
+                  ):
+            v = getattr(self.parser, k, None)
+            if v is not None:
+                setattr(self.ws, k, v)
+
+
+    def bind_all(self):
+        self.bind_cells()
+        self.bind_merged_cells()
+        self.bind_hyperlinks()
+        self.bind_formatting()
+        self.bind_col_dimensions()
+        self.bind_row_dimensions()
+        self.bind_tables()
+        self.bind_properties()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_write_only.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_write_only.py
new file mode 100644
index 00000000..0b1d027d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_write_only.py
@@ -0,0 +1,160 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""Write worksheets to xml representations in an optimized way"""
+
+from inspect import isgenerator
+
+from openpyxl.cell import Cell, WriteOnlyCell
+from openpyxl.workbook.child import _WorkbookChild
+from .worksheet import Worksheet
+from openpyxl.utils.exceptions import WorkbookAlreadySaved
+
+from ._writer import WorksheetWriter
+
+
+class WriteOnlyWorksheet(_WorkbookChild):
+    """
+    Streaming worksheet. Optimised to reduce memory by writing rows just in
+    time.
+    Cells can be styled and have comments Styles for rows and columns
+    must be applied before writing cells
+    """
+
+    __saved = False
+    _writer = None
+    _rows = None
+    _rel_type = Worksheet._rel_type
+    _path = Worksheet._path
+    mime_type = Worksheet.mime_type
+
+    # copy methods from Standard worksheet
+    _add_row = Worksheet._add_row
+    _add_column = Worksheet._add_column
+    add_chart = Worksheet.add_chart
+    add_image = Worksheet.add_image
+    add_table = Worksheet.add_table
+    tables = Worksheet.tables
+    print_titles = Worksheet.print_titles
+    print_title_cols = Worksheet.print_title_cols
+    print_title_rows = Worksheet.print_title_rows
+    freeze_panes = Worksheet.freeze_panes
+    print_area = Worksheet.print_area
+    sheet_view = Worksheet.sheet_view
+    _setup = Worksheet._setup
+
+    def __init__(self, parent, title):
+        super().__init__(parent, title)
+        self._max_col = 0
+        self._max_row = 0
+        self._setup()
+
+    @property
+    def closed(self):
+        return self.__saved
+
+
+    def _write_rows(self):
+        """
+        Send rows to the writer's stream
+        """
+        try:
+            xf = self._writer.xf.send(True)
+        except StopIteration:
+            self._already_saved()
+
+        with xf.element("sheetData"):
+            row_idx = 1
+            try:
+                while True:
+                    row = (yield)
+                    row = self._values_to_row(row, row_idx)
+                    self._writer.write_row(xf, row, row_idx)
+                    row_idx += 1
+            except GeneratorExit:
+                pass
+
+        self._writer.xf.send(None)
+
+
+    def _get_writer(self):
+        if self._writer is None:
+            self._writer = WorksheetWriter(self)
+            self._writer.write_top()
+
+
+    def close(self):
+        if self.__saved:
+            self._already_saved()
+
+        self._get_writer()
+
+        if self._rows is None:
+            self._writer.write_rows()
+        else:
+            self._rows.close()
+
+        self._writer.write_tail()
+
+        self._writer.close()
+        self.__saved = True
+
+
+    def append(self, row):
+        """
+        :param row: iterable containing values to append
+        :type row: iterable
+        """
+
+        if (not isgenerator(row) and
+            not isinstance(row, (list, tuple, range))
+            ):
+            self._invalid_row(row)
+
+        self._get_writer()
+
+        if self._rows is None:
+            self._rows = self._write_rows()
+            next(self._rows)
+
+        self._rows.send(row)
+
+
+    def _values_to_row(self, values, row_idx):
+        """
+        Convert whatever has been appended into a form suitable for work_rows
+        """
+        cell = WriteOnlyCell(self)
+
+        for col_idx, value in enumerate(values, 1):
+            if value is None:
+                continue
+            try:
+                cell.value = value
+            except ValueError:
+                if isinstance(value, Cell):
+                    cell = value
+                else:
+                    raise ValueError
+
+            cell.column = col_idx
+            cell.row = row_idx
+
+            if cell.hyperlink is not None:
+                cell.hyperlink.ref = cell.coordinate
+
+            yield cell
+
+            # reset cell if style applied
+            if cell.has_style or cell.hyperlink:
+                cell = WriteOnlyCell(self)
+
+
+    def _already_saved(self):
+        raise WorkbookAlreadySaved('Workbook has already been saved and cannot be modified or saved anymore.')
+
+
+    def _invalid_row(self, iterable):
+        raise TypeError('Value must be a list, tuple, range or a generator Supplied value is {0}'.format(
+            type(iterable))
+                        )
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_writer.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_writer.py
new file mode 100644
index 00000000..df381d2b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/_writer.py
@@ -0,0 +1,390 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import atexit
+from collections import defaultdict
+from io import BytesIO
+import os
+from tempfile import NamedTemporaryFile
+from warnings import warn
+
+from openpyxl.xml.functions import xmlfile
+from openpyxl.xml.constants import SHEET_MAIN_NS
+
+from openpyxl.comments.comment_sheet import CommentRecord
+from openpyxl.packaging.relationship import Relationship, RelationshipList
+from openpyxl.styles.differential import DifferentialStyle
+
+from .dimensions import SheetDimension
+from .hyperlink import HyperlinkList
+from .merge import MergeCell, MergeCells
+from .related import Related
+from .table import TablePartList
+
+from openpyxl.cell._writer import write_cell
+
+
+ALL_TEMP_FILES = []
+
+@atexit.register
+def _openpyxl_shutdown():
+    for path in ALL_TEMP_FILES:
+        if os.path.exists(path):
+            os.remove(path)
+
+
+def create_temporary_file(suffix=''):
+    fobj = NamedTemporaryFile(mode='w+', suffix=suffix,
+                              prefix='openpyxl.', delete=False)
+    filename = fobj.name
+    fobj.close()
+    ALL_TEMP_FILES.append(filename)
+    return filename
+
+
+class WorksheetWriter:
+
+
+    def __init__(self, ws, out=None):
+        self.ws = ws
+        self.ws._hyperlinks = []
+        self.ws._comments = []
+        if out is None:
+            out = create_temporary_file()
+        self.out = out
+        self._rels = RelationshipList()
+        self.xf = self.get_stream()
+        next(self.xf) # start generator
+
+
+    def write_properties(self):
+        props = self.ws.sheet_properties
+        self.xf.send(props.to_tree())
+
+
+    def write_dimensions(self):
+        """
+        Write worksheet size if known
+        """
+        ref = getattr(self.ws, 'calculate_dimension', None)
+        if ref:
+            dim = SheetDimension(ref())
+            self.xf.send(dim.to_tree())
+
+
+    def write_format(self):
+        self.ws.sheet_format.outlineLevelCol = self.ws.column_dimensions.max_outline
+        fmt = self.ws.sheet_format
+        self.xf.send(fmt.to_tree())
+
+
+    def write_views(self):
+        views = self.ws.views
+        self.xf.send(views.to_tree())
+
+
+    def write_cols(self):
+        cols = self.ws.column_dimensions
+        self.xf.send(cols.to_tree())
+
+
+    def write_top(self):
+        """
+        Write all elements up to rows:
+        properties
+        dimensions
+        views
+        format
+        cols
+        """
+        self.write_properties()
+        self.write_dimensions()
+        self.write_views()
+        self.write_format()
+        self.write_cols()
+
+
+    def rows(self):
+        """Return all rows, and any cells that they contain"""
+        # order cells by row
+        rows = defaultdict(list)
+        for (row, col), cell in sorted(self.ws._cells.items()):
+            rows[row].append(cell)
+
+        # add empty rows if styling has been applied
+        for row in self.ws.row_dimensions.keys() - rows.keys():
+            rows[row] = []
+
+        return sorted(rows.items())
+
+
+    def write_rows(self):
+        xf = self.xf.send(True)
+
+        with xf.element("sheetData"):
+            for row_idx, row in self.rows():
+                self.write_row(xf, row, row_idx)
+
+        self.xf.send(None) # return control to generator
+
+
+    def write_row(self, xf, row, row_idx):
+        attrs = {'r': f"{row_idx}"}
+        dims = self.ws.row_dimensions
+        attrs.update(dims.get(row_idx, {}))
+
+        with xf.element("row", attrs):
+
+            for cell in row:
+                if cell._comment is not None:
+                    comment = CommentRecord.from_cell(cell)
+                    self.ws._comments.append(comment)
+                if (
+                    cell._value is None
+                    and not cell.has_style
+                    and not cell._comment
+                    ):
+                    continue
+                write_cell(xf, self.ws, cell, cell.has_style)
+
+
+    def write_protection(self):
+        prot = self.ws.protection
+        if prot:
+            self.xf.send(prot.to_tree())
+
+
+    def write_scenarios(self):
+        scenarios = self.ws.scenarios
+        if scenarios:
+            self.xf.send(scenarios.to_tree())
+
+
+    def write_filter(self):
+        flt = self.ws.auto_filter
+        if flt:
+            self.xf.send(flt.to_tree())
+
+
+    def write_sort(self):
+        """
+        As per discusion with the OOXML Working Group global sort state is not required.
+        openpyxl never reads it from existing files
+        """
+        pass
+
+
+    def write_merged_cells(self):
+        merged = self.ws.merged_cells
+        if merged:
+            cells = [MergeCell(str(ref)) for ref in self.ws.merged_cells]
+            self.xf.send(MergeCells(mergeCell=cells).to_tree())
+
+
+    def write_formatting(self):
+        df = DifferentialStyle()
+        wb = self.ws.parent
+        for cf in self.ws.conditional_formatting:
+            for rule in cf.rules:
+                if rule.dxf and rule.dxf != df:
+                    rule.dxfId = wb._differential_styles.add(rule.dxf)
+            self.xf.send(cf.to_tree())
+
+
+    def write_validations(self):
+        dv = self.ws.data_validations
+        if dv:
+            self.xf.send(dv.to_tree())
+
+
+    def write_hyperlinks(self):
+
+        links = self.ws._hyperlinks
+
+        for link in links:
+            if link.target:
+                rel = Relationship(type="hyperlink", TargetMode="External", Target=link.target)
+                self._rels.append(rel)
+                link.id = rel.id
+
+        if links:
+            self.xf.send(HyperlinkList(links).to_tree())
+
+
+    def write_print(self):
+        print_options = self.ws.print_options
+        if print_options:
+            self.xf.send(print_options.to_tree())
+
+
+    def write_margins(self):
+        margins = self.ws.page_margins
+        if margins:
+            self.xf.send(margins.to_tree())
+
+
+    def write_page(self):
+        setup = self.ws.page_setup
+        if setup:
+            self.xf.send(setup.to_tree())
+
+
+    def write_header(self):
+        hf = self.ws.HeaderFooter
+        if hf:
+            self.xf.send(hf.to_tree())
+
+
+    def write_breaks(self):
+        brks = (self.ws.row_breaks, self.ws.col_breaks)
+        for brk in brks:
+            if brk:
+                self.xf.send(brk.to_tree())
+
+
+    def write_drawings(self):
+        if self.ws._charts or self.ws._images:
+            rel = Relationship(type="drawing", Target="")
+            self._rels.append(rel)
+            drawing = Related()
+            drawing.id = rel.id
+            self.xf.send(drawing.to_tree("drawing"))
+
+
+    def write_legacy(self):
+        """
+        Comments & VBA controls use VML and require an additional element
+        that is no longer in the specification.
+        """
+        if (self.ws.legacy_drawing is not None or self.ws._comments):
+            legacy = Related(id="anysvml")
+            self.xf.send(legacy.to_tree("legacyDrawing"))
+
+
+    def write_tables(self):
+        tables = TablePartList()
+
+        for table in self.ws.tables.values():
+            if not table.tableColumns:
+                table._initialise_columns()
+                if table.headerRowCount:
+                    try:
+                        row = self.ws[table.ref][0]
+                        for cell, col in zip(row, table.tableColumns):
+                            if cell.data_type != "s":
+                                warn("File may not be readable: column headings must be strings.")
+                            col.name = str(cell.value)
+                    except TypeError:
+                        warn("Column headings are missing, file may not be readable")
+            rel = Relationship(Type=table._rel_type, Target="")
+            self._rels.append(rel)
+            table._rel_id = rel.Id
+            tables.append(Related(id=rel.Id))
+
+        if tables:
+            self.xf.send(tables.to_tree())
+
+
+    def get_stream(self):
+        with xmlfile(self.out) as xf:
+            with xf.element("worksheet", xmlns=SHEET_MAIN_NS):
+                try:
+                    while True:
+                        el = (yield)
+                        if el is True:
+                            yield xf
+                        elif el is None: # et_xmlfile chokes
+                            continue
+                        else:
+                            xf.write(el)
+                except GeneratorExit:
+                    pass
+
+
+    def write_tail(self):
+        """
+        Write all elements after the rows
+        calc properties
+        protection
+        protected ranges #
+        scenarios
+        filters
+        sorts # always ignored
+        data consolidation #
+        custom views #
+        merged cells
+        phonetic properties #
+        conditional formatting
+        data validation
+        hyperlinks
+        print options
+        page margins
+        page setup
+        header
+        row breaks
+        col breaks
+        custom properties #
+        cell watches #
+        ignored errors #
+        smart tags #
+        drawing
+        drawingHF #
+        background #
+        OLE objects #
+        controls #
+        web publishing #
+        tables
+        """
+        self.write_protection()
+        self.write_scenarios()
+        self.write_filter()
+        self.write_merged_cells()
+        self.write_formatting()
+        self.write_validations()
+        self.write_hyperlinks()
+        self.write_print()
+        self.write_margins()
+        self.write_page()
+        self.write_header()
+        self.write_breaks()
+        self.write_drawings()
+        self.write_legacy()
+        self.write_tables()
+
+
+    def write(self):
+        """
+        High level
+        """
+        self.write_top()
+        self.write_rows()
+        self.write_tail()
+        self.close()
+
+
+    def close(self):
+        """
+        Close the context manager
+        """
+        if self.xf:
+            self.xf.close()
+
+
+    def read(self):
+        """
+        Close the context manager and return serialised XML
+        """
+        self.close()
+        if isinstance(self.out, BytesIO):
+            return self.out.getvalue()
+        with open(self.out, "rb") as src:
+            out = src.read()
+
+        return out
+
+
+    def cleanup(self):
+        """
+        Remove tempfile
+        """
+        os.remove(self.out)
+        ALL_TEMP_FILES.remove(self.out)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_range.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_range.py
new file mode 100644
index 00000000..2fbf5e22
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_range.py
@@ -0,0 +1,512 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from copy import copy
+from operator import attrgetter
+
+from openpyxl.descriptors import Strict
+from openpyxl.descriptors import MinMax
+from openpyxl.descriptors.sequence import UniqueSequence
+from openpyxl.descriptors.serialisable import Serialisable
+
+from openpyxl.utils import (
+    range_boundaries,
+    range_to_tuple,
+    get_column_letter,
+    quote_sheetname,
+)
+
+class CellRange(Serialisable):
+    """
+    Represents a range in a sheet: title and coordinates.
+
+    This object is used to perform operations on ranges, like:
+
+    - shift, expand or shrink
+    - union/intersection with another sheet range,
+
+    We can check whether a range is:
+
+    - equal or not equal to another,
+    - disjoint of another,
+    - contained in another.
+
+    We can get:
+
+    - the size of a range.
+    - the range bounds (vertices)
+    - the coordinates,
+    - the string representation,
+
+    """
+
+    min_col = MinMax(min=1, max=18278, expected_type=int)
+    min_row = MinMax(min=1, max=1048576, expected_type=int)
+    max_col = MinMax(min=1, max=18278, expected_type=int)
+    max_row = MinMax(min=1, max=1048576, expected_type=int)
+
+
+    def __init__(self, range_string=None, min_col=None, min_row=None,
+                 max_col=None, max_row=None, title=None):
+        if range_string is not None:
+            if "!" in range_string:
+                title, (min_col, min_row, max_col, max_row) = range_to_tuple(range_string)
+            else:
+                min_col, min_row, max_col, max_row = range_boundaries(range_string)
+
+        self.min_col = min_col
+        self.min_row = min_row
+        self.max_col = max_col
+        self.max_row = max_row
+        self.title = title
+
+        if min_col > max_col:
+            fmt = "{max_col} must be greater than {min_col}"
+            raise ValueError(fmt.format(min_col=min_col, max_col=max_col))
+        if min_row > max_row:
+            fmt = "{max_row} must be greater than {min_row}"
+            raise ValueError(fmt.format(min_row=min_row, max_row=max_row))
+
+
+    @property
+    def bounds(self):
+        """
+        Vertices of the range as a tuple
+        """
+        return self.min_col, self.min_row, self.max_col, self.max_row
+
+
+    @property
+    def coord(self):
+        """
+        Excel-style representation of the range
+        """
+        fmt = "{min_col}{min_row}:{max_col}{max_row}"
+        if (self.min_col == self.max_col
+            and self.min_row == self.max_row):
+            fmt = "{min_col}{min_row}"
+
+        return fmt.format(
+            min_col=get_column_letter(self.min_col),
+            min_row=self.min_row,
+            max_col=get_column_letter(self.max_col),
+            max_row=self.max_row
+        )
+
+    @property
+    def rows(self):
+        """
+        Return cell coordinates as rows
+        """
+        for row in range(self.min_row, self.max_row+1):
+            yield [(row, col) for col in range(self.min_col, self.max_col+1)]
+
+
+    @property
+    def cols(self):
+        """
+        Return cell coordinates as columns
+        """
+        for col in range(self.min_col, self.max_col+1):
+            yield [(row, col) for row in range(self.min_row, self.max_row+1)]
+
+
+    @property
+    def cells(self):
+        from itertools import product
+        return product(range(self.min_row, self.max_row+1), range(self.min_col, self.max_col+1))
+
+
+    def _check_title(self, other):
+        """
+        Check whether comparisons between ranges are possible.
+        Cannot compare ranges from different worksheets
+        Skip if the range passed in has no title.
+        """
+        if not isinstance(other, CellRange):
+            raise TypeError(repr(type(other)))
+
+        if other.title and self.title != other.title:
+            raise ValueError("Cannot work with ranges from different worksheets")
+
+
+    def __repr__(self):
+        fmt = u"<{cls} {coord}>"
+        if self.title:
+            fmt = u"<{cls} {title!r}!{coord}>"
+        return fmt.format(cls=self.__class__.__name__, title=self.title, coord=self.coord)
+
+
+    def __hash__(self):
+        return hash((self.min_row, self.min_col, self.max_row, self.max_col))
+
+
+    def __str__(self):
+        fmt = "{coord}"
+        title = self.title
+        if title:
+            fmt = u"{title}!{coord}"
+            title = quote_sheetname(title)
+        return fmt.format(title=title, coord=self.coord)
+
+
+    def __copy__(self):
+        return self.__class__(min_col=self.min_col, min_row=self.min_row,
+                              max_col=self.max_col, max_row=self.max_row,
+                              title=self.title)
+
+
+    def shift(self, col_shift=0, row_shift=0):
+        """
+        Shift the focus of the range according to the shift values (*col_shift*, *row_shift*).
+
+        :type col_shift: int
+        :param col_shift: number of columns to be moved by, can be negative
+        :type row_shift: int
+        :param row_shift: number of rows to be moved by, can be negative
+        :raise: :class:`ValueError` if any row or column index < 1
+        """
+
+        if (self.min_col + col_shift <= 0
+            or self.min_row + row_shift <= 0):
+            raise ValueError("Invalid shift value: col_shift={0}, row_shift={1}".format(col_shift, row_shift))
+        self.min_col += col_shift
+        self.min_row += row_shift
+        self.max_col += col_shift
+        self.max_row += row_shift
+
+
+    def __ne__(self, other):
+        """
+        Test whether the ranges are not equal.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* != *other*.
+        """
+        try:
+            self._check_title(other)
+        except ValueError:
+            return True
+
+        return (
+            other.min_row != self.min_row
+            or self.max_row != other.max_row
+            or other.min_col != self.min_col
+            or self.max_col != other.max_col
+        )
+
+
+    def __eq__(self, other):
+        """
+        Test whether the ranges are equal.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* == *other*.
+        """
+        return not self.__ne__(other)
+
+
+    def issubset(self, other):
+        """
+        Test whether every cell in this range is also in *other*.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* <= *other*.
+        """
+        self._check_title(other)
+
+        return other.__superset(self)
+
+    __le__ = issubset
+
+
+    def __lt__(self, other):
+        """
+        Test whether *other* contains every cell of this range, and more.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* < *other*.
+        """
+        return self.__le__(other) and self.__ne__(other)
+
+
+    def __superset(self, other):
+        return (
+            (self.min_row <= other.min_row <= other.max_row <= self.max_row)
+            and
+            (self.min_col <= other.min_col <= other.max_col <= self.max_col)
+        )
+
+
+    def issuperset(self, other):
+        """
+        Test whether every cell in *other* is in this range.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* >= *other* (or *other* in *range*).
+        """
+        self._check_title(other)
+
+        return self.__superset(other)
+
+    __ge__ = issuperset
+
+
+    def __contains__(self, coord):
+        """
+        Check whether the range contains a particular cell coordinate
+        """
+        cr = self.__class__(coord)
+        return self.__superset(cr)
+
+
+    def __gt__(self, other):
+        """
+        Test whether this range contains every cell in *other*, and more.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range
+        :return: ``True`` if *range* > *other*.
+        """
+        return self.__ge__(other) and self.__ne__(other)
+
+
+    def isdisjoint(self, other):
+        """
+        Return ``True`` if this range has no cell in common with *other*.
+        Ranges are disjoint if and only if their intersection is the empty range.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range.
+        :return: ``True`` if the range has no cells in common with other.
+        """
+        self._check_title(other)
+
+        # Sort by top-left vertex
+        if self.bounds > other.bounds:
+            self, other = other, self
+
+        return (self.max_col < other.min_col
+                or self.max_row < other.min_row
+                or other.max_row < self.min_row)
+
+
+    def intersection(self, other):
+        """
+        Return a new range with cells common to this range and *other*
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range.
+        :return: the intersecting sheet range.
+        :raise: :class:`ValueError` if the *other* range doesn't intersect
+            with this range.
+        """
+        if self.isdisjoint(other):
+            raise ValueError("Range {0} doesn't intersect {0}".format(self, other))
+
+        min_row = max(self.min_row, other.min_row)
+        max_row = min(self.max_row, other.max_row)
+        min_col = max(self.min_col, other.min_col)
+        max_col = min(self.max_col, other.max_col)
+
+        return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
+                         max_row=max_row)
+
+    __and__ = intersection
+
+
+    def union(self, other):
+        """
+        Return the minimal superset of this range and *other*. This new range
+        will contain all cells from this range, *other*, and any additional
+        cells required to form a rectangular ``CellRange``.
+
+        :type other: openpyxl.worksheet.cell_range.CellRange
+        :param other: Other sheet range.
+        :return: a ``CellRange`` that is a superset of this and *other*.
+        """
+        self._check_title(other)
+
+        min_row = min(self.min_row, other.min_row)
+        max_row = max(self.max_row, other.max_row)
+        min_col = min(self.min_col, other.min_col)
+        max_col = max(self.max_col, other.max_col)
+        return CellRange(min_col=min_col, min_row=min_row, max_col=max_col,
+                         max_row=max_row, title=self.title)
+
+    __or__ = union
+
+
+    def __iter__(self):
+        """
+        For use as a dictionary elsewhere in the library.
+        """
+        for x in self.__attrs__:
+            if x == "title":
+                continue
+            v = getattr(self, x)
+            yield x, v
+
+
+    def expand(self, right=0, down=0, left=0, up=0):
+        """
+        Expand the range by the dimensions provided.
+
+        :type right: int
+        :param right: expand range to the right by this number of cells
+        :type down: int
+        :param down: expand range down by this number of cells
+        :type left: int
+        :param left: expand range to the left by this number of cells
+        :type up: int
+        :param up: expand range up by this number of cells
+        """
+        self.min_col -= left
+        self.min_row -= up
+        self.max_col += right
+        self.max_row += down
+
+
+    def shrink(self, right=0, bottom=0, left=0, top=0):
+        """
+        Shrink the range by the dimensions provided.
+
+        :type right: int
+        :param right: shrink range from the right by this number of cells
+        :type down: int
+        :param down: shrink range from the top by this number of cells
+        :type left: int
+        :param left: shrink range from the left by this number of cells
+        :type up: int
+        :param up: shrink range from the bottom by this number of cells
+        """
+        self.min_col += left
+        self.min_row += top
+        self.max_col -= right
+        self.max_row -= bottom
+
+
+    @property
+    def size(self):
+        """ Return the size of the range as a dictionary of rows and columns. """
+        cols = self.max_col + 1 - self.min_col
+        rows = self.max_row + 1 - self.min_row
+        return {'columns':cols, 'rows':rows}
+
+
+    @property
+    def top(self):
+        """A list of cell coordinates that comprise the top of the range"""
+        return [(self.min_row, col) for col in range(self.min_col, self.max_col+1)]
+
+
+    @property
+    def bottom(self):
+        """A list of cell coordinates that comprise the bottom of the range"""
+        return [(self.max_row, col) for col in range(self.min_col, self.max_col+1)]
+
+
+    @property
+    def left(self):
+        """A list of cell coordinates that comprise the left-side of the range"""
+        return [(row, self.min_col) for row in range(self.min_row, self.max_row+1)]
+
+
+    @property
+    def right(self):
+        """A list of cell coordinates that comprise the right-side of the range"""
+        return [(row, self.max_col) for row in range(self.min_row, self.max_row+1)]
+
+
+class MultiCellRange(Strict):
+
+
+    ranges = UniqueSequence(expected_type=CellRange)
+
+
+    def __init__(self, ranges=set()):
+        if isinstance(ranges, str):
+            ranges = [CellRange(r) for r in ranges.split()]
+        self.ranges = set(ranges)
+
+
+    def __contains__(self, coord):
+        if isinstance(coord, str):
+            coord = CellRange(coord)
+        for r in self.ranges:
+            if coord <= r:
+                return True
+        return False
+
+
+    def __repr__(self):
+        ranges = " ".join([str(r) for r in self.sorted()])
+        return f"<{self.__class__.__name__} [{ranges}]>"
+
+
+    def __str__(self):
+        ranges = u" ".join([str(r) for r in self.sorted()])
+        return ranges
+
+
+    def __hash__(self):
+        return hash(str(self))
+
+
+    def sorted(self):
+        """
+        Return a sorted list of items
+        """
+        return sorted(self.ranges, key=attrgetter('min_col', 'min_row', 'max_col', 'max_row'))
+
+
+    def add(self, coord):
+        """
+        Add a cell coordinate or CellRange
+        """
+        cr = coord
+        if isinstance(coord, str):
+            cr = CellRange(coord)
+        elif not isinstance(coord, CellRange):
+            raise ValueError("You can only add CellRanges")
+        if cr not in self:
+            self.ranges.add(cr)
+
+
+    def __iadd__(self, coord):
+        self.add(coord)
+        return self
+
+
+    def __eq__(self, other):
+        if  isinstance(other, str):
+            other = self.__class__(other)
+        return self.ranges == other.ranges
+
+
+    def __ne__(self, other):
+        return not self == other
+
+
+    def __bool__(self):
+        return bool(self.ranges)
+
+
+    def remove(self, coord):
+        if not isinstance(coord, CellRange):
+            coord = CellRange(coord)
+        self.ranges.remove(coord)
+
+
+    def __iter__(self):
+        for cr in self.ranges:
+            yield cr
+
+
+    def __copy__(self):
+        ranges = {copy(r) for r in self.ranges}
+        return MultiCellRange(ranges)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_watch.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_watch.py
new file mode 100644
index 00000000..dea89caf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/cell_watch.py
@@ -0,0 +1,34 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Sequence,
+    String,
+)
+
+# could be done using a nestedSequence
+
+class CellWatch(Serialisable):
+
+    tagname = "cellWatch"
+
+    r = String()
+
+    def __init__(self,
+                 r=None,
+                ):
+        self.r = r
+
+
+class CellWatches(Serialisable):
+
+    tagname = "cellWatches"
+
+    cellWatch = Sequence(expected_type=CellWatch)
+
+    __elements__ = ('cellWatch',)
+
+    def __init__(self,
+                 cellWatch=(),
+                ):
+        self.cellWatch = cellWatch
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/controls.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/controls.py
new file mode 100644
index 00000000..f1fd1c9e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/controls.py
@@ -0,0 +1,107 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Bool,
+    Integer,
+    String,
+    Sequence,
+)
+
+from openpyxl.descriptors.excel import Relation
+from .ole import ObjectAnchor
+
+
+class ControlProperty(Serialisable):
+
+    tagname = "controlPr"
+
+    anchor = Typed(expected_type=ObjectAnchor, )
+    locked = Bool(allow_none=True)
+    defaultSize = Bool(allow_none=True)
+    _print = Bool(allow_none=True)
+    disabled = Bool(allow_none=True)
+    recalcAlways = Bool(allow_none=True)
+    uiObject = Bool(allow_none=True)
+    autoFill = Bool(allow_none=True)
+    autoLine = Bool(allow_none=True)
+    autoPict = Bool(allow_none=True)
+    macro = String(allow_none=True)
+    altText = String(allow_none=True)
+    linkedCell = String(allow_none=True)
+    listFillRange = String(allow_none=True)
+    cf = String(allow_none=True)
+    id = Relation(allow_none=True)
+
+    __elements__ = ('anchor',)
+
+    def __init__(self,
+                 anchor=None,
+                 locked=True,
+                 defaultSize=True,
+                 _print=True,
+                 disabled=False,
+                 recalcAlways=False,
+                 uiObject=False,
+                 autoFill=True,
+                 autoLine=True,
+                 autoPict=True,
+                 macro=None,
+                 altText=None,
+                 linkedCell=None,
+                 listFillRange=None,
+                 cf='pict',
+                 id=None,
+                ):
+        self.anchor = anchor
+        self.locked = locked
+        self.defaultSize = defaultSize
+        self._print = _print
+        self.disabled = disabled
+        self.recalcAlways = recalcAlways
+        self.uiObject = uiObject
+        self.autoFill = autoFill
+        self.autoLine = autoLine
+        self.autoPict = autoPict
+        self.macro = macro
+        self.altText = altText
+        self.linkedCell = linkedCell
+        self.listFillRange = listFillRange
+        self.cf = cf
+        self.id = id
+
+
+class Control(Serialisable):
+
+    tagname = "control"
+
+    controlPr = Typed(expected_type=ControlProperty, allow_none=True)
+    shapeId = Integer()
+    name = String(allow_none=True)
+
+    __elements__ = ('controlPr',)
+
+    def __init__(self,
+                 controlPr=None,
+                 shapeId=None,
+                 name=None,
+                ):
+        self.controlPr = controlPr
+        self.shapeId = shapeId
+        self.name = name
+
+
+class Controls(Serialisable):
+
+    tagname = "controls"
+
+    control = Sequence(expected_type=Control)
+
+    __elements__ = ('control',)
+
+    def __init__(self,
+                 control=(),
+                ):
+        self.control = control
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/copier.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/copier.py
new file mode 100644
index 00000000..f6601540
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/copier.py
@@ -0,0 +1,70 @@
+# Copyright (c) 2010-2024 openpyxl
+
+#standard lib imports
+from copy import copy
+
+from .worksheet import Worksheet
+
+
+class WorksheetCopy:
+    """
+    Copy the values, styles, dimensions, merged cells, margins, and
+    print/page setup from one worksheet to another within the same
+    workbook.
+    """
+
+    def __init__(self, source_worksheet, target_worksheet):
+        self.source = source_worksheet
+        self.target = target_worksheet
+        self._verify_resources()
+
+
+    def _verify_resources(self):
+
+        if (not isinstance(self.source, Worksheet)
+            and not isinstance(self.target, Worksheet)):
+            raise TypeError("Can only copy worksheets")
+
+        if self.source is self.target:
+            raise ValueError("Cannot copy a worksheet to itself")
+
+        if self.source.parent != self.target.parent:
+            raise ValueError('Cannot copy between worksheets from different workbooks')
+
+
+    def copy_worksheet(self):
+        self._copy_cells()
+        self._copy_dimensions()
+
+        self.target.sheet_format = copy(self.source.sheet_format)
+        self.target.sheet_properties = copy(self.source.sheet_properties)
+        self.target.merged_cells = copy(self.source.merged_cells)
+        self.target.page_margins = copy(self.source.page_margins)
+        self.target.page_setup = copy(self.source.page_setup)
+        self.target.print_options = copy(self.source.print_options)
+
+
+    def _copy_cells(self):
+        for (row, col), source_cell  in self.source._cells.items():
+            target_cell = self.target.cell(column=col, row=row)
+
+            target_cell._value = source_cell._value
+            target_cell.data_type = source_cell.data_type
+
+            if source_cell.has_style:
+                target_cell._style = copy(source_cell._style)
+
+            if source_cell.hyperlink:
+                target_cell._hyperlink = copy(source_cell.hyperlink)
+
+            if source_cell.comment:
+                target_cell.comment = copy(source_cell.comment)
+
+
+    def _copy_dimensions(self):
+        for attr in ('row_dimensions', 'column_dimensions'):
+            src = getattr(self.source, attr)
+            target = getattr(self.target, attr)
+            for key, dim in src.items():
+                target[key] = copy(dim)
+                target[key].worksheet = self.target
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/custom.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/custom.py
new file mode 100644
index 00000000..b3af5c91
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/custom.py
@@ -0,0 +1,35 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    String,
+    Sequence,
+)
+
+# can be done with a nested sequence
+
+
+class CustomProperty(Serialisable):
+
+    tagname = "customProperty"
+
+    name = String()
+
+    def __init__(self,
+                 name=None,
+                ):
+        self.name = name
+
+
+class CustomProperties(Serialisable):
+
+    tagname = "customProperties"
+
+    customPr = Sequence(expected_type=CustomProperty)
+
+    __elements__ = ('customPr',)
+
+    def __init__(self,
+                 customPr=(),
+                ):
+        self.customPr = customPr
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/datavalidation.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/datavalidation.py
new file mode 100644
index 00000000..f5077d97
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/datavalidation.py
@@ -0,0 +1,202 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from collections import defaultdict
+from itertools import chain
+from operator import itemgetter
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Bool,
+    NoneSet,
+    String,
+    Sequence,
+    Alias,
+    Integer,
+    Convertible,
+)
+from openpyxl.descriptors.nested import NestedText
+
+from openpyxl.utils import (
+    rows_from_range,
+    coordinate_to_tuple,
+    get_column_letter,
+)
+
+
+def collapse_cell_addresses(cells, input_ranges=()):
+    """ Collapse a collection of cell co-ordinates down into an optimal
+        range or collection of ranges.
+
+        E.g. Cells A1, A2, A3, B1, B2 and B3 should have the data-validation
+        object applied, attempt to collapse down to a single range, A1:B3.
+
+        Currently only collapsing contiguous vertical ranges (i.e. above
+        example results in A1:A3 B1:B3).
+    """
+
+    ranges = list(input_ranges)
+
+    # convert cell into row, col tuple
+    raw_coords = (coordinate_to_tuple(cell) for cell in cells)
+
+    # group by column in order
+    grouped_coords = defaultdict(list)
+    for row, col in sorted(raw_coords, key=itemgetter(1)):
+        grouped_coords[col].append(row)
+
+    # create range string from first and last row in column
+    for col, cells in grouped_coords.items():
+        col = get_column_letter(col)
+        fmt = "{0}{1}:{2}{3}"
+        if len(cells) == 1:
+            fmt = "{0}{1}"
+        r = fmt.format(col, min(cells), col, max(cells))
+        ranges.append(r)
+
+    return " ".join(ranges)
+
+
+def expand_cell_ranges(range_string):
+    """
+    Expand cell ranges to a sequence of addresses.
+    Reverse of collapse_cell_addresses
+    Eg. converts "A1:A2 B1:B2" to (A1, A2, B1, B2)
+    """
+    # expand ranges to rows and then flatten
+    rows = (rows_from_range(rs) for rs in range_string.split()) # list of rows
+    cells = (chain(*row) for row in rows) # flatten rows
+    return set(chain(*cells))
+
+
+from .cell_range import MultiCellRange
+
+
+class DataValidation(Serialisable):
+
+    tagname = "dataValidation"
+
+    sqref = Convertible(expected_type=MultiCellRange)
+    cells = Alias("sqref")
+    ranges = Alias("sqref")
+
+    showDropDown = Bool(allow_none=True)
+    hide_drop_down = Alias('showDropDown')
+    showInputMessage = Bool(allow_none=True)
+    showErrorMessage = Bool(allow_none=True)
+    allowBlank = Bool(allow_none=True)
+    allow_blank = Alias('allowBlank')
+
+    errorTitle = String(allow_none = True)
+    error = String(allow_none = True)
+    promptTitle = String(allow_none = True)
+    prompt = String(allow_none = True)
+    formula1 = NestedText(allow_none=True, expected_type=str)
+    formula2 = NestedText(allow_none=True, expected_type=str)
+
+    type = NoneSet(values=("whole", "decimal", "list", "date", "time",
+                           "textLength", "custom"))
+    errorStyle = NoneSet(values=("stop", "warning", "information"))
+    imeMode = NoneSet(values=("noControl", "off", "on", "disabled",
+                              "hiragana", "fullKatakana", "halfKatakana", "fullAlpha","halfAlpha",
+                              "fullHangul", "halfHangul"))
+    operator = NoneSet(values=("between", "notBetween", "equal", "notEqual",
+                               "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual"))
+    validation_type = Alias('type')
+
+    def __init__(self,
+                 type=None,
+                 formula1=None,
+                 formula2=None,
+                 showErrorMessage=False,
+                 showInputMessage=False,
+                 showDropDown=False,
+                 allowBlank=False,
+                 sqref=(),
+                 promptTitle=None,
+                 errorStyle=None,
+                 error=None,
+                 prompt=None,
+                 errorTitle=None,
+                 imeMode=None,
+                 operator=None,
+                 allow_blank=None,
+                 ):
+        self.sqref = sqref
+        self.showDropDown = showDropDown
+        self.imeMode = imeMode
+        self.operator = operator
+        self.formula1 = formula1
+        self.formula2 = formula2
+        if allow_blank is not None:
+            allowBlank = allow_blank
+        self.allowBlank = allowBlank
+        self.showErrorMessage = showErrorMessage
+        self.showInputMessage = showInputMessage
+        self.type = type
+        self.promptTitle = promptTitle
+        self.errorStyle = errorStyle
+        self.error = error
+        self.prompt = prompt
+        self.errorTitle = errorTitle
+
+
+    def add(self, cell):
+        """Adds a cell or cell coordinate to this validator"""
+        if hasattr(cell, "coordinate"):
+            cell = cell.coordinate
+        self.sqref += cell
+
+
+    def __contains__(self, cell):
+        if hasattr(cell, "coordinate"):
+            cell = cell.coordinate
+        return cell in self.sqref
+
+
+class DataValidationList(Serialisable):
+
+    tagname = "dataValidations"
+
+    disablePrompts = Bool(allow_none=True)
+    xWindow = Integer(allow_none=True)
+    yWindow = Integer(allow_none=True)
+    dataValidation = Sequence(expected_type=DataValidation)
+
+    __elements__ = ('dataValidation',)
+    __attrs__ = ('disablePrompts', 'xWindow', 'yWindow', 'count')
+
+    def __init__(self,
+                 disablePrompts=None,
+                 xWindow=None,
+                 yWindow=None,
+                 count=None,
+                 dataValidation=(),
+                ):
+        self.disablePrompts = disablePrompts
+        self.xWindow = xWindow
+        self.yWindow = yWindow
+        self.dataValidation = dataValidation
+
+
+    @property
+    def count(self):
+        return len(self)
+
+
+    def __len__(self):
+        return len(self.dataValidation)
+
+
+    def append(self, dv):
+        self.dataValidation.append(dv)
+
+
+    def to_tree(self, tagname=None):
+        """
+        Need to skip validations that have no cell ranges
+        """
+        ranges = self.dataValidation # copy
+        self.dataValidation = [r for r in self.dataValidation if bool(r.sqref)]
+        xml = super().to_tree(tagname)
+        self.dataValidation = ranges
+        return xml
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/dimensions.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/dimensions.py
new file mode 100644
index 00000000..482717a1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/dimensions.py
@@ -0,0 +1,306 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from copy import copy
+
+from openpyxl.compat import safe_string
+from openpyxl.utils import (
+    get_column_letter,
+    get_column_interval,
+    column_index_from_string,
+    range_boundaries,
+)
+from openpyxl.utils.units import DEFAULT_COLUMN_WIDTH
+from openpyxl.descriptors import (
+    Integer,
+    Float,
+    Bool,
+    Strict,
+    String,
+    Alias,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.styles.styleable import StyleableObject
+from openpyxl.utils.bound_dictionary import BoundDictionary
+from openpyxl.xml.functions import Element
+
+
+class Dimension(Strict, StyleableObject):
+    """Information about the display properties of a row or column."""
+    __fields__ = ('hidden',
+                 'outlineLevel',
+                 'collapsed',)
+
+    index = Integer()
+    hidden = Bool()
+    outlineLevel = Integer(allow_none=True)
+    outline_level = Alias('outlineLevel')
+    collapsed = Bool()
+    style = Alias('style_id')
+
+
+    def __init__(self, index, hidden, outlineLevel,
+                 collapsed, worksheet, visible=True, style=None):
+        super().__init__(sheet=worksheet, style_array=style)
+        self.index = index
+        self.hidden = hidden
+        self.outlineLevel = outlineLevel
+        self.collapsed = collapsed
+
+
+    def __iter__(self):
+        for key in self.__fields__:
+            value = getattr(self, key, None)
+            if value:
+                yield key, safe_string(value)
+
+
+    def __copy__(self):
+        cp = self.__new__(self.__class__)
+        attrib = self.__dict__
+        attrib['worksheet'] = self.parent
+        cp.__init__(**attrib)
+        cp._style = copy(self._style)
+        return cp
+
+
+    def __repr__(self):
+        return f"<{self.__class__.__name__} Instance, Attributes={dict(self)}>"
+
+
+class RowDimension(Dimension):
+    """Information about the display properties of a row."""
+
+    __fields__ = Dimension.__fields__ + ('ht', 'customFormat', 'customHeight', 's',
+                                         'thickBot', 'thickTop')
+    r = Alias('index')
+    s = Alias('style_id')
+    ht = Float(allow_none=True)
+    height = Alias('ht')
+    thickBot = Bool()
+    thickTop = Bool()
+
+    def __init__(self,
+                 worksheet,
+                 index=0,
+                 ht=None,
+                 customHeight=None, # do not write
+                 s=None,
+                 customFormat=None, # do not write
+                 hidden=False,
+                 outlineLevel=0,
+                 outline_level=None,
+                 collapsed=False,
+                 visible=None,
+                 height=None,
+                 r=None,
+                 spans=None,
+                 thickBot=None,
+                 thickTop=None,
+                 **kw
+                 ):
+        if r is not None:
+            index = r
+        if height is not None:
+            ht = height
+        self.ht = ht
+        if visible is not None:
+            hidden = not visible
+        if outline_level is not None:
+            outlineLevel = outline_level
+        self.thickBot = thickBot
+        self.thickTop = thickTop
+        super().__init__(index, hidden, outlineLevel,
+                                           collapsed, worksheet, style=s)
+
+    @property
+    def customFormat(self):
+        """Always true if there is a style for the row"""
+        return self.has_style
+
+    @property
+    def customHeight(self):
+        """Always true if there is a height for the row"""
+        return self.ht is not None
+
+
+class ColumnDimension(Dimension):
+    """Information about the display properties of a column."""
+
+    width = Float()
+    bestFit = Bool()
+    auto_size = Alias('bestFit')
+    index = String()
+    min = Integer(allow_none=True)
+    max = Integer(allow_none=True)
+    collapsed = Bool()
+
+    __fields__ = Dimension.__fields__ + ('width', 'bestFit', 'customWidth', 'style',
+                                         'min', 'max')
+
+    def __init__(self,
+                 worksheet,
+                 index='A',
+                 width=DEFAULT_COLUMN_WIDTH,
+                 bestFit=False,
+                 hidden=False,
+                 outlineLevel=0,
+                 outline_level=None,
+                 collapsed=False,
+                 style=None,
+                 min=None,
+                 max=None,
+                 customWidth=False, # do not write
+                 visible=None,
+                 auto_size=None,):
+        self.width = width
+        self.min = min
+        self.max = max
+        if visible is not None:
+            hidden = not visible
+        if auto_size is not None:
+            bestFit = auto_size
+        self.bestFit = bestFit
+        if outline_level is not None:
+            outlineLevel = outline_level
+        self.collapsed = collapsed
+        super().__init__(index, hidden, outlineLevel,
+                                              collapsed, worksheet, style=style)
+
+
+    @property
+    def customWidth(self):
+        """Always true if there is a width for the column"""
+        return bool(self.width)
+
+
+    def reindex(self):
+        """
+        Set boundaries for column definition
+        """
+        if not all([self.min, self.max]):
+            self.min = self.max = column_index_from_string(self.index)
+
+    @property
+    def range(self):
+        """Return the range of cells actually covered"""
+        return f"{get_column_letter(self.min)}:{get_column_letter(self.max)}"
+
+
+    def to_tree(self):
+        attrs = dict(self)
+        if attrs.keys() != {'min', 'max'}:
+            return Element("col", **attrs)
+
+
+class DimensionHolder(BoundDictionary):
+    """
+    Allow columns to be grouped
+    """
+
+    def __init__(self, worksheet, reference="index", default_factory=None):
+        self.worksheet = worksheet
+        self.max_outline = None
+        self.default_factory = default_factory
+        super().__init__(reference, default_factory)
+
+
+    def group(self, start, end=None, outline_level=1, hidden=False):
+        """allow grouping a range of consecutive rows or columns together
+
+        :param start: first row or column to be grouped (mandatory)
+        :param end: last row or column to be grouped (optional, default to start)
+        :param outline_level: outline level
+        :param hidden: should the group be hidden on workbook open or not
+        """
+        if end is None:
+            end = start
+
+        if isinstance(self.default_factory(), ColumnDimension):
+            new_dim = self[start]
+            new_dim.outline_level = outline_level
+            new_dim.hidden = hidden
+            work_sequence = get_column_interval(start, end)[1:]
+            for column_letter in work_sequence:
+                if column_letter in self:
+                    del self[column_letter]
+            new_dim.min, new_dim.max = map(column_index_from_string, (start, end))
+        elif isinstance(self.default_factory(), RowDimension):
+            for el in range(start, end + 1):
+                new_dim = self.worksheet.row_dimensions[el]
+                new_dim.outline_level = outline_level
+                new_dim.hidden = hidden
+
+
+    def to_tree(self):
+
+        def sorter(value):
+            value.reindex()
+            return value.min
+
+        el = Element('cols')
+        outlines = set()
+
+        for col in sorted(self.values(), key=sorter):
+            obj = col.to_tree()
+            if obj is not None:
+                outlines.add(col.outlineLevel)
+                el.append(obj)
+
+        if outlines:
+            self.max_outline = max(outlines)
+
+        if len(el):
+            return el # must have at least one child
+
+
+class SheetFormatProperties(Serialisable):
+
+    tagname = "sheetFormatPr"
+
+    baseColWidth = Integer(allow_none=True)
+    defaultColWidth = Float(allow_none=True)
+    defaultRowHeight = Float()
+    customHeight = Bool(allow_none=True)
+    zeroHeight = Bool(allow_none=True)
+    thickTop = Bool(allow_none=True)
+    thickBottom = Bool(allow_none=True)
+    outlineLevelRow = Integer(allow_none=True)
+    outlineLevelCol = Integer(allow_none=True)
+
+    def __init__(self,
+                 baseColWidth=8, #according to spec
+                 defaultColWidth=None,
+                 defaultRowHeight=15,
+                 customHeight=None,
+                 zeroHeight=None,
+                 thickTop=None,
+                 thickBottom=None,
+                 outlineLevelRow=None,
+                 outlineLevelCol=None,
+                ):
+        self.baseColWidth = baseColWidth
+        self.defaultColWidth = defaultColWidth
+        self.defaultRowHeight = defaultRowHeight
+        self.customHeight = customHeight
+        self.zeroHeight = zeroHeight
+        self.thickTop = thickTop
+        self.thickBottom = thickBottom
+        self.outlineLevelRow = outlineLevelRow
+        self.outlineLevelCol = outlineLevelCol
+
+
+class SheetDimension(Serialisable):
+
+    tagname = "dimension"
+
+    ref = String()
+
+    def __init__(self,
+                 ref=None,
+                ):
+        self.ref = ref
+
+
+    @property
+    def boundaries(self):
+        return range_boundaries(self.ref)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/drawing.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/drawing.py
new file mode 100644
index 00000000..45bf4d35
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/drawing.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.excel import Relation
+
+
+class Drawing(Serialisable):
+
+    tagname = "drawing"
+
+    id = Relation()
+
+    def __init__(self, id=None):
+        self.id = id
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/errors.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/errors.py
new file mode 100644
index 00000000..1bed3f78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/errors.py
@@ -0,0 +1,93 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    String,
+    Bool,
+    Sequence,
+)
+from openpyxl.descriptors.excel import CellRange
+
+
+class Extension(Serialisable):
+
+    tagname = "extension"
+
+    uri = String(allow_none=True)
+
+    def __init__(self,
+                 uri=None,
+                ):
+        self.uri = uri
+
+
+class ExtensionList(Serialisable):
+
+    tagname = "extensionList"
+
+    # uses element group EG_ExtensionList
+    ext = Sequence(expected_type=Extension)
+
+    __elements__ = ('ext',)
+
+    def __init__(self,
+                 ext=(),
+                ):
+        self.ext = ext
+
+
+class IgnoredError(Serialisable):
+
+    tagname = "ignoredError"
+
+    sqref = CellRange
+    evalError = Bool(allow_none=True)
+    twoDigitTextYear = Bool(allow_none=True)
+    numberStoredAsText = Bool(allow_none=True)
+    formula = Bool(allow_none=True)
+    formulaRange = Bool(allow_none=True)
+    unlockedFormula = Bool(allow_none=True)
+    emptyCellReference = Bool(allow_none=True)
+    listDataValidation = Bool(allow_none=True)
+    calculatedColumn = Bool(allow_none=True)
+
+    def __init__(self,
+                 sqref=None,
+                 evalError=False,
+                 twoDigitTextYear=False,
+                 numberStoredAsText=False,
+                 formula=False,
+                 formulaRange=False,
+                 unlockedFormula=False,
+                 emptyCellReference=False,
+                 listDataValidation=False,
+                 calculatedColumn=False,
+                ):
+        self.sqref = sqref
+        self.evalError = evalError
+        self.twoDigitTextYear = twoDigitTextYear
+        self.numberStoredAsText = numberStoredAsText
+        self.formula = formula
+        self.formulaRange = formulaRange
+        self.unlockedFormula = unlockedFormula
+        self.emptyCellReference = emptyCellReference
+        self.listDataValidation = listDataValidation
+        self.calculatedColumn = calculatedColumn
+
+
+class IgnoredErrors(Serialisable):
+
+    tagname = "ignoredErrors"
+
+    ignoredError = Sequence(expected_type=IgnoredError)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('ignoredError', 'extLst')
+
+    def __init__(self,
+                 ignoredError=(),
+                 extLst=None,
+                ):
+        self.ignoredError = ignoredError
+        self.extLst = extLst
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/filters.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/filters.py
new file mode 100644
index 00000000..a2cfd8eb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/filters.py
@@ -0,0 +1,486 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import re
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Alias,
+    Typed,
+    Set,
+    Float,
+    DateTime,
+    NoneSet,
+    Bool,
+    Integer,
+    String,
+    Sequence,
+    MinMax,
+)
+from openpyxl.descriptors.excel import ExtensionList, CellRange
+from openpyxl.descriptors.sequence import ValueSequence
+from openpyxl.utils import absolute_coordinate
+
+
+class SortCondition(Serialisable):
+
+    tagname = "sortCondition"
+
+    descending = Bool(allow_none=True)
+    sortBy = NoneSet(values=(['value', 'cellColor', 'fontColor', 'icon']))
+    ref = CellRange()
+    customList = String(allow_none=True)
+    dxfId = Integer(allow_none=True)
+    iconSet = NoneSet(values=(['3Arrows', '3ArrowsGray', '3Flags',
+                           '3TrafficLights1', '3TrafficLights2', '3Signs', '3Symbols', '3Symbols2',
+                           '4Arrows', '4ArrowsGray', '4RedToBlack', '4Rating', '4TrafficLights',
+                           '5Arrows', '5ArrowsGray', '5Rating', '5Quarters']))
+    iconId = Integer(allow_none=True)
+
+    def __init__(self,
+                 ref=None,
+                 descending=None,
+                 sortBy=None,
+                 customList=None,
+                 dxfId=None,
+                 iconSet=None,
+                 iconId=None,
+                ):
+        self.descending = descending
+        self.sortBy = sortBy
+        self.ref = ref
+        self.customList = customList
+        self.dxfId = dxfId
+        self.iconSet = iconSet
+        self.iconId = iconId
+
+
+class SortState(Serialisable):
+
+    tagname = "sortState"
+
+    columnSort = Bool(allow_none=True)
+    caseSensitive = Bool(allow_none=True)
+    sortMethod = NoneSet(values=(['stroke', 'pinYin']))
+    ref = CellRange()
+    sortCondition = Sequence(expected_type=SortCondition, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('sortCondition',)
+
+    def __init__(self,
+                 columnSort=None,
+                 caseSensitive=None,
+                 sortMethod=None,
+                 ref=None,
+                 sortCondition=(),
+                 extLst=None,
+                ):
+        self.columnSort = columnSort
+        self.caseSensitive = caseSensitive
+        self.sortMethod = sortMethod
+        self.ref = ref
+        self.sortCondition = sortCondition
+
+
+    def __bool__(self):
+        return self.ref is not None
+
+
+
+class IconFilter(Serialisable):
+
+    tagname = "iconFilter"
+
+    iconSet = Set(values=(['3Arrows', '3ArrowsGray', '3Flags',
+                           '3TrafficLights1', '3TrafficLights2', '3Signs', '3Symbols', '3Symbols2',
+                           '4Arrows', '4ArrowsGray', '4RedToBlack', '4Rating', '4TrafficLights',
+                           '5Arrows', '5ArrowsGray', '5Rating', '5Quarters']))
+    iconId = Integer(allow_none=True)
+
+    def __init__(self,
+                 iconSet=None,
+                 iconId=None,
+                ):
+        self.iconSet = iconSet
+        self.iconId = iconId
+
+
+class ColorFilter(Serialisable):
+
+    tagname = "colorFilter"
+
+    dxfId = Integer(allow_none=True)
+    cellColor = Bool(allow_none=True)
+
+    def __init__(self,
+                 dxfId=None,
+                 cellColor=None,
+                ):
+        self.dxfId = dxfId
+        self.cellColor = cellColor
+
+
+class DynamicFilter(Serialisable):
+
+    tagname = "dynamicFilter"
+
+    type = Set(values=(['null', 'aboveAverage', 'belowAverage', 'tomorrow',
+                        'today', 'yesterday', 'nextWeek', 'thisWeek', 'lastWeek', 'nextMonth',
+                        'thisMonth', 'lastMonth', 'nextQuarter', 'thisQuarter', 'lastQuarter',
+                        'nextYear', 'thisYear', 'lastYear', 'yearToDate', 'Q1', 'Q2', 'Q3', 'Q4',
+                        'M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10', 'M11',
+                        'M12']))
+    val = Float(allow_none=True)
+    valIso = DateTime(allow_none=True)
+    maxVal = Float(allow_none=True)
+    maxValIso = DateTime(allow_none=True)
+
+    def __init__(self,
+                 type=None,
+                 val=None,
+                 valIso=None,
+                 maxVal=None,
+                 maxValIso=None,
+                ):
+        self.type = type
+        self.val = val
+        self.valIso = valIso
+        self.maxVal = maxVal
+        self.maxValIso = maxValIso
+
+
+class CustomFilter(Serialisable):
+
+    tagname = "customFilter"
+
+    val = String()
+    operator = Set(values=['equal', 'lessThan', 'lessThanOrEqual',
+                           'notEqual', 'greaterThanOrEqual', 'greaterThan'])
+
+    def __init__(self, operator="equal", val=None):
+        self.operator = operator
+        self.val = val
+
+
+    def _get_subtype(self):
+        if self.val == " ":
+            subtype = BlankFilter
+        else:
+            try:
+                float(self.val)
+                subtype = NumberFilter
+            except ValueError:
+                subtype = StringFilter
+        return subtype
+
+
+    def convert(self):
+        """Convert to more specific filter"""
+        typ = self._get_subtype()
+        if typ in (BlankFilter, NumberFilter):
+            return typ(**dict(self))
+
+        operator, term = StringFilter._guess_operator(self.val)
+        flt = StringFilter(operator, term)
+        if self.operator == "notEqual":
+            flt.exclude = True
+        return flt
+
+
+class BlankFilter(CustomFilter):
+    """
+    Exclude blanks
+    """
+
+    __attrs__ = ("operator", "val")
+
+    def __init__(self, **kw):
+        pass
+
+
+    @property
+    def operator(self):
+        return "notEqual"
+
+
+    @property
+    def val(self):
+        return " "
+
+
+class NumberFilter(CustomFilter):
+
+
+    operator = Set(values=
+                   ['equal', 'lessThan', 'lessThanOrEqual',
+                    'notEqual', 'greaterThanOrEqual', 'greaterThan'])
+    val = Float()
+
+    def __init__(self, operator="equal", val=None):
+        self.operator = operator
+        self.val = val
+
+
+string_format_mapping = {
+    "contains": "*{}*",
+    "startswith": "{}*",
+    "endswith": "*{}",
+    "wildcard":  "{}",
+}
+
+
+class StringFilter(CustomFilter):
+
+    operator = Set(values=['contains', 'startswith', 'endswith', 'wildcard']
+                   )
+    val = String()
+    exclude = Bool()
+
+
+    def __init__(self, operator="contains", val=None, exclude=False):
+        self.operator = operator
+        self.val = val
+        self.exclude = exclude
+
+
+    def _escape(self):
+        """Escape wildcards ~, * ? when serialising"""
+        if self.operator == "wildcard":
+            return self.val
+        return re.sub(r"~|\*|\?", r"~\g<0>", self.val)
+
+
+    @staticmethod
+    def _unescape(value):
+        """
+        Unescape value
+        """
+        return re.sub(r"~(?P<op>[~*?])", r"\g<op>", value)
+
+
+    @staticmethod
+    def _guess_operator(value):
+        value = StringFilter._unescape(value)
+        endswith = r"^(?P<endswith>\*)(?P<term>[^\*\?]*$)"
+        startswith = r"^(?P<term>[^\*\?]*)(?P<startswith>\*)$"
+        contains = r"^(?P<contains>\*)(?P<term>[^\*\?]*)\*$"
+        d = {"wildcard": True, "term": value}
+        for pat in [contains, startswith, endswith]:
+            m = re.match(pat, value)
+            if m:
+                d = m.groupdict()
+
+        term = d.pop("term")
+        op = list(d)[0]
+        return op, term
+
+
+    def to_tree(self, tagname=None, idx=None, namespace=None):
+        fmt = string_format_mapping[self.operator]
+        op = self.exclude and "notEqual" or "equal"
+        value = fmt.format(self._escape())
+        flt = CustomFilter(op, value)
+        return flt.to_tree(tagname, idx, namespace)
+
+
+class CustomFilters(Serialisable):
+
+    tagname = "customFilters"
+
+    _and = Bool(allow_none=True)
+    customFilter = Sequence(expected_type=CustomFilter) # min 1, max 2
+
+    __elements__ = ('customFilter',)
+
+    def __init__(self,
+                 _and=None,
+                 customFilter=(),
+                ):
+        self._and = _and
+        self.customFilter = customFilter
+
+
+class Top10(Serialisable):
+
+    tagname = "top10"
+
+    top = Bool(allow_none=True)
+    percent = Bool(allow_none=True)
+    val = Float()
+    filterVal = Float(allow_none=True)
+
+    def __init__(self,
+                 top=None,
+                 percent=None,
+                 val=None,
+                 filterVal=None,
+                ):
+        self.top = top
+        self.percent = percent
+        self.val = val
+        self.filterVal = filterVal
+
+
+class DateGroupItem(Serialisable):
+
+    tagname = "dateGroupItem"
+
+    year = Integer()
+    month = MinMax(min=1, max=12, allow_none=True)
+    day = MinMax(min=1, max=31, allow_none=True)
+    hour = MinMax(min=0, max=23, allow_none=True)
+    minute = MinMax(min=0, max=59, allow_none=True)
+    second = Integer(min=0, max=59, allow_none=True)
+    dateTimeGrouping = Set(values=(['year', 'month', 'day', 'hour', 'minute',
+                                    'second']))
+
+    def __init__(self,
+                 year=None,
+                 month=None,
+                 day=None,
+                 hour=None,
+                 minute=None,
+                 second=None,
+                 dateTimeGrouping=None,
+                ):
+        self.year = year
+        self.month = month
+        self.day = day
+        self.hour = hour
+        self.minute = minute
+        self.second = second
+        self.dateTimeGrouping = dateTimeGrouping
+
+
+class Filters(Serialisable):
+
+    tagname = "filters"
+
+    blank = Bool(allow_none=True)
+    calendarType = NoneSet(values=["gregorian","gregorianUs",
+                                   "gregorianMeFrench","gregorianArabic", "hijri","hebrew",
+                                   "taiwan","japan", "thai","korea",
+                                   "saka","gregorianXlitEnglish","gregorianXlitFrench"])
+    filter = ValueSequence(expected_type=str)
+    dateGroupItem = Sequence(expected_type=DateGroupItem, allow_none=True)
+
+    __elements__ = ('filter', 'dateGroupItem')
+
+    def __init__(self,
+                 blank=None,
+                 calendarType=None,
+                 filter=(),
+                 dateGroupItem=(),
+                ):
+        self.blank = blank
+        self.calendarType = calendarType
+        self.filter = filter
+        self.dateGroupItem = dateGroupItem
+
+
+class FilterColumn(Serialisable):
+
+    tagname = "filterColumn"
+
+    colId = Integer()
+    col_id = Alias('colId')
+    hiddenButton = Bool(allow_none=True)
+    showButton = Bool(allow_none=True)
+    # some elements are choice
+    filters = Typed(expected_type=Filters, allow_none=True)
+    top10 = Typed(expected_type=Top10, allow_none=True)
+    customFilters = Typed(expected_type=CustomFilters, allow_none=True)
+    dynamicFilter = Typed(expected_type=DynamicFilter, allow_none=True)
+    colorFilter = Typed(expected_type=ColorFilter, allow_none=True)
+    iconFilter = Typed(expected_type=IconFilter, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('filters', 'top10', 'customFilters', 'dynamicFilter',
+                    'colorFilter', 'iconFilter')
+
+    def __init__(self,
+                 colId=None,
+                 hiddenButton=False,
+                 showButton=True,
+                 filters=None,
+                 top10=None,
+                 customFilters=None,
+                 dynamicFilter=None,
+                 colorFilter=None,
+                 iconFilter=None,
+                 extLst=None,
+                 blank=None,
+                 vals=None,
+                ):
+        self.colId = colId
+        self.hiddenButton = hiddenButton
+        self.showButton = showButton
+        self.filters = filters
+        self.top10 = top10
+        self.customFilters = customFilters
+        self.dynamicFilter = dynamicFilter
+        self.colorFilter = colorFilter
+        self.iconFilter = iconFilter
+        if blank is not None and self.filters:
+            self.filters.blank = blank
+        if vals is not None and self.filters:
+            self.filters.filter = vals
+
+
+class AutoFilter(Serialisable):
+
+    tagname = "autoFilter"
+
+    ref = CellRange()
+    filterColumn = Sequence(expected_type=FilterColumn, allow_none=True)
+    sortState = Typed(expected_type=SortState, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('filterColumn', 'sortState')
+
+    def __init__(self,
+                 ref=None,
+                 filterColumn=(),
+                 sortState=None,
+                 extLst=None,
+                ):
+        self.ref = ref
+        self.filterColumn = filterColumn
+        self.sortState = sortState
+
+
+    def __bool__(self):
+        return self.ref is not None
+
+
+    def __str__(self):
+        return absolute_coordinate(self.ref)
+
+
+    def add_filter_column(self, col_id, vals, blank=False):
+        """
+        Add row filter for specified column.
+
+        :param col_id: Zero-origin column id. 0 means first column.
+        :type  col_id: int
+        :param vals: Value list to show.
+        :type  vals: str[]
+        :param blank: Show rows that have blank cell if True (default=``False``)
+        :type  blank: bool
+        """
+        self.filterColumn.append(FilterColumn(colId=col_id, filters=Filters(blank=blank, filter=vals)))
+
+
+    def add_sort_condition(self, ref, descending=False):
+        """
+        Add sort condition for cpecified range of cells.
+
+        :param ref: range of the cells (e.g. 'A2:A150')
+        :type  ref: string, is the same as that of the filter
+        :param descending: Descending sort order (default=``False``)
+        :type  descending: bool
+        """
+        cond = SortCondition(ref, descending)
+        if self.sortState is None:
+            self.sortState = SortState(ref=self.ref)
+        self.sortState.sortCondition.append(cond)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/formula.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/formula.py
new file mode 100644
index 00000000..7eb920e9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/formula.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.compat import safe_string
+
+class DataTableFormula:
+
+
+    t = "dataTable"
+
+    def __init__(self,
+                 ref,
+                 ca=False,
+                 dt2D=False,
+                 dtr=False,
+                 r1=None,
+                 r2=None,
+                 del1=False,
+                 del2=False,
+                 **kw):
+        self.ref = ref
+        self.ca = ca
+        self.dt2D = dt2D
+        self.dtr = dtr
+        self.r1 = r1
+        self.r2 = r2
+        self.del1 = del1
+        self.del2 = del2
+
+
+    def __iter__(self):
+        for k in ["t", "ref", "dt2D", "dtr", "r1", "r2", "del1", "del2", "ca"]:
+            v = getattr(self, k)
+            if v:
+                yield k, safe_string(v)
+
+
+class ArrayFormula:
+
+    t = "array"
+
+
+    def __init__(self, ref, text=None):
+        self.ref = ref
+        self.text = text
+
+
+    def __iter__(self):
+        for k in ["t", "ref"]:
+            v = getattr(self, k)
+            if v:
+                yield k, safe_string(v)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/header_footer.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/header_footer.py
new file mode 100644
index 00000000..598aa23d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/header_footer.py
@@ -0,0 +1,270 @@
+# Copyright (c) 2010-2024 openpyxl
+
+# Simplified implementation of headers and footers: let worksheets have separate items
+
+import re
+from warnings import warn
+
+from openpyxl.descriptors import (
+    Alias,
+    Bool,
+    Strict,
+    String,
+    Integer,
+    MatchPattern,
+    Typed,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+from openpyxl.xml.functions import Element
+from openpyxl.utils.escape import escape, unescape
+
+
+FONT_PATTERN = '&"(?P<font>.+)"'
+COLOR_PATTERN  = "&K(?P<color>[A-F0-9]{6})"
+SIZE_REGEX = r"&(?P<size>\d+\s?)"
+FORMAT_REGEX = re.compile("{0}|{1}|{2}".format(FONT_PATTERN, COLOR_PATTERN,
+                                               SIZE_REGEX)
+                          )
+
+def _split_string(text):
+    """
+    Split the combined (decoded) string into left, center and right parts
+
+    # See http://stackoverflow.com/questions/27711175/regex-with-multiple-optional-groups for discussion
+    """
+
+    ITEM_REGEX = re.compile("""
+    (&L(?P<left>.+?))?
+    (&C(?P<center>.+?))?
+    (&R(?P<right>.+?))?
+    $""", re.VERBOSE | re.DOTALL)
+
+    m = ITEM_REGEX.match(text)
+    try:
+        parts = m.groupdict()
+    except AttributeError:
+        warn("""Cannot parse header or footer so it will be ignored""")
+        parts = {'left':'', 'right':'', 'center':''}
+    return parts
+
+
+class _HeaderFooterPart(Strict):
+
+    """
+    Individual left/center/right header/footer part
+
+    Do not use directly.
+
+    Header & Footer ampersand codes:
+
+    * &A   Inserts the worksheet name
+    * &B   Toggles bold
+    * &D or &[Date]   Inserts the current date
+    * &E   Toggles double-underline
+    * &F or &[File]   Inserts the workbook name
+    * &I   Toggles italic
+    * &N or &[Pages]   Inserts the total page count
+    * &S   Toggles strikethrough
+    * &T   Inserts the current time
+    * &[Tab]   Inserts the worksheet name
+    * &U   Toggles underline
+    * &X   Toggles superscript
+    * &Y   Toggles subscript
+    * &P or &[Page]   Inserts the current page number
+    * &P+n   Inserts the page number incremented by n
+    * &P-n   Inserts the page number decremented by n
+    * &[Path]   Inserts the workbook path
+    * &&   Escapes the ampersand character
+    * &"fontname"   Selects the named font
+    * &nn   Selects the specified 2-digit font point size
+
+    Colours are in RGB Hex
+    """
+
+    text = String(allow_none=True)
+    font = String(allow_none=True)
+    size = Integer(allow_none=True)
+    RGB = ("^[A-Fa-f0-9]{6}$")
+    color = MatchPattern(allow_none=True, pattern=RGB)
+
+
+    def __init__(self, text=None, font=None, size=None, color=None):
+        self.text = text
+        self.font = font
+        self.size = size
+        self.color = color
+
+
+    def __str__(self):
+        """
+        Convert to Excel HeaderFooter miniformat minus position
+        """
+        fmt = []
+        if self.font:
+            fmt.append(u'&"{0}"'.format(self.font))
+        if self.size:
+            fmt.append("&{0} ".format(self.size))
+        if self.color:
+            fmt.append("&K{0}".format(self.color))
+        return u"".join(fmt + [self.text])
+
+    def __bool__(self):
+        return bool(self.text)
+
+
+
+    @classmethod
+    def from_str(cls, text):
+        """
+        Convert from miniformat to object
+        """
+        keys = ('font', 'color', 'size')
+        kw = dict((k, v) for match in FORMAT_REGEX.findall(text)
+                  for k, v in zip(keys, match) if v)
+
+        kw['text'] = FORMAT_REGEX.sub('', text)
+
+        return cls(**kw)
+
+
+class HeaderFooterItem(Strict):
+    """
+    Header or footer item
+
+    """
+
+    left = Typed(expected_type=_HeaderFooterPart)
+    center = Typed(expected_type=_HeaderFooterPart)
+    centre = Alias("center")
+    right = Typed(expected_type=_HeaderFooterPart)
+
+    __keys = ('L', 'C', 'R')
+
+
+    def __init__(self, left=None, right=None, center=None):
+        if left is None:
+            left = _HeaderFooterPart()
+        self.left = left
+        if center is None:
+            center = _HeaderFooterPart()
+        self.center = center
+        if right is None:
+            right = _HeaderFooterPart()
+        self.right = right
+
+
+    def __str__(self):
+        """
+        Pack parts into a single string
+        """
+        TRANSFORM = {'&[Tab]': '&A', '&[Pages]': '&N', '&[Date]': '&D',
+                     '&[Path]': '&Z', '&[Page]': '&P', '&[Time]': '&T', '&[File]': '&F',
+                     '&[Picture]': '&G'}
+
+        # escape keys and create regex
+        SUBS_REGEX = re.compile("|".join(["({0})".format(re.escape(k))
+                                          for k in TRANSFORM]))
+
+        def replace(match):
+            """
+            Callback for re.sub
+            Replace expanded control with mini-format equivalent
+            """
+            sub = match.group(0)
+            return TRANSFORM[sub]
+
+        txt = []
+        for key, part in zip(
+            self.__keys, [self.left, self.center, self.right]):
+            if part.text is not None:
+                txt.append(u"&{0}{1}".format(key, str(part)))
+        txt = "".join(txt)
+        txt = SUBS_REGEX.sub(replace, txt)
+        return escape(txt)
+
+
+    def __bool__(self):
+        return any([self.left, self.center, self.right])
+
+
+
+    def to_tree(self, tagname):
+        """
+        Return as XML node
+        """
+        el = Element(tagname)
+        el.text = str(self)
+        return el
+
+
+    @classmethod
+    def from_tree(cls, node):
+        if node.text:
+            text = unescape(node.text)
+            parts = _split_string(text)
+            for k, v in parts.items():
+                if v is not None:
+                    parts[k] = _HeaderFooterPart.from_str(v)
+            self = cls(**parts)
+            return self
+
+
+class HeaderFooter(Serialisable):
+
+    tagname = "headerFooter"
+
+    differentOddEven = Bool(allow_none=True)
+    differentFirst = Bool(allow_none=True)
+    scaleWithDoc = Bool(allow_none=True)
+    alignWithMargins = Bool(allow_none=True)
+    oddHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
+    oddFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
+    evenHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
+    evenFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
+    firstHeader = Typed(expected_type=HeaderFooterItem, allow_none=True)
+    firstFooter = Typed(expected_type=HeaderFooterItem, allow_none=True)
+
+    __elements__ = ("oddHeader", "oddFooter", "evenHeader", "evenFooter", "firstHeader", "firstFooter")
+
+    def __init__(self,
+                 differentOddEven=None,
+                 differentFirst=None,
+                 scaleWithDoc=None,
+                 alignWithMargins=None,
+                 oddHeader=None,
+                 oddFooter=None,
+                 evenHeader=None,
+                 evenFooter=None,
+                 firstHeader=None,
+                 firstFooter=None,
+                ):
+        self.differentOddEven = differentOddEven
+        self.differentFirst = differentFirst
+        self.scaleWithDoc = scaleWithDoc
+        self.alignWithMargins = alignWithMargins
+        if oddHeader is None:
+            oddHeader = HeaderFooterItem()
+        self.oddHeader = oddHeader
+        if oddFooter is None:
+            oddFooter = HeaderFooterItem()
+        self.oddFooter = oddFooter
+        if evenHeader is None:
+            evenHeader = HeaderFooterItem()
+        self.evenHeader = evenHeader
+        if evenFooter is None:
+            evenFooter = HeaderFooterItem()
+        self.evenFooter = evenFooter
+        if firstHeader is None:
+            firstHeader = HeaderFooterItem()
+        self.firstHeader = firstHeader
+        if firstFooter is None:
+            firstFooter = HeaderFooterItem()
+        self.firstFooter = firstFooter
+
+
+    def __bool__(self):
+        parts = [getattr(self, attr) for attr in self.__attrs__ + self.__elements__]
+        return any(parts)
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/hyperlink.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/hyperlink.py
new file mode 100644
index 00000000..332b4154
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/hyperlink.py
@@ -0,0 +1,46 @@
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    String,
+    Sequence,
+)
+from openpyxl.descriptors.excel import Relation
+
+
+class Hyperlink(Serialisable):
+
+    tagname = "hyperlink"
+
+    ref = String()
+    location = String(allow_none=True)
+    tooltip = String(allow_none=True)
+    display = String(allow_none=True)
+    id = Relation()
+    target = String(allow_none=True)
+
+    __attrs__ = ("ref", "location", "tooltip", "display", "id")
+
+    def __init__(self,
+                 ref=None,
+                 location=None,
+                 tooltip=None,
+                 display=None,
+                 id=None,
+                 target=None,
+                ):
+        self.ref = ref
+        self.location = location
+        self.tooltip = tooltip
+        self.display = display
+        self.id = id
+        self.target = target
+
+
+class HyperlinkList(Serialisable):
+
+    tagname = "hyperlinks"
+
+    __expected_type = Hyperlink
+    hyperlink = Sequence(expected_type=__expected_type)
+
+    def __init__(self, hyperlink=()):
+        self.hyperlink = hyperlink
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/merge.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/merge.py
new file mode 100644
index 00000000..a3a6bebd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/merge.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import copy
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Integer,
+    Sequence,
+)
+
+from openpyxl.cell.cell import MergedCell
+from openpyxl.styles.borders import Border
+
+from .cell_range import CellRange
+
+
+class MergeCell(CellRange):
+
+    tagname = "mergeCell"
+    ref = CellRange.coord
+
+    __attrs__ = ("ref",)
+
+
+    def __init__(self,
+                 ref=None,
+                ):
+        super().__init__(ref)
+
+
+    def __copy__(self):
+        return self.__class__(self.ref)
+
+
+class MergeCells(Serialisable):
+
+    tagname = "mergeCells"
+
+    count = Integer(allow_none=True)
+    mergeCell = Sequence(expected_type=MergeCell, )
+
+    __elements__ = ('mergeCell',)
+    __attrs__ = ('count',)
+
+    def __init__(self,
+                 count=None,
+                 mergeCell=(),
+                ):
+        self.mergeCell = mergeCell
+
+
+    @property
+    def count(self):
+        return len(self.mergeCell)
+
+
+class MergedCellRange(CellRange):
+
+    """
+    MergedCellRange stores the border information of a merged cell in the top
+    left cell of the merged cell.
+    The remaining cells in the merged cell are stored as MergedCell objects and
+    get their border information from the upper left cell.
+    """
+
+    def __init__(self, worksheet, coord):
+        self.ws = worksheet
+        super().__init__(range_string=coord)
+        self.start_cell = None
+        self._get_borders()
+
+
+    def _get_borders(self):
+        """
+        If the upper left cell of the merged cell does not yet exist, it is
+        created.
+        The upper left cell gets the border information of the bottom and right
+        border from the bottom right cell of the merged cell, if available.
+        """
+
+        # Top-left cell.
+        self.start_cell = self.ws._cells.get((self.min_row, self.min_col))
+        if self.start_cell is None:
+            self.start_cell = self.ws.cell(row=self.min_row, column=self.min_col)
+
+        # Bottom-right cell
+        end_cell = self.ws._cells.get((self.max_row, self.max_col))
+        if end_cell is not None:
+            self.start_cell.border += Border(right=end_cell.border.right,
+                                             bottom=end_cell.border.bottom)
+
+
+    def format(self):
+        """
+        Each cell of the merged cell is created as MergedCell if it does not
+        already exist.
+
+        The MergedCells at the edge of the merged cell gets its borders from
+        the upper left cell.
+
+         - The top MergedCells get the top border from the top left cell.
+         - The bottom MergedCells get the bottom border from the top left cell.
+         - The left MergedCells get the left border from the top left cell.
+         - The right MergedCells get the right border from the top left cell.
+        """
+
+        names = ['top', 'left', 'right', 'bottom']
+
+        for name in names:
+            side = getattr(self.start_cell.border, name)
+            if side and side.style is None:
+                continue # don't need to do anything if there is no border style
+            border = Border(**{name:side})
+            for coord in getattr(self, name):
+                cell = self.ws._cells.get(coord)
+                if cell is None:
+                    row, col = coord
+                    cell = MergedCell(self.ws, row=row, column=col)
+                    self.ws._cells[(cell.row, cell.column)] = cell
+                cell.border += border
+
+        protected = self.start_cell.protection is not None
+        if protected:
+            protection = copy.copy(self.start_cell.protection)
+        for coord in self.cells:
+            cell = self.ws._cells.get(coord)
+            if cell is None:
+                row, col = coord
+                cell = MergedCell(self.ws, row=row, column=col)
+                self.ws._cells[(cell.row, cell.column)] = cell
+
+            if protected:
+                cell.protection = protection
+
+
+    def __contains__(self, coord):
+        return coord in CellRange(self.coord)
+
+
+    def __copy__(self):
+        return self.__class__(self.ws, self.coord)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/ole.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/ole.py
new file mode 100644
index 00000000..61dc0048
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/ole.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Typed,
+    Integer,
+    String,
+    Set,
+    Bool,
+    Sequence,
+)
+
+from openpyxl.drawing.spreadsheet_drawing import AnchorMarker
+from openpyxl.xml.constants import SHEET_DRAWING_NS
+
+
+class ObjectAnchor(Serialisable):
+
+    tagname = "anchor"
+
+    _from = Typed(expected_type=AnchorMarker, namespace=SHEET_DRAWING_NS)
+    to = Typed(expected_type=AnchorMarker, namespace=SHEET_DRAWING_NS)
+    moveWithCells = Bool(allow_none=True)
+    sizeWithCells = Bool(allow_none=True)
+    z_order = Integer(allow_none=True, hyphenated=True)
+
+
+    def __init__(self,
+                 _from=None,
+                 to=None,
+                 moveWithCells=False,
+                 sizeWithCells=False,
+                 z_order=None,
+                ):
+        self._from = _from
+        self.to = to
+        self.moveWithCells = moveWithCells
+        self.sizeWithCells = sizeWithCells
+        self.z_order = z_order
+
+
+class ObjectPr(Serialisable):
+
+    tagname = "objectPr"
+
+    anchor = Typed(expected_type=ObjectAnchor, )
+    locked = Bool(allow_none=True)
+    defaultSize = Bool(allow_none=True)
+    _print = Bool(allow_none=True)
+    disabled = Bool(allow_none=True)
+    uiObject = Bool(allow_none=True)
+    autoFill = Bool(allow_none=True)
+    autoLine = Bool(allow_none=True)
+    autoPict = Bool(allow_none=True)
+    macro = String()
+    altText = String(allow_none=True)
+    dde = Bool(allow_none=True)
+
+    __elements__ = ('anchor',)
+
+    def __init__(self,
+                 anchor=None,
+                 locked=True,
+                 defaultSize=True,
+                 _print=True,
+                 disabled=False,
+                 uiObject=False,
+                 autoFill=True,
+                 autoLine=True,
+                 autoPict=True,
+                 macro=None,
+                 altText=None,
+                 dde=False,
+                ):
+        self.anchor = anchor
+        self.locked = locked
+        self.defaultSize = defaultSize
+        self._print = _print
+        self.disabled = disabled
+        self.uiObject = uiObject
+        self.autoFill = autoFill
+        self.autoLine = autoLine
+        self.autoPict = autoPict
+        self.macro = macro
+        self.altText = altText
+        self.dde = dde
+
+
+class OleObject(Serialisable):
+
+    tagname = "oleObject"
+
+    objectPr = Typed(expected_type=ObjectPr, allow_none=True)
+    progId = String(allow_none=True)
+    dvAspect = Set(values=(['DVASPECT_CONTENT', 'DVASPECT_ICON']))
+    link = String(allow_none=True)
+    oleUpdate = Set(values=(['OLEUPDATE_ALWAYS', 'OLEUPDATE_ONCALL']))
+    autoLoad = Bool(allow_none=True)
+    shapeId = Integer()
+
+    __elements__ = ('objectPr',)
+
+    def __init__(self,
+                 objectPr=None,
+                 progId=None,
+                 dvAspect='DVASPECT_CONTENT',
+                 link=None,
+                 oleUpdate=None,
+                 autoLoad=False,
+                 shapeId=None,
+                ):
+        self.objectPr = objectPr
+        self.progId = progId
+        self.dvAspect = dvAspect
+        self.link = link
+        self.oleUpdate = oleUpdate
+        self.autoLoad = autoLoad
+        self.shapeId = shapeId
+
+
+class OleObjects(Serialisable):
+
+    tagname = "oleObjects"
+
+    oleObject = Sequence(expected_type=OleObject)
+
+    __elements__ = ('oleObject',)
+
+    def __init__(self,
+                 oleObject=(),
+                ):
+        self.oleObject = oleObject
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/page.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/page.py
new file mode 100644
index 00000000..7d630c2c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/page.py
@@ -0,0 +1,174 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Float,
+    Bool,
+    Integer,
+    NoneSet,
+    )
+from openpyxl.descriptors.excel import UniversalMeasure, Relation
+
+
+class PrintPageSetup(Serialisable):
+    """ Worksheet print page setup """
+
+    tagname = "pageSetup"
+
+    orientation = NoneSet(values=("default", "portrait", "landscape"))
+    paperSize = Integer(allow_none=True)
+    scale = Integer(allow_none=True)
+    fitToHeight = Integer(allow_none=True)
+    fitToWidth = Integer(allow_none=True)
+    firstPageNumber = Integer(allow_none=True)
+    useFirstPageNumber = Bool(allow_none=True)
+    paperHeight = UniversalMeasure(allow_none=True)
+    paperWidth = UniversalMeasure(allow_none=True)
+    pageOrder = NoneSet(values=("downThenOver", "overThenDown"))
+    usePrinterDefaults = Bool(allow_none=True)
+    blackAndWhite = Bool(allow_none=True)
+    draft = Bool(allow_none=True)
+    cellComments = NoneSet(values=("asDisplayed", "atEnd"))
+    errors = NoneSet(values=("displayed", "blank", "dash", "NA"))
+    horizontalDpi = Integer(allow_none=True)
+    verticalDpi = Integer(allow_none=True)
+    copies = Integer(allow_none=True)
+    id = Relation()
+
+
+    def __init__(self,
+                 worksheet=None,
+                 orientation=None,
+                 paperSize=None,
+                 scale=None,
+                 fitToHeight=None,
+                 fitToWidth=None,
+                 firstPageNumber=None,
+                 useFirstPageNumber=None,
+                 paperHeight=None,
+                 paperWidth=None,
+                 pageOrder=None,
+                 usePrinterDefaults=None,
+                 blackAndWhite=None,
+                 draft=None,
+                 cellComments=None,
+                 errors=None,
+                 horizontalDpi=None,
+                 verticalDpi=None,
+                 copies=None,
+                 id=None):
+        self._parent = worksheet
+        self.orientation = orientation
+        self.paperSize = paperSize
+        self.scale = scale
+        self.fitToHeight = fitToHeight
+        self.fitToWidth = fitToWidth
+        self.firstPageNumber = firstPageNumber
+        self.useFirstPageNumber = useFirstPageNumber
+        self.paperHeight = paperHeight
+        self.paperWidth = paperWidth
+        self.pageOrder = pageOrder
+        self.usePrinterDefaults = usePrinterDefaults
+        self.blackAndWhite = blackAndWhite
+        self.draft = draft
+        self.cellComments = cellComments
+        self.errors = errors
+        self.horizontalDpi = horizontalDpi
+        self.verticalDpi = verticalDpi
+        self.copies = copies
+        self.id = id
+
+
+    def __bool__(self):
+        return bool(dict(self))
+
+
+
+
+    @property
+    def sheet_properties(self):
+        """
+        Proxy property
+        """
+        return self._parent.sheet_properties.pageSetUpPr
+
+
+    @property
+    def fitToPage(self):
+        return self.sheet_properties.fitToPage
+
+
+    @fitToPage.setter
+    def fitToPage(self, value):
+        self.sheet_properties.fitToPage = value
+
+
+    @property
+    def autoPageBreaks(self):
+        return self.sheet_properties.autoPageBreaks
+
+
+    @autoPageBreaks.setter
+    def autoPageBreaks(self, value):
+        self.sheet_properties.autoPageBreaks = value
+
+
+    @classmethod
+    def from_tree(cls, node):
+        self = super().from_tree(node)
+        self.id = None # strip link to binary settings
+        return self
+
+
+class PrintOptions(Serialisable):
+    """ Worksheet print options """
+
+    tagname = "printOptions"
+    horizontalCentered = Bool(allow_none=True)
+    verticalCentered = Bool(allow_none=True)
+    headings = Bool(allow_none=True)
+    gridLines = Bool(allow_none=True)
+    gridLinesSet = Bool(allow_none=True)
+
+    def __init__(self, horizontalCentered=None,
+                 verticalCentered=None,
+                 headings=None,
+                 gridLines=None,
+                 gridLinesSet=None,
+                 ):
+        self.horizontalCentered = horizontalCentered
+        self.verticalCentered = verticalCentered
+        self.headings = headings
+        self.gridLines = gridLines
+        self.gridLinesSet = gridLinesSet
+
+
+    def __bool__(self):
+        return bool(dict(self))
+
+
+class PageMargins(Serialisable):
+    """
+    Information about page margins for view/print layouts.
+    Standard values (in inches)
+    left, right = 0.75
+    top, bottom = 1
+    header, footer = 0.5
+    """
+    tagname = "pageMargins"
+
+    left = Float()
+    right = Float()
+    top = Float()
+    bottom = Float()
+    header = Float()
+    footer = Float()
+
+    def __init__(self, left=0.75, right=0.75, top=1, bottom=1, header=0.5,
+                 footer=0.5):
+        self.left = left
+        self.right = right
+        self.top = top
+        self.bottom = bottom
+        self.header = header
+        self.footer = footer
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/pagebreak.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/pagebreak.py
new file mode 100644
index 00000000..ad50a321
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/pagebreak.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Integer,
+    Bool,
+    Sequence,
+)
+
+
+class Break(Serialisable):
+
+    tagname = "brk"
+
+    id = Integer(allow_none=True)
+    min = Integer(allow_none=True)
+    max = Integer(allow_none=True)
+    man = Bool(allow_none=True)
+    pt = Bool(allow_none=True)
+
+    def __init__(self,
+                 id=0,
+                 min=0,
+                 max=16383,
+                 man=True,
+                 pt=None,
+                ):
+        self.id = id
+        self.min = min
+        self.max = max
+        self.man = man
+        self.pt = pt
+
+
+class RowBreak(Serialisable):
+
+    tagname = "rowBreaks"
+
+    count = Integer(allow_none=True)
+    manualBreakCount = Integer(allow_none=True)
+    brk = Sequence(expected_type=Break, allow_none=True)
+
+    __elements__ = ('brk',)
+    __attrs__ = ("count", "manualBreakCount",)
+
+    def __init__(self,
+                 count=None,
+                 manualBreakCount=None,
+                 brk=(),
+                ):
+        self.brk = brk
+
+
+    def __bool__(self):
+        return len(self.brk) > 0
+
+
+    def __len__(self):
+        return len(self.brk)
+
+
+    @property
+    def count(self):
+        return len(self)
+
+
+    @property
+    def manualBreakCount(self):
+        return len(self)
+
+
+    def append(self, brk=None):
+        """
+        Add a page break
+        """
+        vals = list(self.brk)
+        if not isinstance(brk, Break):
+            brk = Break(id=self.count+1)
+        vals.append(brk)
+        self.brk = vals
+
+
+PageBreak = RowBreak
+
+
+class ColBreak(RowBreak):
+
+    tagname = "colBreaks"
+
+    count = RowBreak.count
+    manualBreakCount = RowBreak.manualBreakCount
+    brk = RowBreak.brk
+
+    __attrs__ = RowBreak.__attrs__
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/picture.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/picture.py
new file mode 100644
index 00000000..8fff338a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/picture.py
@@ -0,0 +1,8 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+
+# same as related
+
+class SheetBackgroundPicture(Serialisable):
+
+    tagname = "sheetBackgroundPicture"
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/print_settings.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/print_settings.py
new file mode 100644
index 00000000..b4629df1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/print_settings.py
@@ -0,0 +1,184 @@
+# Copyright (c) 2010-2024 openpyxl
+
+import re
+from openpyxl.descriptors import (
+    Strict,
+    Integer,
+    String,
+    Typed,
+)
+from openpyxl.utils import quote_sheetname, absolute_coordinate
+from openpyxl.utils.cell import SHEET_TITLE, SHEETRANGE_RE, RANGE_EXPR
+
+from .cell_range import MultiCellRange
+
+COL_RANGE = r"""(?P<cols>[$]?(?P<min_col>[a-zA-Z]{1,3}):[$]?(?P<max_col>[a-zA-Z]{1,3}))"""
+COL_RANGE_RE = re.compile(COL_RANGE)
+ROW_RANGE = r"""(?P<rows>[$]?(?P<min_row>\d+):[$]?(?P<max_row>\d+))"""
+ROW_RANGE_RE = re.compile(ROW_RANGE)
+TITLES_REGEX = re.compile("""{0}{1}?,?{2}?,?""".format(SHEET_TITLE, ROW_RANGE, COL_RANGE),
+                          re.VERBOSE)
+PRINT_AREA_RE = re.compile(f"({SHEET_TITLE})?(?P<cells>{RANGE_EXPR})", re.VERBOSE)
+
+class ColRange(Strict):
+    """
+    Represent a range of at least one column
+    """
+
+    min_col = String()
+    max_col = String()
+
+
+    def __init__(self, range_string=None, min_col=None, max_col=None):
+        if range_string is not None:
+            match = COL_RANGE_RE.match(range_string)
+            if not match:
+                raise ValueError(f"{range_string} is not a valid column range")
+            min_col, max_col = match.groups()[1:]
+        self.min_col = min_col
+        self.max_col = max_col
+
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return (self.min_col == other.min_col
+                    and
+                    self.max_col == other.max_col)
+        elif isinstance(other, str):
+            return (str(self) == other
+                    or
+                    f"{self.min_col}:{self.max_col}")
+        return False
+
+
+    def __repr__(self):
+        return f"Range of columns from '{self.min_col}' to '{self.max_col}'"
+
+
+    def __str__(self):
+        return f"${self.min_col}:${self.max_col}"
+
+
+class RowRange(Strict):
+    """
+    Represent a range of at least one row
+    """
+
+    min_row = Integer()
+    max_row = Integer()
+
+    def __init__(self, range_string=None, min_row=None, max_row=None):
+        if range_string is not None:
+            match = ROW_RANGE_RE.match(range_string)
+            if not match:
+                raise ValueError(f"{range_string} is not a valid row range")
+            min_row, max_row = match.groups()[1:]
+        self.min_row = min_row
+        self.max_row = max_row
+
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return (self.min_row == other.min_row
+                    and
+                    self.max_row == other.max_row)
+        elif isinstance(other, str):
+            return (str(self) == other
+                    or
+                    f"{self.min_row}:{self.max_row}")
+        return False
+
+    def __repr__(self):
+        return f"Range of rows from '{self.min_row}' to '{self.max_row}'"
+
+
+    def __str__(self):
+        return f"${self.min_row}:${self.max_row}"
+
+
+class PrintTitles(Strict):
+    """
+    Contains at least either a range of rows or columns
+    """
+
+    cols = Typed(expected_type=ColRange, allow_none=True)
+    rows = Typed(expected_type=RowRange, allow_none=True)
+    title = String()
+
+
+    def __init__(self, cols=None, rows=None, title=""):
+        self.cols = cols
+        self.rows = rows
+        self.title = title
+
+
+    @classmethod
+    def from_string(cls, value):
+        kw = dict((k, v) for match in TITLES_REGEX.finditer(value)
+                  for k, v in match.groupdict().items() if v)
+
+        if not kw:
+            raise ValueError(f"{value} is not a valid print titles definition")
+
+        cols = rows = None
+
+        if "cols" in kw:
+            cols = ColRange(kw["cols"])
+        if "rows" in kw:
+            rows = RowRange(kw["rows"])
+
+        title = kw.get("quoted") or kw.get("notquoted")
+
+        return cls(cols=cols, rows=rows, title=title)
+
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return (self.cols == other.cols
+                    and
+                    self.rows == other.rows
+                    and
+                    self.title == other.title)
+        elif isinstance(other, str):
+            return str(self) == other
+        return False
+
+    def __repr__(self):
+        return f"Print titles for sheet {self.title} cols {self.rows}, rows {self.cols}"
+
+
+    def __str__(self):
+        title = quote_sheetname(self.title)
+        titles = ",".join([f"{title}!{value}" for value in (self.rows, self.cols) if value])
+        return titles or ""
+
+
+class PrintArea(MultiCellRange):
+
+
+    @classmethod
+    def from_string(cls, value):
+        new = []
+        for m in PRINT_AREA_RE.finditer(value): # can be multiple
+            coord = m.group("cells")
+            if coord:
+                new.append(coord)
+        return cls(new)
+
+
+    def __init__(self, ranges=(), title=""):
+        self.title = ""
+        super().__init__(ranges)
+
+
+    def __str__(self):
+        if self.ranges:
+            return ",".join([f"{quote_sheetname(self.title)}!{absolute_coordinate(str(range))}"
+                             for range in self.sorted()])
+        return ""
+
+
+    def __eq__(self, other):
+        super().__eq__(other)
+        if isinstance(other, str):
+            return str(self) == other
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/properties.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/properties.py
new file mode 100644
index 00000000..e16d15be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/properties.py
@@ -0,0 +1,97 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Worksheet Properties"""
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import String, Bool, Typed
+from openpyxl.styles.colors import ColorDescriptor
+
+
+class Outline(Serialisable):
+
+    tagname = "outlinePr"
+
+    applyStyles = Bool(allow_none=True)
+    summaryBelow = Bool(allow_none=True)
+    summaryRight = Bool(allow_none=True)
+    showOutlineSymbols = Bool(allow_none=True)
+
+
+    def __init__(self,
+                 applyStyles=None,
+                 summaryBelow=None,
+                 summaryRight=None,
+                 showOutlineSymbols=None
+                 ):
+        self.applyStyles = applyStyles
+        self.summaryBelow = summaryBelow
+        self.summaryRight = summaryRight
+        self.showOutlineSymbols = showOutlineSymbols
+
+
+class PageSetupProperties(Serialisable):
+
+    tagname = "pageSetUpPr"
+
+    autoPageBreaks = Bool(allow_none=True)
+    fitToPage = Bool(allow_none=True)
+
+    def __init__(self, autoPageBreaks=None, fitToPage=None):
+        self.autoPageBreaks = autoPageBreaks
+        self.fitToPage = fitToPage
+
+
+class WorksheetProperties(Serialisable):
+
+    tagname = "sheetPr"
+
+    codeName = String(allow_none=True)
+    enableFormatConditionsCalculation = Bool(allow_none=True)
+    filterMode = Bool(allow_none=True)
+    published = Bool(allow_none=True)
+    syncHorizontal = Bool(allow_none=True)
+    syncRef = String(allow_none=True)
+    syncVertical = Bool(allow_none=True)
+    transitionEvaluation = Bool(allow_none=True)
+    transitionEntry = Bool(allow_none=True)
+    tabColor = ColorDescriptor(allow_none=True)
+    outlinePr = Typed(expected_type=Outline, allow_none=True)
+    pageSetUpPr = Typed(expected_type=PageSetupProperties, allow_none=True)
+
+    __elements__ = ('tabColor', 'outlinePr', 'pageSetUpPr')
+
+
+    def __init__(self,
+                 codeName=None,
+                 enableFormatConditionsCalculation=None,
+                 filterMode=None,
+                 published=None,
+                 syncHorizontal=None,
+                 syncRef=None,
+                 syncVertical=None,
+                 transitionEvaluation=None,
+                 transitionEntry=None,
+                 tabColor=None,
+                 outlinePr=None,
+                 pageSetUpPr=None
+                 ):
+        """ Attributes """
+        self.codeName = codeName
+        self.enableFormatConditionsCalculation = enableFormatConditionsCalculation
+        self.filterMode = filterMode
+        self.published = published
+        self.syncHorizontal = syncHorizontal
+        self.syncRef = syncRef
+        self.syncVertical = syncVertical
+        self.transitionEvaluation = transitionEvaluation
+        self.transitionEntry = transitionEntry
+        """ Elements """
+        self.tabColor = tabColor
+        if outlinePr is None:
+            self.outlinePr = Outline(summaryBelow=True, summaryRight=True)
+        else:
+            self.outlinePr = outlinePr
+
+        if pageSetUpPr is None:
+            pageSetUpPr = PageSetupProperties()
+        self.pageSetUpPr = pageSetUpPr
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/protection.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/protection.py
new file mode 100644
index 00000000..7f931840
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/protection.py
@@ -0,0 +1,120 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Bool,
+    String,
+    Alias,
+    Integer,
+)
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.excel import (
+    Base64Binary,
+)
+from openpyxl.utils.protection import hash_password
+
+
+class _Protected:
+    _password = None
+
+    def set_password(self, value='', already_hashed=False):
+        """Set a password on this sheet."""
+        if not already_hashed:
+            value = hash_password(value)
+        self._password = value
+
+    @property
+    def password(self):
+        """Return the password value, regardless of hash."""
+        return self._password
+
+    @password.setter
+    def password(self, value):
+        """Set a password directly, forcing a hash step."""
+        self.set_password(value)
+
+
+class SheetProtection(Serialisable, _Protected):
+    """
+    Information about protection of various aspects of a sheet. True values
+    mean that protection for the object or action is active This is the
+    **default** when protection is active, ie. users cannot do something
+    """
+
+    tagname = "sheetProtection"
+
+    sheet = Bool()
+    enabled = Alias('sheet')
+    objects = Bool()
+    scenarios = Bool()
+    formatCells = Bool()
+    formatColumns = Bool()
+    formatRows = Bool()
+    insertColumns = Bool()
+    insertRows = Bool()
+    insertHyperlinks = Bool()
+    deleteColumns = Bool()
+    deleteRows = Bool()
+    selectLockedCells = Bool()
+    selectUnlockedCells = Bool()
+    sort = Bool()
+    autoFilter = Bool()
+    pivotTables = Bool()
+    saltValue = Base64Binary(allow_none=True)
+    spinCount = Integer(allow_none=True)
+    algorithmName = String(allow_none=True)
+    hashValue = Base64Binary(allow_none=True)
+
+
+    __attrs__ = ('selectLockedCells', 'selectUnlockedCells', 'algorithmName',
+              'sheet', 'objects', 'insertRows', 'insertHyperlinks', 'autoFilter',
+              'scenarios', 'formatColumns', 'deleteColumns', 'insertColumns',
+              'pivotTables', 'deleteRows', 'formatCells', 'saltValue', 'formatRows',
+              'sort', 'spinCount', 'password', 'hashValue')
+
+
+    def __init__(self, sheet=False, objects=False, scenarios=False,
+                 formatCells=True, formatRows=True, formatColumns=True,
+                 insertColumns=True, insertRows=True, insertHyperlinks=True,
+                 deleteColumns=True, deleteRows=True, selectLockedCells=False,
+                 selectUnlockedCells=False, sort=True, autoFilter=True, pivotTables=True,
+                 password=None, algorithmName=None, saltValue=None, spinCount=None, hashValue=None):
+        self.sheet = sheet
+        self.objects = objects
+        self.scenarios = scenarios
+        self.formatCells = formatCells
+        self.formatColumns = formatColumns
+        self.formatRows = formatRows
+        self.insertColumns = insertColumns
+        self.insertRows = insertRows
+        self.insertHyperlinks = insertHyperlinks
+        self.deleteColumns = deleteColumns
+        self.deleteRows = deleteRows
+        self.selectLockedCells = selectLockedCells
+        self.selectUnlockedCells = selectUnlockedCells
+        self.sort = sort
+        self.autoFilter = autoFilter
+        self.pivotTables = pivotTables
+        if password is not None:
+            self.password = password
+        self.algorithmName = algorithmName
+        self.saltValue = saltValue
+        self.spinCount = spinCount
+        self.hashValue = hashValue
+
+
+    def set_password(self, value='', already_hashed=False):
+        super().set_password(value, already_hashed)
+        self.enable()
+
+
+    def enable(self):
+        self.sheet = True
+
+
+    def disable(self):
+        self.sheet = False
+
+
+    def  __bool__(self):
+        return self.sheet
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/related.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/related.py
new file mode 100644
index 00000000..2bf05019
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/related.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors.excel import Relation
+
+
+class Related(Serialisable):
+
+    id = Relation()
+
+
+    def __init__(self, id=None):
+        self.id = id
+
+
+    def to_tree(self, tagname, idx=None):
+        return super().to_tree(tagname)
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/scenario.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/scenario.py
new file mode 100644
index 00000000..3c86f607
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/scenario.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    String,
+    Integer,
+    Bool,
+    Sequence,
+    Convertible,
+)
+from .cell_range import MultiCellRange
+
+
+class InputCells(Serialisable):
+
+    tagname = "inputCells"
+
+    r = String()
+    deleted = Bool(allow_none=True)
+    undone = Bool(allow_none=True)
+    val = String()
+    numFmtId = Integer(allow_none=True)
+
+    def __init__(self,
+                 r=None,
+                 deleted=False,
+                 undone=False,
+                 val=None,
+                 numFmtId=None,
+                ):
+        self.r = r
+        self.deleted = deleted
+        self.undone = undone
+        self.val = val
+        self.numFmtId = numFmtId
+
+
+class Scenario(Serialisable):
+
+    tagname = "scenario"
+
+    inputCells = Sequence(expected_type=InputCells)
+    name = String()
+    locked = Bool(allow_none=True)
+    hidden = Bool(allow_none=True)
+    user = String(allow_none=True)
+    comment = String(allow_none=True)
+
+    __elements__ = ('inputCells',)
+    __attrs__ = ('name', 'locked', 'hidden', 'user', 'comment', 'count')
+
+    def __init__(self,
+                 inputCells=(),
+                 name=None,
+                 locked=False,
+                 hidden=False,
+                 count=None,
+                 user=None,
+                 comment=None,
+                ):
+        self.inputCells = inputCells
+        self.name = name
+        self.locked = locked
+        self.hidden = hidden
+        self.user = user
+        self.comment = comment
+
+
+    @property
+    def count(self):
+        return len(self.inputCells)
+
+
+class ScenarioList(Serialisable):
+
+    tagname = "scenarios"
+
+    scenario = Sequence(expected_type=Scenario)
+    current = Integer(allow_none=True)
+    show = Integer(allow_none=True)
+    sqref = Convertible(expected_type=MultiCellRange, allow_none=True)
+
+    __elements__ = ('scenario',)
+
+    def __init__(self,
+                 scenario=(),
+                 current=None,
+                 show=None,
+                 sqref=None,
+                ):
+        self.scenario = scenario
+        self.current = current
+        self.show = show
+        self.sqref = sqref
+
+
+    def append(self, scenario):
+        s = self.scenario
+        s.append(scenario)
+        self.scenario = s
+
+
+    def __bool__(self):
+        return bool(self.scenario)
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/smart_tag.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/smart_tag.py
new file mode 100644
index 00000000..29fe1926
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/smart_tag.py
@@ -0,0 +1,78 @@
+#Autogenerated schema
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Bool,
+    Integer,
+    String,
+    Sequence,
+)
+
+
+class CellSmartTagPr(Serialisable):
+
+    tagname = "cellSmartTagPr"
+
+    key = String()
+    val = String()
+
+    def __init__(self,
+                 key=None,
+                 val=None,
+                ):
+        self.key = key
+        self.val = val
+
+
+class CellSmartTag(Serialisable):
+
+    tagname = "cellSmartTag"
+
+    cellSmartTagPr = Sequence(expected_type=CellSmartTagPr)
+    type = Integer()
+    deleted = Bool(allow_none=True)
+    xmlBased = Bool(allow_none=True)
+
+    __elements__ = ('cellSmartTagPr',)
+
+    def __init__(self,
+                 cellSmartTagPr=(),
+                 type=None,
+                 deleted=False,
+                 xmlBased=False,
+                ):
+        self.cellSmartTagPr = cellSmartTagPr
+        self.type = type
+        self.deleted = deleted
+        self.xmlBased = xmlBased
+
+
+class CellSmartTags(Serialisable):
+
+    tagname = "cellSmartTags"
+
+    cellSmartTag = Sequence(expected_type=CellSmartTag)
+    r = String()
+
+    __elements__ = ('cellSmartTag',)
+
+    def __init__(self,
+                 cellSmartTag=(),
+                 r=None,
+                ):
+        self.cellSmartTag = cellSmartTag
+        self.r = r
+
+
+class SmartTags(Serialisable):
+
+    tagname = "smartTags"
+
+    cellSmartTags = Sequence(expected_type=CellSmartTags)
+
+    __elements__ = ('cellSmartTags',)
+
+    def __init__(self,
+                 cellSmartTags=(),
+                ):
+        self.cellSmartTags = cellSmartTags
+
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/table.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/table.py
new file mode 100644
index 00000000..756345f9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/table.py
@@ -0,0 +1,385 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors.serialisable import Serialisable
+from openpyxl.descriptors import (
+    Descriptor,
+    Alias,
+    Typed,
+    Bool,
+    Integer,
+    NoneSet,
+    String,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList, CellRange
+from openpyxl.descriptors.sequence import NestedSequence
+from openpyxl.xml.constants import SHEET_MAIN_NS, REL_NS
+from openpyxl.xml.functions import tostring
+from openpyxl.utils import range_boundaries
+from openpyxl.utils.escape import escape, unescape
+
+from .related import Related
+
+from .filters import (
+    AutoFilter,
+    SortState,
+)
+
+TABLESTYLES = tuple(
+    ["TableStyleMedium{0}".format(i) for i in range(1, 29)]
+    + ["TableStyleLight{0}".format(i) for i in range(1, 22)]
+    + ["TableStyleDark{0}".format(i) for i in range(1, 12)]
+)
+
+PIVOTSTYLES = tuple(
+    ["PivotStyleMedium{0}".format(i) for i in range(1, 29)]
+    + ["PivotStyleLight{0}".format(i) for i in range(1, 29)]
+    + ["PivotStyleDark{0}".format(i) for i in range(1, 29)]
+)
+
+
+class TableStyleInfo(Serialisable):
+
+    tagname = "tableStyleInfo"
+
+    name = String(allow_none=True)
+    showFirstColumn = Bool(allow_none=True)
+    showLastColumn = Bool(allow_none=True)
+    showRowStripes = Bool(allow_none=True)
+    showColumnStripes = Bool(allow_none=True)
+
+    def __init__(self,
+                 name=None,
+                 showFirstColumn=None,
+                 showLastColumn=None,
+                 showRowStripes=None,
+                 showColumnStripes=None,
+                ):
+        self.name = name
+        self.showFirstColumn = showFirstColumn
+        self.showLastColumn = showLastColumn
+        self.showRowStripes = showRowStripes
+        self.showColumnStripes = showColumnStripes
+
+
+class XMLColumnProps(Serialisable):
+
+    tagname = "xmlColumnPr"
+
+    mapId = Integer()
+    xpath = String()
+    denormalized = Bool(allow_none=True)
+    xmlDataType = String()
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ()
+
+    def __init__(self,
+                 mapId=None,
+                 xpath=None,
+                 denormalized=None,
+                 xmlDataType=None,
+                 extLst=None,
+                ):
+        self.mapId = mapId
+        self.xpath = xpath
+        self.denormalized = denormalized
+        self.xmlDataType = xmlDataType
+
+
+class TableFormula(Serialisable):
+
+    tagname = "tableFormula"
+
+    ## Note formula is stored as the text value
+
+    array = Bool(allow_none=True)
+    attr_text = Descriptor()
+    text = Alias('attr_text')
+
+
+    def __init__(self,
+                 array=None,
+                 attr_text=None,
+                ):
+        self.array = array
+        self.attr_text = attr_text
+
+
+class TableColumn(Serialisable):
+
+    tagname = "tableColumn"
+
+    id = Integer()
+    uniqueName = String(allow_none=True)
+    name = String()
+    totalsRowFunction = NoneSet(values=(['sum', 'min', 'max', 'average',
+                                         'count', 'countNums', 'stdDev', 'var', 'custom']))
+    totalsRowLabel = String(allow_none=True)
+    queryTableFieldId = Integer(allow_none=True)
+    headerRowDxfId = Integer(allow_none=True)
+    dataDxfId = Integer(allow_none=True)
+    totalsRowDxfId = Integer(allow_none=True)
+    headerRowCellStyle = String(allow_none=True)
+    dataCellStyle = String(allow_none=True)
+    totalsRowCellStyle = String(allow_none=True)
+    calculatedColumnFormula = Typed(expected_type=TableFormula, allow_none=True)
+    totalsRowFormula = Typed(expected_type=TableFormula, allow_none=True)
+    xmlColumnPr = Typed(expected_type=XMLColumnProps, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('calculatedColumnFormula', 'totalsRowFormula',
+                    'xmlColumnPr', 'extLst')
+
+    def __init__(self,
+                 id=None,
+                 uniqueName=None,
+                 name=None,
+                 totalsRowFunction=None,
+                 totalsRowLabel=None,
+                 queryTableFieldId=None,
+                 headerRowDxfId=None,
+                 dataDxfId=None,
+                 totalsRowDxfId=None,
+                 headerRowCellStyle=None,
+                 dataCellStyle=None,
+                 totalsRowCellStyle=None,
+                 calculatedColumnFormula=None,
+                 totalsRowFormula=None,
+                 xmlColumnPr=None,
+                 extLst=None,
+                ):
+        self.id = id
+        self.uniqueName = uniqueName
+        self.name = name
+        self.totalsRowFunction = totalsRowFunction
+        self.totalsRowLabel = totalsRowLabel
+        self.queryTableFieldId = queryTableFieldId
+        self.headerRowDxfId = headerRowDxfId
+        self.dataDxfId = dataDxfId
+        self.totalsRowDxfId = totalsRowDxfId
+        self.headerRowCellStyle = headerRowCellStyle
+        self.dataCellStyle = dataCellStyle
+        self.totalsRowCellStyle = totalsRowCellStyle
+        self.calculatedColumnFormula = calculatedColumnFormula
+        self.totalsRowFormula = totalsRowFormula
+        self.xmlColumnPr = xmlColumnPr
+        self.extLst = extLst
+
+
+    def __iter__(self):
+        for k, v in super().__iter__():
+            if k == 'name':
+                v = escape(v)
+            yield k, v
+
+
+    @classmethod
+    def from_tree(cls, node):
+        self = super().from_tree(node)
+        self.name = unescape(self.name)
+        return self
+
+
+class TableNameDescriptor(String):
+
+    """
+    Table names cannot have spaces in them
+    """
+
+    def __set__(self, instance, value):
+        if value is not None and " " in value:
+            raise ValueError("Table names cannot have spaces")
+        super().__set__(instance, value)
+
+
+class Table(Serialisable):
+
+    _path = "/tables/table{0}.xml"
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
+    _rel_type = REL_NS + "/table"
+    _rel_id = None
+
+    tagname = "table"
+
+    id = Integer()
+    name = String(allow_none=True)
+    displayName = TableNameDescriptor()
+    comment = String(allow_none=True)
+    ref = CellRange()
+    tableType = NoneSet(values=(['worksheet', 'xml', 'queryTable']))
+    headerRowCount = Integer(allow_none=True)
+    insertRow = Bool(allow_none=True)
+    insertRowShift = Bool(allow_none=True)
+    totalsRowCount = Integer(allow_none=True)
+    totalsRowShown = Bool(allow_none=True)
+    published = Bool(allow_none=True)
+    headerRowDxfId = Integer(allow_none=True)
+    dataDxfId = Integer(allow_none=True)
+    totalsRowDxfId = Integer(allow_none=True)
+    headerRowBorderDxfId = Integer(allow_none=True)
+    tableBorderDxfId = Integer(allow_none=True)
+    totalsRowBorderDxfId = Integer(allow_none=True)
+    headerRowCellStyle = String(allow_none=True)
+    dataCellStyle = String(allow_none=True)
+    totalsRowCellStyle = String(allow_none=True)
+    connectionId = Integer(allow_none=True)
+    autoFilter = Typed(expected_type=AutoFilter, allow_none=True)
+    sortState = Typed(expected_type=SortState, allow_none=True)
+    tableColumns = NestedSequence(expected_type=TableColumn, count=True)
+    tableStyleInfo = Typed(expected_type=TableStyleInfo, allow_none=True)
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('autoFilter', 'sortState', 'tableColumns',
+                    'tableStyleInfo')
+
+    def __init__(self,
+                 id=1,
+                 displayName=None,
+                 ref=None,
+                 name=None,
+                 comment=None,
+                 tableType=None,
+                 headerRowCount=1,
+                 insertRow=None,
+                 insertRowShift=None,
+                 totalsRowCount=None,
+                 totalsRowShown=None,
+                 published=None,
+                 headerRowDxfId=None,
+                 dataDxfId=None,
+                 totalsRowDxfId=None,
+                 headerRowBorderDxfId=None,
+                 tableBorderDxfId=None,
+                 totalsRowBorderDxfId=None,
+                 headerRowCellStyle=None,
+                 dataCellStyle=None,
+                 totalsRowCellStyle=None,
+                 connectionId=None,
+                 autoFilter=None,
+                 sortState=None,
+                 tableColumns=(),
+                 tableStyleInfo=None,
+                 extLst=None,
+                ):
+        self.id = id
+        self.displayName = displayName
+        if name is None:
+            name = displayName
+        self.name = name
+        self.comment = comment
+        self.ref = ref
+        self.tableType = tableType
+        self.headerRowCount = headerRowCount
+        self.insertRow = insertRow
+        self.insertRowShift = insertRowShift
+        self.totalsRowCount = totalsRowCount
+        self.totalsRowShown = totalsRowShown
+        self.published = published
+        self.headerRowDxfId = headerRowDxfId
+        self.dataDxfId = dataDxfId
+        self.totalsRowDxfId = totalsRowDxfId
+        self.headerRowBorderDxfId = headerRowBorderDxfId
+        self.tableBorderDxfId = tableBorderDxfId
+        self.totalsRowBorderDxfId = totalsRowBorderDxfId
+        self.headerRowCellStyle = headerRowCellStyle
+        self.dataCellStyle = dataCellStyle
+        self.totalsRowCellStyle = totalsRowCellStyle
+        self.connectionId = connectionId
+        self.autoFilter = autoFilter
+        self.sortState = sortState
+        self.tableColumns = tableColumns
+        self.tableStyleInfo = tableStyleInfo
+
+
+    def to_tree(self):
+        tree = super().to_tree()
+        tree.set("xmlns", SHEET_MAIN_NS)
+        return tree
+
+
+    @property
+    def path(self):
+        """
+        Return path within the archive
+        """
+        return "/xl" + self._path.format(self.id)
+
+
+    def _write(self, archive):
+        """
+        Serialise to XML and write to archive
+        """
+        xml = self.to_tree()
+        archive.writestr(self.path[1:], tostring(xml))
+
+
+    def _initialise_columns(self):
+        """
+        Create a list of table columns from a cell range
+        Always set a ref if we have headers (the default)
+        Column headings must be strings and must match cells in the worksheet.
+        """
+
+        min_col, min_row, max_col, max_row = range_boundaries(self.ref)
+        for idx in range(min_col, max_col+1):
+            col = TableColumn(id=idx, name="Column{0}".format(idx))
+            self.tableColumns.append(col)
+        if self.headerRowCount and not self.autoFilter:
+            self.autoFilter = AutoFilter(ref=self.ref)
+
+
+    @property
+    def column_names(self):
+        return [column.name for column in self.tableColumns]
+
+
+class TablePartList(Serialisable):
+
+    tagname = "tableParts"
+
+    count = Integer(allow_none=True)
+    tablePart = Sequence(expected_type=Related)
+
+    __elements__ = ('tablePart',)
+    __attrs__ = ('count',)
+
+    def __init__(self,
+                 count=None,
+                 tablePart=(),
+                ):
+        self.tablePart = tablePart
+
+
+    def append(self, part):
+        self.tablePart.append(part)
+
+
+    @property
+    def count(self):
+        return len(self.tablePart)
+
+
+    def __bool__(self):
+        return bool(self.tablePart)
+
+
+class TableList(dict):
+
+
+    def add(self, table):
+        if not isinstance(table, Table):
+            raise TypeError("You can only add tables")
+        self[table.name] = table
+
+
+    def get(self, name=None, table_range=None):
+        if name is not None:
+            return super().get(name)
+        for table in self.values():
+            if table_range == table.ref:
+                return table
+
+
+    def items(self):
+        return [(name, table.ref) for name, table in super().items()]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/views.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/views.py
new file mode 100644
index 00000000..27046b0d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/views.py
@@ -0,0 +1,155 @@
+# Copyright (c) 2010-2024 openpyxl
+
+from openpyxl.descriptors import (
+    Bool,
+    Integer,
+    String,
+    Set,
+    Float,
+    Typed,
+    NoneSet,
+    Sequence,
+)
+from openpyxl.descriptors.excel import ExtensionList
+from openpyxl.descriptors.serialisable import Serialisable
+
+
+class Pane(Serialisable):
+    xSplit = Float(allow_none=True)
+    ySplit = Float(allow_none=True)
+    topLeftCell = String(allow_none=True)
+    activePane = Set(values=("bottomRight", "topRight", "bottomLeft", "topLeft"))
+    state = Set(values=("split", "frozen", "frozenSplit"))
+
+    def __init__(self,
+                 xSplit=None,
+                 ySplit=None,
+                 topLeftCell=None,
+                 activePane="topLeft",
+                 state="split"):
+        self.xSplit = xSplit
+        self.ySplit = ySplit
+        self.topLeftCell = topLeftCell
+        self.activePane = activePane
+        self.state = state
+
+
+class Selection(Serialisable):
+    pane = NoneSet(values=("bottomRight", "topRight", "bottomLeft", "topLeft"))
+    activeCell = String(allow_none=True)
+    activeCellId = Integer(allow_none=True)
+    sqref = String(allow_none=True)
+
+    def __init__(self,
+                 pane=None,
+                 activeCell="A1",
+                 activeCellId=None,
+                 sqref="A1"):
+        self.pane = pane
+        self.activeCell = activeCell
+        self.activeCellId = activeCellId
+        self.sqref = sqref
+
+
+class SheetView(Serialisable):
+
+    """Information about the visible portions of this sheet."""
+
+    tagname = "sheetView"
+
+    windowProtection = Bool(allow_none=True)
+    showFormulas = Bool(allow_none=True)
+    showGridLines = Bool(allow_none=True)
+    showRowColHeaders = Bool(allow_none=True)
+    showZeros = Bool(allow_none=True)
+    rightToLeft = Bool(allow_none=True)
+    tabSelected = Bool(allow_none=True)
+    showRuler = Bool(allow_none=True)
+    showOutlineSymbols = Bool(allow_none=True)
+    defaultGridColor = Bool(allow_none=True)
+    showWhiteSpace = Bool(allow_none=True)
+    view = NoneSet(values=("normal", "pageBreakPreview", "pageLayout"))
+    topLeftCell = String(allow_none=True)
+    colorId = Integer(allow_none=True)
+    zoomScale = Integer(allow_none=True)
+    zoomScaleNormal = Integer(allow_none=True)
+    zoomScaleSheetLayoutView = Integer(allow_none=True)
+    zoomScalePageLayoutView = Integer(allow_none=True)
+    zoomToFit = Bool(allow_none=True) # Chart sheets only
+    workbookViewId = Integer()
+    selection = Sequence(expected_type=Selection)
+    pane = Typed(expected_type=Pane, allow_none=True)
+
+    def __init__(self,
+                 windowProtection=None,
+                 showFormulas=None,
+                 showGridLines=None,
+                 showRowColHeaders=None,
+                 showZeros=None,
+                 rightToLeft=None,
+                 tabSelected=None,
+                 showRuler=None,
+                 showOutlineSymbols=None,
+                 defaultGridColor=None,
+                 showWhiteSpace=None,
+                 view=None,
+                 topLeftCell=None,
+                 colorId=None,
+                 zoomScale=None,
+                 zoomScaleNormal=None,
+                 zoomScaleSheetLayoutView=None,
+                 zoomScalePageLayoutView=None,
+                 zoomToFit=None,
+                 workbookViewId=0,
+                 selection=None,
+                 pane=None,):
+        self.windowProtection = windowProtection
+        self.showFormulas = showFormulas
+        self.showGridLines = showGridLines
+        self.showRowColHeaders = showRowColHeaders
+        self.showZeros = showZeros
+        self.rightToLeft = rightToLeft
+        self.tabSelected = tabSelected
+        self.showRuler = showRuler
+        self.showOutlineSymbols = showOutlineSymbols
+        self.defaultGridColor = defaultGridColor
+        self.showWhiteSpace = showWhiteSpace
+        self.view = view
+        self.topLeftCell = topLeftCell
+        self.colorId = colorId
+        self.zoomScale = zoomScale
+        self.zoomScaleNormal = zoomScaleNormal
+        self.zoomScaleSheetLayoutView = zoomScaleSheetLayoutView
+        self.zoomScalePageLayoutView = zoomScalePageLayoutView
+        self.zoomToFit = zoomToFit
+        self.workbookViewId = workbookViewId
+        self.pane = pane
+        if selection is None:
+            selection = (Selection(), )
+        self.selection = selection
+
+
+class SheetViewList(Serialisable):
+
+    tagname = "sheetViews"
+
+    sheetView = Sequence(expected_type=SheetView, )
+    extLst = Typed(expected_type=ExtensionList, allow_none=True)
+
+    __elements__ = ('sheetView',)
+
+    def __init__(self,
+                 sheetView=None,
+                 extLst=None,
+                ):
+        if sheetView is None:
+            sheetView = [SheetView()]
+        self.sheetView = sheetView
+
+
+    @property
+    def active(self):
+        """
+        Returns the first sheet view which is assumed to be active
+        """
+        return self.sheetView[0]
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/worksheet/worksheet.py b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/worksheet.py
new file mode 100644
index 00000000..b7ffbebc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/worksheet/worksheet.py
@@ -0,0 +1,907 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Worksheet is the 2nd-level container in Excel."""
+
+
+# Python stdlib imports
+from itertools import chain
+from operator import itemgetter
+from inspect import isgenerator
+from warnings import warn
+
+# compatibility imports
+from openpyxl.compat import (
+    deprecated,
+)
+
+# package imports
+from openpyxl.utils import (
+    column_index_from_string,
+    get_column_letter,
+    range_boundaries,
+    coordinate_to_tuple,
+)
+from openpyxl.cell import Cell, MergedCell
+from openpyxl.formatting.formatting import ConditionalFormattingList
+from openpyxl.packaging.relationship import RelationshipList
+from openpyxl.workbook.child import _WorkbookChild
+from openpyxl.workbook.defined_name import (
+    DefinedNameDict,
+)
+
+from openpyxl.formula.translate import Translator
+
+from .datavalidation import DataValidationList
+from .page import (
+    PrintPageSetup,
+    PageMargins,
+    PrintOptions,
+)
+from .dimensions import (
+    ColumnDimension,
+    RowDimension,
+    DimensionHolder,
+    SheetFormatProperties,
+)
+from .protection import SheetProtection
+from .filters import AutoFilter
+from .views import (
+    Pane,
+    Selection,
+    SheetViewList,
+)
+from .cell_range import MultiCellRange, CellRange
+from .merge import MergedCellRange
+from .properties import WorksheetProperties
+from .pagebreak import RowBreak, ColBreak
+from .scenario import ScenarioList
+from .table import TableList
+from .formula import ArrayFormula
+from .print_settings import (
+    PrintTitles,
+    ColRange,
+    RowRange,
+    PrintArea,
+)
+
+
+class Worksheet(_WorkbookChild):
+    """Represents a worksheet.
+
+    Do not create worksheets yourself,
+    use :func:`openpyxl.workbook.Workbook.create_sheet` instead
+
+    """
+
+    _rel_type = "worksheet"
+    _path = "/xl/worksheets/sheet{0}.xml"
+    mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
+
+    BREAK_NONE = 0
+    BREAK_ROW = 1
+    BREAK_COLUMN = 2
+
+    SHEETSTATE_VISIBLE = 'visible'
+    SHEETSTATE_HIDDEN = 'hidden'
+    SHEETSTATE_VERYHIDDEN = 'veryHidden'
+
+    # Paper size
+    PAPERSIZE_LETTER = '1'
+    PAPERSIZE_LETTER_SMALL = '2'
+    PAPERSIZE_TABLOID = '3'
+    PAPERSIZE_LEDGER = '4'
+    PAPERSIZE_LEGAL = '5'
+    PAPERSIZE_STATEMENT = '6'
+    PAPERSIZE_EXECUTIVE = '7'
+    PAPERSIZE_A3 = '8'
+    PAPERSIZE_A4 = '9'
+    PAPERSIZE_A4_SMALL = '10'
+    PAPERSIZE_A5 = '11'
+
+    # Page orientation
+    ORIENTATION_PORTRAIT = 'portrait'
+    ORIENTATION_LANDSCAPE = 'landscape'
+
+    def __init__(self, parent, title=None):
+        _WorkbookChild.__init__(self, parent, title)
+        self._setup()
+
+    def _setup(self):
+        self.row_dimensions = DimensionHolder(worksheet=self,
+                                              default_factory=self._add_row)
+        self.column_dimensions = DimensionHolder(worksheet=self,
+                                                 default_factory=self._add_column)
+        self.row_breaks = RowBreak()
+        self.col_breaks = ColBreak()
+        self._cells = {}
+        self._charts = []
+        self._images = []
+        self._rels = RelationshipList()
+        self._drawing = None
+        self._comments = []
+        self.merged_cells = MultiCellRange()
+        self._tables = TableList()
+        self._pivots = []
+        self.data_validations = DataValidationList()
+        self._hyperlinks = []
+        self.sheet_state = 'visible'
+        self.page_setup = PrintPageSetup(worksheet=self)
+        self.print_options = PrintOptions()
+        self._print_rows = None
+        self._print_cols = None
+        self._print_area = PrintArea()
+        self.page_margins = PageMargins()
+        self.views = SheetViewList()
+        self.protection = SheetProtection()
+        self.defined_names = DefinedNameDict()
+
+        self._current_row = 0
+        self.auto_filter = AutoFilter()
+        self.conditional_formatting = ConditionalFormattingList()
+        self.legacy_drawing = None
+        self.sheet_properties = WorksheetProperties()
+        self.sheet_format = SheetFormatProperties()
+        self.scenarios = ScenarioList()
+
+
+    @property
+    def sheet_view(self):
+        return self.views.active
+
+
+    @property
+    def selected_cell(self):
+        return self.sheet_view.selection[0].sqref
+
+
+    @property
+    def active_cell(self):
+        return self.sheet_view.selection[0].activeCell
+
+
+    @property
+    def array_formulae(self):
+        """Returns a dictionary of cells with array formulae and the cells in array"""
+        result = {}
+        for c in self._cells.values():
+            if c.data_type == "f":
+                if isinstance(c.value, ArrayFormula):
+                    result[c.coordinate] = c.value.ref
+        return result
+
+
+    @property
+    def show_gridlines(self):
+        return self.sheet_view.showGridLines
+
+
+    @property
+    def freeze_panes(self):
+        if self.sheet_view.pane is not None:
+            return self.sheet_view.pane.topLeftCell
+
+
+    @freeze_panes.setter
+    def freeze_panes(self, topLeftCell=None):
+        if isinstance(topLeftCell, Cell):
+            topLeftCell = topLeftCell.coordinate
+        if topLeftCell == 'A1':
+            topLeftCell = None
+
+        if not topLeftCell:
+            self.sheet_view.pane = None
+            return
+
+        row, column = coordinate_to_tuple(topLeftCell)
+
+        view = self.sheet_view
+        view.pane = Pane(topLeftCell=topLeftCell,
+                        activePane="topRight",
+                        state="frozen")
+        view.selection[0].pane = "topRight"
+
+        if column > 1:
+            view.pane.xSplit = column - 1
+        if row > 1:
+            view.pane.ySplit = row - 1
+            view.pane.activePane = 'bottomLeft'
+            view.selection[0].pane = "bottomLeft"
+            if column > 1:
+                view.selection[0].pane = "bottomRight"
+                view.pane.activePane = 'bottomRight'
+
+        if row > 1 and column > 1:
+            sel = list(view.selection)
+            sel.insert(0, Selection(pane="topRight", activeCell=None, sqref=None))
+            sel.insert(1, Selection(pane="bottomLeft", activeCell=None, sqref=None))
+            view.selection = sel
+
+
+    def cell(self, row, column, value=None):
+        """
+        Returns a cell object based on the given coordinates.
+
+        Usage: cell(row=15, column=1, value=5)
+
+        Calling `cell` creates cells in memory when they
+        are first accessed.
+
+        :param row: row index of the cell (e.g. 4)
+        :type row: int
+
+        :param column: column index of the cell (e.g. 3)
+        :type column: int
+
+        :param value: value of the cell (e.g. 5)
+        :type value: numeric or time or string or bool or none
+
+        :rtype: openpyxl.cell.cell.Cell
+        """
+
+        if row < 1 or column < 1:
+            raise ValueError("Row or column values must be at least 1")
+
+        cell = self._get_cell(row, column)
+        if value is not None:
+            cell.value = value
+
+        return cell
+
+
+    def _get_cell(self, row, column):
+        """
+        Internal method for getting a cell from a worksheet.
+        Will create a new cell if one doesn't already exist.
+        """
+        if not 0 < row < 1048577:
+            raise ValueError(f"Row numbers must be between 1 and 1048576. Row number supplied was {row}")
+        coordinate = (row, column)
+        if not coordinate in self._cells:
+            cell = Cell(self, row=row, column=column)
+            self._add_cell(cell)
+        return self._cells[coordinate]
+
+
+    def _add_cell(self, cell):
+        """
+        Internal method for adding cell objects.
+        """
+        column = cell.col_idx
+        row = cell.row
+        self._current_row = max(row, self._current_row)
+        self._cells[(row, column)] = cell
+
+
+    def __getitem__(self, key):
+        """Convenience access by Excel style coordinates
+
+        The key can be a single cell coordinate 'A1', a range of cells 'A1:D25',
+        individual rows or columns 'A', 4 or ranges of rows or columns 'A:D',
+        4:10.
+
+        Single cells will always be created if they do not exist.
+
+        Returns either a single cell or a tuple of rows or columns.
+        """
+        if isinstance(key, slice):
+            if not all([key.start, key.stop]):
+                raise IndexError("{0} is not a valid coordinate or range".format(key))
+            key = "{0}:{1}".format(key.start, key.stop)
+
+        if isinstance(key, int):
+            key = str(key
+                      )
+        min_col, min_row, max_col, max_row = range_boundaries(key)
+
+        if not any([min_col, min_row, max_col, max_row]):
+            raise IndexError("{0} is not a valid coordinate or range".format(key))
+
+        if min_row is None:
+            cols = tuple(self.iter_cols(min_col, max_col))
+            if min_col == max_col:
+                cols = cols[0]
+            return cols
+        if min_col is None:
+            rows = tuple(self.iter_rows(min_col=min_col, min_row=min_row,
+                                        max_col=self.max_column, max_row=max_row))
+            if min_row == max_row:
+                rows = rows[0]
+            return rows
+        if ":" not in key:
+            return self._get_cell(min_row, min_col)
+        return tuple(self.iter_rows(min_row=min_row, min_col=min_col,
+                                    max_row=max_row, max_col=max_col))
+
+
+    def __setitem__(self, key, value):
+        self[key].value = value
+
+
+    def __iter__(self):
+        return self.iter_rows()
+
+
+    def __delitem__(self, key):
+        row, column = coordinate_to_tuple(key)
+        if (row, column) in self._cells:
+            del self._cells[(row, column)]
+
+
+    @property
+    def min_row(self):
+        """The minimum row index containing data (1-based)
+
+        :type: int
+        """
+        min_row = 1
+        if self._cells:
+            min_row = min(self._cells)[0]
+        return min_row
+
+
+    @property
+    def max_row(self):
+        """The maximum row index containing data (1-based)
+
+        :type: int
+        """
+        max_row = 1
+        if self._cells:
+            max_row = max(self._cells)[0]
+        return max_row
+
+
+    @property
+    def min_column(self):
+        """The minimum column index containing data (1-based)
+
+        :type: int
+        """
+        min_col = 1
+        if self._cells:
+            min_col = min(c[1] for c in self._cells)
+        return min_col
+
+
+    @property
+    def max_column(self):
+        """The maximum column index containing data (1-based)
+
+        :type: int
+        """
+        max_col = 1
+        if self._cells:
+            max_col = max(c[1] for c in self._cells)
+        return max_col
+
+
+    def calculate_dimension(self):
+        """Return the minimum bounding range for all cells containing data (ex. 'A1:M24')
+
+        :rtype: string
+        """
+        if self._cells:
+            rows = set()
+            cols = set()
+            for row, col in self._cells:
+                rows.add(row)
+                cols.add(col)
+            max_row = max(rows)
+            max_col = max(cols)
+            min_col = min(cols)
+            min_row = min(rows)
+        else:
+            return "A1:A1"
+
+        return f"{get_column_letter(min_col)}{min_row}:{get_column_letter(max_col)}{max_row}"
+
+
+    @property
+    def dimensions(self):
+        """Returns the result of :func:`calculate_dimension`"""
+        return self.calculate_dimension()
+
+
+    def iter_rows(self, min_row=None, max_row=None, min_col=None, max_col=None, values_only=False):
+        """
+        Produces cells from the worksheet, by row. Specify the iteration range
+        using indices of rows and columns.
+
+        If no indices are specified the range starts at A1.
+
+        If no cells are in the worksheet an empty tuple will be returned.
+
+        :param min_col: smallest column index (1-based index)
+        :type min_col: int
+
+        :param min_row: smallest row index (1-based index)
+        :type min_row: int
+
+        :param max_col: largest column index (1-based index)
+        :type max_col: int
+
+        :param max_row: largest row index (1-based index)
+        :type max_row: int
+
+        :param values_only: whether only cell values should be returned
+        :type values_only: bool
+
+        :rtype: generator
+        """
+
+        if self._current_row == 0 and not any([min_col, min_row, max_col, max_row ]):
+            return iter(())
+
+
+        min_col = min_col or 1
+        min_row = min_row or 1
+        max_col = max_col or self.max_column
+        max_row = max_row or self.max_row
+
+        return self._cells_by_row(min_col, min_row, max_col, max_row, values_only)
+
+
+    def _cells_by_row(self, min_col, min_row, max_col, max_row, values_only=False):
+        for row in range(min_row, max_row + 1):
+            cells = (self.cell(row=row, column=column) for column in range(min_col, max_col + 1))
+            if values_only:
+                yield tuple(cell.value for cell in cells)
+            else:
+                yield tuple(cells)
+
+
+    @property
+    def rows(self):
+        """Produces all cells in the worksheet, by row (see :func:`iter_rows`)
+
+        :type: generator
+        """
+        return self.iter_rows()
+
+
+    @property
+    def values(self):
+        """Produces all cell values in the worksheet, by row
+
+        :type: generator
+        """
+        for row in self.iter_rows(values_only=True):
+            yield row
+
+
+    def iter_cols(self, min_col=None, max_col=None, min_row=None, max_row=None, values_only=False):
+        """
+        Produces cells from the worksheet, by column. Specify the iteration range
+        using indices of rows and columns.
+
+        If no indices are specified the range starts at A1.
+
+        If no cells are in the worksheet an empty tuple will be returned.
+
+        :param min_col: smallest column index (1-based index)
+        :type min_col: int
+
+        :param min_row: smallest row index (1-based index)
+        :type min_row: int
+
+        :param max_col: largest column index (1-based index)
+        :type max_col: int
+
+        :param max_row: largest row index (1-based index)
+        :type max_row: int
+
+        :param values_only: whether only cell values should be returned
+        :type values_only: bool
+
+        :rtype: generator
+        """
+
+        if self._current_row == 0 and not any([min_col, min_row, max_col, max_row]):
+            return iter(())
+
+        min_col = min_col or 1
+        min_row = min_row or 1
+        max_col = max_col or self.max_column
+        max_row = max_row or self.max_row
+
+        return self._cells_by_col(min_col, min_row, max_col, max_row, values_only)
+
+
+    def _cells_by_col(self, min_col, min_row, max_col, max_row, values_only=False):
+        """
+        Get cells by column
+        """
+        for column in range(min_col, max_col+1):
+            cells = (self.cell(row=row, column=column)
+                        for row in range(min_row, max_row+1))
+            if values_only:
+                yield tuple(cell.value for cell in cells)
+            else:
+                yield tuple(cells)
+
+
+    @property
+    def columns(self):
+        """Produces all cells in the worksheet, by column  (see :func:`iter_cols`)"""
+        return self.iter_cols()
+
+
+    @property
+    def column_groups(self):
+        """
+        Return a list of column ranges where more than one column
+        """
+        return [cd.range for cd in self.column_dimensions.values() if cd.min and cd.max > cd.min]
+
+
+    def set_printer_settings(self, paper_size, orientation):
+        """Set printer settings """
+
+        self.page_setup.paperSize = paper_size
+        self.page_setup.orientation = orientation
+
+
+    def add_data_validation(self, data_validation):
+        """ Add a data-validation object to the sheet.  The data-validation
+            object defines the type of data-validation to be applied and the
+            cell or range of cells it should apply to.
+        """
+        self.data_validations.append(data_validation)
+
+
+    def add_chart(self, chart, anchor=None):
+        """
+        Add a chart to the sheet
+        Optionally provide a cell for the top-left anchor
+        """
+        if anchor is not None:
+            chart.anchor = anchor
+        self._charts.append(chart)
+
+
+    def add_image(self, img, anchor=None):
+        """
+        Add an image to the sheet.
+        Optionally provide a cell for the top-left anchor
+        """
+        if anchor is not None:
+            img.anchor = anchor
+        self._images.append(img)
+
+
+    def add_table(self, table):
+        """
+        Check for duplicate name in definedNames and other worksheet tables
+        before adding table.
+        """
+
+        if self.parent._duplicate_name(table.name):
+            raise ValueError("Table with name {0} already exists".format(table.name))
+        if not hasattr(self, "_get_cell"):
+            warn("In write-only mode you must add table columns manually")
+        self._tables.add(table)
+
+
+    @property
+    def tables(self):
+        return self._tables
+
+
+    def add_pivot(self, pivot):
+        self._pivots.append(pivot)
+
+
+    def merge_cells(self, range_string=None, start_row=None, start_column=None, end_row=None, end_column=None):
+        """ Set merge on a cell range.  Range is a cell range (e.g. A1:E1) """
+        if range_string is None:
+            cr = CellRange(range_string=range_string, min_col=start_column, min_row=start_row,
+                      max_col=end_column, max_row=end_row)
+            range_string = cr.coord
+        mcr = MergedCellRange(self, range_string)
+        self.merged_cells.add(mcr)
+        self._clean_merge_range(mcr)
+
+
+    def _clean_merge_range(self, mcr):
+        """
+        Remove all but the top left-cell from a range of merged cells
+        and recreate the lost border information.
+        Borders are then applied
+        """
+        cells = mcr.cells
+        next(cells) # skip first cell
+        for row, col in cells:
+            self._cells[row, col] = MergedCell(self, row, col)
+        mcr.format()
+
+
+    @property
+    @deprecated("Use ws.merged_cells.ranges")
+    def merged_cell_ranges(self):
+        """Return a copy of cell ranges"""
+        return self.merged_cells.ranges[:]
+
+
+    def unmerge_cells(self, range_string=None, start_row=None, start_column=None, end_row=None, end_column=None):
+        """ Remove merge on a cell range.  Range is a cell range (e.g. A1:E1) """
+        cr = CellRange(range_string=range_string, min_col=start_column, min_row=start_row,
+                      max_col=end_column, max_row=end_row)
+
+        if cr.coord not in self.merged_cells:
+            raise ValueError("Cell range {0} is not merged".format(cr.coord))
+
+        self.merged_cells.remove(cr)
+
+        cells = cr.cells
+        next(cells) # skip first cell
+        for row, col in cells:
+            del self._cells[(row, col)]
+
+
+    def append(self, iterable):
+        """Appends a group of values at the bottom of the current sheet.
+
+        * If it's a list: all values are added in order, starting from the first column
+        * If it's a dict: values are assigned to the columns indicated by the keys (numbers or letters)
+
+        :param iterable: list, range or generator, or dict containing values to append
+        :type iterable: list|tuple|range|generator or dict
+
+        Usage:
+
+        * append(['This is A1', 'This is B1', 'This is C1'])
+        * **or** append({'A' : 'This is A1', 'C' : 'This is C1'})
+        * **or** append({1 : 'This is A1', 3 : 'This is C1'})
+
+        :raise: TypeError when iterable is neither a list/tuple nor a dict
+
+        """
+        row_idx = self._current_row + 1
+
+        if (isinstance(iterable, (list, tuple, range))
+            or isgenerator(iterable)):
+            for col_idx, content in enumerate(iterable, 1):
+                if isinstance(content, Cell):
+                    # compatible with write-only mode
+                    cell = content
+                    if cell.parent and cell.parent != self:
+                        raise ValueError("Cells cannot be copied from other worksheets")
+                    cell.parent = self
+                    cell.column = col_idx
+                    cell.row = row_idx
+                else:
+                    cell = Cell(self, row=row_idx, column=col_idx, value=content)
+                self._cells[(row_idx, col_idx)] = cell
+
+        elif isinstance(iterable, dict):
+            for col_idx, content in iterable.items():
+                if isinstance(col_idx, str):
+                    col_idx = column_index_from_string(col_idx)
+                cell = Cell(self, row=row_idx, column=col_idx, value=content)
+                self._cells[(row_idx, col_idx)] = cell
+
+        else:
+            self._invalid_row(iterable)
+
+        self._current_row = row_idx
+
+
+    def _move_cells(self, min_row=None, min_col=None, offset=0, row_or_col="row"):
+        """
+        Move either rows or columns around by the offset
+        """
+        reverse = offset > 0 # start at the end if inserting
+        row_offset = 0
+        col_offset = 0
+
+        # need to make affected ranges contiguous
+        if row_or_col == 'row':
+            cells = self.iter_rows(min_row=min_row)
+            row_offset = offset
+            key = 0
+        else:
+            cells = self.iter_cols(min_col=min_col)
+            col_offset = offset
+            key = 1
+        cells = list(cells)
+
+        for row, column in sorted(self._cells, key=itemgetter(key), reverse=reverse):
+            if min_row and row < min_row:
+                continue
+            elif min_col and column < min_col:
+                continue
+
+            self._move_cell(row, column, row_offset, col_offset)
+
+
+    def insert_rows(self, idx, amount=1):
+        """
+        Insert row or rows before row==idx
+        """
+        self._move_cells(min_row=idx, offset=amount, row_or_col="row")
+        self._current_row = self.max_row
+
+
+    def insert_cols(self, idx, amount=1):
+        """
+        Insert column or columns before col==idx
+        """
+        self._move_cells(min_col=idx, offset=amount, row_or_col="column")
+
+
+    def delete_rows(self, idx, amount=1):
+        """
+        Delete row or rows from row==idx
+        """
+
+        remainder = _gutter(idx, amount, self.max_row)
+
+        self._move_cells(min_row=idx+amount, offset=-amount, row_or_col="row")
+
+        # calculating min and max col is an expensive operation, do it only once
+        min_col = self.min_column
+        max_col = self.max_column + 1
+        for row in remainder:
+            for col in range(min_col, max_col):
+                if (row, col) in self._cells:
+                    del self._cells[row, col]
+        self._current_row = self.max_row
+        if not self._cells:
+            self._current_row = 0
+
+
+    def delete_cols(self, idx, amount=1):
+        """
+        Delete column or columns from col==idx
+        """
+
+        remainder = _gutter(idx, amount, self.max_column)
+
+        self._move_cells(min_col=idx+amount, offset=-amount, row_or_col="column")
+
+        # calculating min and max row is an expensive operation, do it only once
+        min_row = self.min_row
+        max_row = self.max_row + 1
+        for col in remainder:
+            for row in range(min_row, max_row):
+                if (row, col) in self._cells:
+                    del self._cells[row, col]
+
+
+    def move_range(self, cell_range, rows=0, cols=0, translate=False):
+        """
+        Move a cell range by the number of rows and/or columns:
+        down if rows > 0 and up if rows < 0
+        right if cols > 0 and left if cols < 0
+        Existing cells will be overwritten.
+        Formulae and references will not be updated.
+        """
+        if isinstance(cell_range, str):
+            cell_range = CellRange(cell_range)
+        if not isinstance(cell_range, CellRange):
+            raise ValueError("Only CellRange objects can be moved")
+        if not rows and not cols:
+            return
+
+        down = rows > 0
+        right = cols > 0
+
+        if rows:
+            cells = sorted(cell_range.rows, reverse=down)
+        else:
+            cells = sorted(cell_range.cols, reverse=right)
+
+        for row, col in chain.from_iterable(cells):
+            self._move_cell(row, col, rows, cols, translate)
+
+        # rebase moved range
+        cell_range.shift(row_shift=rows, col_shift=cols)
+
+
+    def _move_cell(self, row, column, row_offset, col_offset, translate=False):
+        """
+        Move a cell from one place to another.
+        Delete at old index
+        Rebase coordinate
+        """
+        cell = self._get_cell(row, column)
+        new_row = cell.row + row_offset
+        new_col = cell.column + col_offset
+        self._cells[new_row, new_col] = cell
+        del self._cells[(cell.row, cell.column)]
+        cell.row = new_row
+        cell.column = new_col
+        if translate and cell.data_type == "f":
+            t = Translator(cell.value, cell.coordinate)
+            cell.value = t.translate_formula(row_delta=row_offset, col_delta=col_offset)
+
+
+    def _invalid_row(self, iterable):
+        raise TypeError('Value must be a list, tuple, range or generator, or a dict. Supplied value is {0}'.format(
+            type(iterable))
+                        )
+
+
+    def _add_column(self):
+        """Dimension factory for column information"""
+
+        return ColumnDimension(self)
+
+    def _add_row(self):
+        """Dimension factory for row information"""
+
+        return RowDimension(self)
+
+
+    @property
+    def print_title_rows(self):
+        """Rows to be printed at the top of every page (ex: '1:3')"""
+        if self._print_rows:
+            return str(self._print_rows)
+
+
+    @print_title_rows.setter
+    def print_title_rows(self, rows):
+        """
+        Set rows to be printed on the top of every page
+        format `1:3`
+        """
+        if rows is not None:
+            self._print_rows = RowRange(rows)
+
+
+    @property
+    def print_title_cols(self):
+        """Columns to be printed at the left side of every page (ex: 'A:C')"""
+        if self._print_cols:
+            return str(self._print_cols)
+
+
+    @print_title_cols.setter
+    def print_title_cols(self, cols):
+        """
+        Set cols to be printed on the left of every page
+        format ``A:C`
+        """
+        if cols is not None:
+            self._print_cols = ColRange(cols)
+
+
+    @property
+    def print_titles(self):
+        titles = PrintTitles(cols=self._print_cols, rows=self._print_rows, title=self.title)
+        return str(titles)
+
+
+    @property
+    def print_area(self):
+        """
+        The print area for the worksheet, or None if not set. To set, supply a range
+        like 'A1:D4' or a list of ranges.
+        """
+        self._print_area.title = self.title
+        return str(self._print_area)
+
+
+    @print_area.setter
+    def print_area(self, value):
+        """
+        Range of cells in the form A1:D4 or list of ranges. Print area can be cleared
+        by passing `None` or an empty list
+        """
+        if not value:
+            self._print_area = PrintArea()
+        elif isinstance(value, str):
+            self._print_area = PrintArea.from_string(value)
+        elif hasattr(value, "__iter__"):
+            self._print_area = PrintArea.from_string(",".join(value))
+
+
+def _gutter(idx, offset, max_val):
+    """
+    When deleting rows and columns are deleted we rely on overwriting.
+    This may not be the case for a large offset on small set of cells:
+    range(cells_to_delete) > range(cell_to_be_moved)
+    """
+    gutter = range(max(max_val+1-offset, idx), min(idx+offset, max_val)+1)
+    return gutter
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/writer/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/writer/__init__.py
new file mode 100644
index 00000000..ab6cdead
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/writer/__init__.py
@@ -0,0 +1 @@
+# Copyright (c) 2010-2024 openpyxl
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/writer/excel.py b/.venv/lib/python3.12/site-packages/openpyxl/writer/excel.py
new file mode 100644
index 00000000..c1154fd2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/writer/excel.py
@@ -0,0 +1,295 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+# Python stdlib imports
+import datetime
+import re
+from zipfile import ZipFile, ZIP_DEFLATED
+
+# package imports
+from openpyxl.utils.exceptions import InvalidFileException
+from openpyxl.xml.constants import (
+    ARC_ROOT_RELS,
+    ARC_WORKBOOK_RELS,
+    ARC_APP,
+    ARC_CORE,
+    ARC_CUSTOM,
+    CPROPS_TYPE,
+    ARC_THEME,
+    ARC_STYLE,
+    ARC_WORKBOOK,
+    )
+from openpyxl.drawing.spreadsheet_drawing import SpreadsheetDrawing
+from openpyxl.xml.functions import tostring, fromstring
+from openpyxl.packaging.manifest import Manifest
+from openpyxl.packaging.relationship import (
+    get_rels_path,
+    RelationshipList,
+    Relationship,
+)
+from openpyxl.comments.comment_sheet import CommentSheet
+from openpyxl.styles.stylesheet import write_stylesheet
+from openpyxl.worksheet._writer import WorksheetWriter
+from openpyxl.workbook._writer import WorkbookWriter
+from .theme import theme_xml
+
+
+class ExcelWriter:
+    """Write a workbook object to an Excel file."""
+
+    def __init__(self, workbook, archive):
+        self._archive = archive
+        self.workbook = workbook
+        self.manifest = Manifest()
+        self.vba_modified = set()
+        self._tables = []
+        self._charts = []
+        self._images = []
+        self._drawings = []
+        self._comments = []
+        self._pivots = []
+
+
+    def write_data(self):
+        from openpyxl.packaging.extended import ExtendedProperties
+        """Write the various xml files into the zip archive."""
+        # cleanup all worksheets
+        archive = self._archive
+
+        props = ExtendedProperties()
+        archive.writestr(ARC_APP, tostring(props.to_tree()))
+
+        archive.writestr(ARC_CORE, tostring(self.workbook.properties.to_tree()))
+        if self.workbook.loaded_theme:
+            archive.writestr(ARC_THEME, self.workbook.loaded_theme)
+        else:
+            archive.writestr(ARC_THEME, theme_xml)
+
+        if len(self.workbook.custom_doc_props) >= 1:
+            archive.writestr(ARC_CUSTOM, tostring(self.workbook.custom_doc_props.to_tree()))
+            class CustomOverride():
+                path = "/" + ARC_CUSTOM #PartName
+                mime_type = CPROPS_TYPE #ContentType
+
+            custom_override = CustomOverride()
+            self.manifest.append(custom_override)
+
+        self._write_worksheets()
+        self._write_chartsheets()
+        self._write_images()
+        self._write_charts()
+
+        self._write_external_links()
+
+        stylesheet = write_stylesheet(self.workbook)
+        archive.writestr(ARC_STYLE, tostring(stylesheet))
+
+        writer = WorkbookWriter(self.workbook)
+        archive.writestr(ARC_ROOT_RELS, writer.write_root_rels())
+        archive.writestr(ARC_WORKBOOK, writer.write())
+        archive.writestr(ARC_WORKBOOK_RELS, writer.write_rels())
+
+        self._merge_vba()
+
+        self.manifest._write(archive, self.workbook)
+
+    def _merge_vba(self):
+        """
+        If workbook contains macros then extract associated files from cache
+        of old file and add to archive
+        """
+        ARC_VBA = re.compile("|".join(
+            ('xl/vba', r'xl/drawings/.*vmlDrawing\d\.vml',
+             'xl/ctrlProps', 'customUI', 'xl/activeX', r'xl/media/.*\.emf')
+        )
+                             )
+
+        if self.workbook.vba_archive:
+            for name in set(self.workbook.vba_archive.namelist()) - self.vba_modified:
+                if ARC_VBA.match(name):
+                    self._archive.writestr(name, self.workbook.vba_archive.read(name))
+
+
+    def _write_images(self):
+        # delegate to object
+        for img in self._images:
+            self._archive.writestr(img.path[1:], img._data())
+
+
+    def _write_charts(self):
+        # delegate to object
+        if len(self._charts) != len(set(self._charts)):
+            raise InvalidFileException("The same chart cannot be used in more than one worksheet")
+        for chart in self._charts:
+            self._archive.writestr(chart.path[1:], tostring(chart._write()))
+            self.manifest.append(chart)
+
+
+    def _write_drawing(self, drawing):
+        """
+        Write a drawing
+        """
+        self._drawings.append(drawing)
+        drawing._id = len(self._drawings)
+        for chart in drawing.charts:
+            self._charts.append(chart)
+            chart._id = len(self._charts)
+        for img in drawing.images:
+            self._images.append(img)
+            img._id = len(self._images)
+        rels_path = get_rels_path(drawing.path)[1:]
+        self._archive.writestr(drawing.path[1:], tostring(drawing._write()))
+        self._archive.writestr(rels_path, tostring(drawing._write_rels()))
+        self.manifest.append(drawing)
+
+
+    def _write_chartsheets(self):
+        for idx, sheet in enumerate(self.workbook.chartsheets, 1):
+
+            sheet._id = idx
+            xml = tostring(sheet.to_tree())
+
+            self._archive.writestr(sheet.path[1:], xml)
+            self.manifest.append(sheet)
+
+            if sheet._drawing:
+                self._write_drawing(sheet._drawing)
+
+                rel = Relationship(type="drawing", Target=sheet._drawing.path)
+                rels = RelationshipList()
+                rels.append(rel)
+                tree = rels.to_tree()
+
+                rels_path = get_rels_path(sheet.path[1:])
+                self._archive.writestr(rels_path, tostring(tree))
+
+
+    def _write_comment(self, ws):
+
+        cs = CommentSheet.from_comments(ws._comments)
+        self._comments.append(cs)
+        cs._id = len(self._comments)
+        self._archive.writestr(cs.path[1:], tostring(cs.to_tree()))
+        self.manifest.append(cs)
+
+        if ws.legacy_drawing is None or self.workbook.vba_archive is None:
+            ws.legacy_drawing = 'xl/drawings/commentsDrawing{0}.vml'.format(cs._id)
+            vml = None
+        else:
+            vml = fromstring(self.workbook.vba_archive.read(ws.legacy_drawing))
+
+        vml = cs.write_shapes(vml)
+
+        self._archive.writestr(ws.legacy_drawing, vml)
+        self.vba_modified.add(ws.legacy_drawing)
+
+        comment_rel = Relationship(Id="comments", type=cs._rel_type, Target=cs.path)
+        ws._rels.append(comment_rel)
+
+
+    def write_worksheet(self, ws):
+        ws._drawing = SpreadsheetDrawing()
+        ws._drawing.charts = ws._charts
+        ws._drawing.images = ws._images
+        if self.workbook.write_only:
+            if not ws.closed:
+                ws.close()
+            writer = ws._writer
+        else:
+            writer = WorksheetWriter(ws)
+            writer.write()
+
+        ws._rels = writer._rels
+        self._archive.write(writer.out, ws.path[1:])
+        self.manifest.append(ws)
+        writer.cleanup()
+
+
+    def _write_worksheets(self):
+
+        pivot_caches = set()
+
+        for idx, ws in enumerate(self.workbook.worksheets, 1):
+
+            ws._id = idx
+            self.write_worksheet(ws)
+
+            if ws._drawing:
+                self._write_drawing(ws._drawing)
+
+                for r in ws._rels:
+                    if "drawing" in r.Type:
+                        r.Target = ws._drawing.path
+
+            if ws._comments:
+                self._write_comment(ws)
+
+            if ws.legacy_drawing is not None:
+                shape_rel = Relationship(type="vmlDrawing", Id="anysvml",
+                                         Target="/" + ws.legacy_drawing)
+                ws._rels.append(shape_rel)
+
+            for t in ws._tables.values():
+                self._tables.append(t)
+                t.id = len(self._tables)
+                t._write(self._archive)
+                self.manifest.append(t)
+                ws._rels.get(t._rel_id).Target = t.path
+
+            for p in ws._pivots:
+                if p.cache not in pivot_caches:
+                    pivot_caches.add(p.cache)
+                    p.cache._id = len(pivot_caches)
+
+                self._pivots.append(p)
+                p._id = len(self._pivots)
+                p._write(self._archive, self.manifest)
+                self.workbook._pivots.append(p)
+                r = Relationship(Type=p.rel_type, Target=p.path)
+                ws._rels.append(r)
+
+            if ws._rels:
+                tree = ws._rels.to_tree()
+                rels_path = get_rels_path(ws.path)[1:]
+                self._archive.writestr(rels_path, tostring(tree))
+
+
+    def _write_external_links(self):
+        # delegate to object
+        """Write links to external workbooks"""
+        wb = self.workbook
+        for idx, link in enumerate(wb._external_links, 1):
+            link._id = idx
+            rels_path = get_rels_path(link.path[1:])
+
+            xml = link.to_tree()
+            self._archive.writestr(link.path[1:], tostring(xml))
+            rels = RelationshipList()
+            rels.append(link.file_link)
+            self._archive.writestr(rels_path, tostring(rels.to_tree()))
+            self.manifest.append(link)
+
+
+    def save(self):
+        """Write data into the archive."""
+        self.write_data()
+        self._archive.close()
+
+
+def save_workbook(workbook, filename):
+    """Save the given workbook on the filesystem under the name filename.
+
+    :param workbook: the workbook to save
+    :type workbook: :class:`openpyxl.workbook.Workbook`
+
+    :param filename: the path to which save the workbook
+    :type filename: string
+
+    :rtype: bool
+
+    """
+    archive = ZipFile(filename, 'w', ZIP_DEFLATED, allowZip64=True)
+    workbook.properties.modified = datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
+    writer = ExcelWriter(workbook, archive)
+    writer.save()
+    return True
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/writer/theme.py b/.venv/lib/python3.12/site-packages/openpyxl/writer/theme.py
new file mode 100644
index 00000000..20c1d607
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/writer/theme.py
@@ -0,0 +1,291 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""Write the theme xml based on a fixed string."""
+
+
+theme_xml = """<?xml version="1.0"?>
+<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
+  <a:themeElements>
+    <a:clrScheme name="Office">
+      <a:dk1>
+        <a:sysClr val="windowText" lastClr="000000"/>
+      </a:dk1>
+      <a:lt1>
+        <a:sysClr val="window" lastClr="FFFFFF"/>
+      </a:lt1>
+      <a:dk2>
+        <a:srgbClr val="1F497D"/>
+      </a:dk2>
+      <a:lt2>
+        <a:srgbClr val="EEECE1"/>
+      </a:lt2>
+      <a:accent1>
+        <a:srgbClr val="4F81BD"/>
+      </a:accent1>
+      <a:accent2>
+        <a:srgbClr val="C0504D"/>
+      </a:accent2>
+      <a:accent3>
+        <a:srgbClr val="9BBB59"/>
+      </a:accent3>
+      <a:accent4>
+        <a:srgbClr val="8064A2"/>
+      </a:accent4>
+      <a:accent5>
+        <a:srgbClr val="4BACC6"/>
+      </a:accent5>
+      <a:accent6>
+        <a:srgbClr val="F79646"/>
+      </a:accent6>
+      <a:hlink>
+        <a:srgbClr val="0000FF"/>
+      </a:hlink>
+      <a:folHlink>
+        <a:srgbClr val="800080"/>
+      </a:folHlink>
+    </a:clrScheme>
+    <a:fontScheme name="Office">
+      <a:majorFont>
+        <a:latin typeface="Cambria"/>
+        <a:ea typeface=""/>
+        <a:cs typeface=""/>
+        <a:font script="Jpan" typeface="&#xFF2D;&#xFF33; &#xFF30;&#x30B4;&#x30B7;&#x30C3;&#x30AF;"/>
+        <a:font script="Hang" typeface="&#xB9D1;&#xC740; &#xACE0;&#xB515;"/>
+        <a:font script="Hans" typeface="&#x5B8B;&#x4F53;"/>
+        <a:font script="Hant" typeface="&#x65B0;&#x7D30;&#x660E;&#x9AD4;"/>
+        <a:font script="Arab" typeface="Times New Roman"/>
+        <a:font script="Hebr" typeface="Times New Roman"/>
+        <a:font script="Thai" typeface="Tahoma"/>
+        <a:font script="Ethi" typeface="Nyala"/>
+        <a:font script="Beng" typeface="Vrinda"/>
+        <a:font script="Gujr" typeface="Shruti"/>
+        <a:font script="Khmr" typeface="MoolBoran"/>
+        <a:font script="Knda" typeface="Tunga"/>
+        <a:font script="Guru" typeface="Raavi"/>
+        <a:font script="Cans" typeface="Euphemia"/>
+        <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+        <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+        <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+        <a:font script="Thaa" typeface="MV Boli"/>
+        <a:font script="Deva" typeface="Mangal"/>
+        <a:font script="Telu" typeface="Gautami"/>
+        <a:font script="Taml" typeface="Latha"/>
+        <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+        <a:font script="Orya" typeface="Kalinga"/>
+        <a:font script="Mlym" typeface="Kartika"/>
+        <a:font script="Laoo" typeface="DokChampa"/>
+        <a:font script="Sinh" typeface="Iskoola Pota"/>
+        <a:font script="Mong" typeface="Mongolian Baiti"/>
+        <a:font script="Viet" typeface="Times New Roman"/>
+        <a:font script="Uigh" typeface="Microsoft Uighur"/>
+      </a:majorFont>
+      <a:minorFont>
+        <a:latin typeface="Calibri"/>
+        <a:ea typeface=""/>
+        <a:cs typeface=""/>
+        <a:font script="Jpan" typeface="&#xFF2D;&#xFF33; &#xFF30;&#x30B4;&#x30B7;&#x30C3;&#x30AF;"/>
+        <a:font script="Hang" typeface="&#xB9D1;&#xC740; &#xACE0;&#xB515;"/>
+        <a:font script="Hans" typeface="&#x5B8B;&#x4F53;"/>
+        <a:font script="Hant" typeface="&#x65B0;&#x7D30;&#x660E;&#x9AD4;"/>
+        <a:font script="Arab" typeface="Arial"/>
+        <a:font script="Hebr" typeface="Arial"/>
+        <a:font script="Thai" typeface="Tahoma"/>
+        <a:font script="Ethi" typeface="Nyala"/>
+        <a:font script="Beng" typeface="Vrinda"/>
+        <a:font script="Gujr" typeface="Shruti"/>
+        <a:font script="Khmr" typeface="DaunPenh"/>
+        <a:font script="Knda" typeface="Tunga"/>
+        <a:font script="Guru" typeface="Raavi"/>
+        <a:font script="Cans" typeface="Euphemia"/>
+        <a:font script="Cher" typeface="Plantagenet Cherokee"/>
+        <a:font script="Yiii" typeface="Microsoft Yi Baiti"/>
+        <a:font script="Tibt" typeface="Microsoft Himalaya"/>
+        <a:font script="Thaa" typeface="MV Boli"/>
+        <a:font script="Deva" typeface="Mangal"/>
+        <a:font script="Telu" typeface="Gautami"/>
+        <a:font script="Taml" typeface="Latha"/>
+        <a:font script="Syrc" typeface="Estrangelo Edessa"/>
+        <a:font script="Orya" typeface="Kalinga"/>
+        <a:font script="Mlym" typeface="Kartika"/>
+        <a:font script="Laoo" typeface="DokChampa"/>
+        <a:font script="Sinh" typeface="Iskoola Pota"/>
+        <a:font script="Mong" typeface="Mongolian Baiti"/>
+        <a:font script="Viet" typeface="Arial"/>
+        <a:font script="Uigh" typeface="Microsoft Uighur"/>
+      </a:minorFont>
+    </a:fontScheme>
+    <a:fmtScheme name="Office">
+      <a:fillStyleLst>
+        <a:solidFill>
+          <a:schemeClr val="phClr"/>
+        </a:solidFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="50000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="35000">
+              <a:schemeClr val="phClr">
+                <a:tint val="37000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:tint val="15000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:lin ang="16200000" scaled="1"/>
+        </a:gradFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:shade val="51000"/>
+                <a:satMod val="130000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="80000">
+              <a:schemeClr val="phClr">
+                <a:shade val="93000"/>
+                <a:satMod val="130000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:shade val="94000"/>
+                <a:satMod val="135000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:lin ang="16200000" scaled="0"/>
+        </a:gradFill>
+      </a:fillStyleLst>
+      <a:lnStyleLst>
+        <a:ln w="9525" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr">
+              <a:shade val="95000"/>
+              <a:satMod val="105000"/>
+            </a:schemeClr>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+        <a:ln w="25400" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr"/>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+        <a:ln w="38100" cap="flat" cmpd="sng" algn="ctr">
+          <a:solidFill>
+            <a:schemeClr val="phClr"/>
+          </a:solidFill>
+          <a:prstDash val="solid"/>
+        </a:ln>
+      </a:lnStyleLst>
+      <a:effectStyleLst>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="20000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="38000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+        </a:effectStyle>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="35000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+        </a:effectStyle>
+        <a:effectStyle>
+          <a:effectLst>
+            <a:outerShdw blurRad="40000" dist="23000" dir="5400000" rotWithShape="0">
+              <a:srgbClr val="000000">
+                <a:alpha val="35000"/>
+              </a:srgbClr>
+            </a:outerShdw>
+          </a:effectLst>
+          <a:scene3d>
+            <a:camera prst="orthographicFront">
+              <a:rot lat="0" lon="0" rev="0"/>
+            </a:camera>
+            <a:lightRig rig="threePt" dir="t">
+              <a:rot lat="0" lon="0" rev="1200000"/>
+            </a:lightRig>
+          </a:scene3d>
+          <a:sp3d>
+            <a:bevelT w="63500" h="25400"/>
+          </a:sp3d>
+        </a:effectStyle>
+      </a:effectStyleLst>
+      <a:bgFillStyleLst>
+        <a:solidFill>
+          <a:schemeClr val="phClr"/>
+        </a:solidFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="40000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="40000">
+              <a:schemeClr val="phClr">
+                <a:tint val="45000"/>
+                <a:shade val="99000"/>
+                <a:satMod val="350000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:shade val="20000"/>
+                <a:satMod val="255000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:path path="circle">
+            <a:fillToRect l="50000" t="-80000" r="50000" b="180000"/>
+          </a:path>
+        </a:gradFill>
+        <a:gradFill rotWithShape="1">
+          <a:gsLst>
+            <a:gs pos="0">
+              <a:schemeClr val="phClr">
+                <a:tint val="80000"/>
+                <a:satMod val="300000"/>
+              </a:schemeClr>
+            </a:gs>
+            <a:gs pos="100000">
+              <a:schemeClr val="phClr">
+                <a:shade val="30000"/>
+                <a:satMod val="200000"/>
+              </a:schemeClr>
+            </a:gs>
+          </a:gsLst>
+          <a:path path="circle">
+            <a:fillToRect l="50000" t="50000" r="50000" b="50000"/>
+          </a:path>
+        </a:gradFill>
+      </a:bgFillStyleLst>
+    </a:fmtScheme>
+  </a:themeElements>
+  <a:objectDefaults/>
+  <a:extraClrSchemeLst/>
+</a:theme>
+"""
+
+def write_theme():
+    """Write the theme xml."""
+    return theme_xml
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/xml/__init__.py b/.venv/lib/python3.12/site-packages/openpyxl/xml/__init__.py
new file mode 100644
index 00000000..db510aa1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/xml/__init__.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""Collection of XML resources compatible across different Python versions"""
+import os
+
+
+def lxml_available():
+    try:
+        from lxml.etree import LXML_VERSION
+        LXML = LXML_VERSION >= (3, 3, 1, 0)
+        if not LXML:
+            import warnings
+            warnings.warn("The installed version of lxml is too old to be used with openpyxl")
+            return False  # we have it, but too old
+        else:
+            return True  # we have it, and recent enough
+    except ImportError:
+        return False  # we don't even have it
+
+
+def lxml_env_set():
+    return os.environ.get("OPENPYXL_LXML", "True") == "True"
+
+
+LXML = lxml_available() and lxml_env_set()
+
+
+def defusedxml_available():
+    try:
+        import defusedxml # noqa
+    except ImportError:
+        return False
+    else:
+        return True
+
+
+def defusedxml_env_set():
+    return os.environ.get("OPENPYXL_DEFUSEDXML", "True") == "True"
+
+
+DEFUSEDXML = defusedxml_available() and defusedxml_env_set()
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/xml/constants.py b/.venv/lib/python3.12/site-packages/openpyxl/xml/constants.py
new file mode 100644
index 00000000..4e0fd433
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/xml/constants.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2010-2024 openpyxl
+
+
+"""Constants for fixed paths in a file and xml namespace urls."""
+
+MIN_ROW = 0
+MIN_COLUMN = 0
+MAX_COLUMN = 16384
+MAX_ROW = 1048576
+
+# constants
+PACKAGE_PROPS = 'docProps'
+PACKAGE_XL = 'xl'
+PACKAGE_RELS = '_rels'
+PACKAGE_THEME = PACKAGE_XL + '/' + 'theme'
+PACKAGE_WORKSHEETS = PACKAGE_XL + '/' + 'worksheets'
+PACKAGE_CHARTSHEETS = PACKAGE_XL + '/' + 'chartsheets'
+PACKAGE_DRAWINGS = PACKAGE_XL + '/' + 'drawings'
+PACKAGE_CHARTS = PACKAGE_XL + '/' + 'charts'
+PACKAGE_IMAGES = PACKAGE_XL + '/' + 'media'
+PACKAGE_WORKSHEET_RELS = PACKAGE_WORKSHEETS + '/' + '_rels'
+PACKAGE_CHARTSHEETS_RELS = PACKAGE_CHARTSHEETS + '/' + '_rels'
+PACKAGE_PIVOT_TABLE = PACKAGE_XL + '/' + 'pivotTables'
+PACKAGE_PIVOT_CACHE = PACKAGE_XL + '/' + 'pivotCache'
+
+ARC_CONTENT_TYPES = '[Content_Types].xml'
+ARC_ROOT_RELS = PACKAGE_RELS + '/.rels'
+ARC_WORKBOOK_RELS = PACKAGE_XL + '/' + PACKAGE_RELS + '/workbook.xml.rels'
+ARC_CORE = PACKAGE_PROPS + '/core.xml'
+ARC_APP = PACKAGE_PROPS + '/app.xml'
+ARC_CUSTOM = PACKAGE_PROPS + '/custom.xml'
+ARC_WORKBOOK = PACKAGE_XL + '/workbook.xml'
+ARC_STYLE = PACKAGE_XL + '/styles.xml'
+ARC_THEME = PACKAGE_THEME + '/theme1.xml'
+ARC_SHARED_STRINGS = PACKAGE_XL + '/sharedStrings.xml'
+ARC_CUSTOM_UI = 'customUI/customUI.xml'
+
+## namespaces
+# XML
+XML_NS = "http://www.w3.org/XML/1998/namespace"
+# Dublin Core
+DCORE_NS = 'http://purl.org/dc/elements/1.1/'
+DCTERMS_NS = 'http://purl.org/dc/terms/'
+DCTERMS_PREFIX = 'dcterms'
+
+# Document
+DOC_NS = "http://schemas.openxmlformats.org/officeDocument/2006/"
+REL_NS = DOC_NS + "relationships"
+COMMENTS_NS = REL_NS + "/comments"
+IMAGE_NS = REL_NS + "/image"
+VML_NS =  REL_NS + "/vmlDrawing"
+VTYPES_NS = DOC_NS + 'docPropsVTypes'
+XPROPS_NS = DOC_NS + 'extended-properties'
+CUSTPROPS_NS = DOC_NS + 'custom-properties'
+EXTERNAL_LINK_NS = REL_NS + "/externalLink"
+
+# CustomDocumentProperty FMTID:
+CPROPS_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
+
+# Package
+PKG_NS = "http://schemas.openxmlformats.org/package/2006/"
+PKG_REL_NS = PKG_NS + "relationships"
+COREPROPS_NS = PKG_NS + 'metadata/core-properties'
+CONTYPES_NS = PKG_NS + 'content-types'
+
+XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
+XML_NS = 'http://www.w3.org/XML/1998/namespace'
+SHEET_MAIN_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
+
+# Drawing
+CHART_NS = "http://schemas.openxmlformats.org/drawingml/2006/chart"
+DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
+SHEET_DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
+CHART_DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
+
+CUSTOMUI_NS = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility'
+
+
+NAMESPACES = {
+    'cp': COREPROPS_NS,
+    'dc': DCORE_NS,
+    DCTERMS_PREFIX: DCTERMS_NS,
+    'dcmitype': 'http://purl.org/dc/dcmitype/',
+    'xsi': XSI_NS,
+    'vt': VTYPES_NS,
+    'xml': XML_NS,
+    'main': SHEET_MAIN_NS,
+    'cust': CUSTPROPS_NS,
+}
+
+## Mime types
+WORKBOOK_MACRO = "application/vnd.ms-excel.%s.macroEnabled.main+xml"
+WORKBOOK = "application/vnd.openxmlformats-officedocument.spreadsheetml.%s.main+xml"
+SPREADSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.%s+xml"
+SHARED_STRINGS = SPREADSHEET % "sharedStrings"
+EXTERNAL_LINK = SPREADSHEET % "externalLink"
+WORKSHEET_TYPE = SPREADSHEET % "worksheet"
+COMMENTS_TYPE = SPREADSHEET % "comments"
+STYLES_TYPE = SPREADSHEET % "styles"
+CHARTSHEET_TYPE = SPREADSHEET % "chartsheet"
+DRAWING_TYPE = "application/vnd.openxmlformats-officedocument.drawing+xml"
+CHART_TYPE = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
+CHARTSHAPE_TYPE = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
+THEME_TYPE = "application/vnd.openxmlformats-officedocument.theme+xml"
+CPROPS_TYPE = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
+XLTM = WORKBOOK_MACRO % 'template'
+XLSM = WORKBOOK_MACRO % 'sheet'
+XLTX = WORKBOOK % 'template'
+XLSX = WORKBOOK % 'sheet'
+
+
+# Extensions to the specification
+
+EXT_TYPES = {
+    '{78C0D931-6437-407D-A8EE-F0AAD7539E65}': 'Conditional Formatting',
+    '{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}': 'Data Validation',
+    '{05C60535-1F16-4FD2-B633-F4F36F0B64E0}': 'Sparkline Group',
+    '{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}': 'Slicer List',
+    '{FC87AEE6-9EDD-4A0A-B7FB-166176984837}': 'Protected Range',
+    '{01252117-D84E-4E92-8308-4BE1C098FCBB}': 'Ignored Error',
+    '{F7C9EE02-42E1-4005-9D12-6889AFFD525C}': 'Web Extension',
+    '{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}': 'Slicer List',
+    '{7E03D99C-DC04-49d9-9315-930204A7B6E9}': 'Timeline Ref',
+}
+
+# Objects related to macros that we preserve
+CTRL = "application/vnd.ms-excel.controlproperties+xml"
+ACTIVEX = "application/vnd.ms-office.activeX+xml"
+VBA = "application/vnd.ms-office.vbaProject"
diff --git a/.venv/lib/python3.12/site-packages/openpyxl/xml/functions.py b/.venv/lib/python3.12/site-packages/openpyxl/xml/functions.py
new file mode 100644
index 00000000..385cca60
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/openpyxl/xml/functions.py
@@ -0,0 +1,87 @@
+# Copyright (c) 2010-2024 openpyxl
+
+"""
+XML compatibility functions
+"""
+
+# Python stdlib imports
+import re
+from functools import partial
+
+from openpyxl import DEFUSEDXML, LXML
+
+if LXML is True:
+    from lxml.etree import (
+    Element,
+    SubElement,
+    register_namespace,
+    QName,
+    xmlfile,
+    XMLParser,
+    )
+    from lxml.etree import fromstring, tostring
+    # do not resolve entities
+    safe_parser = XMLParser(resolve_entities=False)
+    fromstring = partial(fromstring, parser=safe_parser)
+
+else:
+    from xml.etree.ElementTree import (
+    Element,
+    SubElement,
+    fromstring,
+    tostring,
+    QName,
+    register_namespace
+    )
+    from et_xmlfile import xmlfile
+    if DEFUSEDXML is True:
+        from defusedxml.ElementTree import fromstring
+
+from xml.etree.ElementTree import iterparse
+if DEFUSEDXML is True:
+    from defusedxml.ElementTree import iterparse
+
+from openpyxl.xml.constants import (
+    CHART_NS,
+    DRAWING_NS,
+    SHEET_DRAWING_NS,
+    CHART_DRAWING_NS,
+    SHEET_MAIN_NS,
+    REL_NS,
+    VTYPES_NS,
+    COREPROPS_NS,
+    CUSTPROPS_NS,
+    DCTERMS_NS,
+    DCTERMS_PREFIX,
+    XML_NS
+)
+
+register_namespace(DCTERMS_PREFIX, DCTERMS_NS)
+register_namespace('dcmitype', 'http://purl.org/dc/dcmitype/')
+register_namespace('cp', COREPROPS_NS)
+register_namespace('c', CHART_NS)
+register_namespace('a', DRAWING_NS)
+register_namespace('s', SHEET_MAIN_NS)
+register_namespace('r', REL_NS)
+register_namespace('vt', VTYPES_NS)
+register_namespace('xdr', SHEET_DRAWING_NS)
+register_namespace('cdr', CHART_DRAWING_NS)
+register_namespace('xml', XML_NS)
+register_namespace('cust', CUSTPROPS_NS)
+
+
+tostring = partial(tostring, encoding="utf-8")
+
+NS_REGEX = re.compile("({(?P<namespace>.*)})?(?P<localname>.*)")
+
+def localname(node):
+    if callable(node.tag):
+        return "comment"
+    m = NS_REGEX.match(node.tag)
+    return m.group('localname')
+
+
+def whitespace(node):
+    stripped = node.text.strip()
+    if stripped and node.text != stripped:
+        node.set("{%s}space" % XML_NS, "preserve")