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