about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/openpyxl/workbook
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/openpyxl/workbook')
-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
14 files changed, 1864 insertions, 0 deletions
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
+