1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# Copyright (c) 2010-2024 openpyxl
 
import posixpath
from warnings import warn
 
from openpyxl.descriptors import (
    String,
    Alias,
    Sequence,
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors.container import ElementList
 
from openpyxl.xml.constants import REL_NS, PKG_REL_NS
from openpyxl.xml.functions import (
    Element,
    fromstring,
)
 
 
class Relationship(Serialisable):
    """Represents many kinds of relationships."""
 
    tagname = "Relationship"
 
    Type = String()
    Target = String()
    target = Alias("Target")
    TargetMode = String(allow_none=True)
    Id = String(allow_none=True)
    id = Alias("Id")
 
 
    def __init__(self,
                 Id=None,
                 Type=None,
                 type=None,
                 Target=None,
                 TargetMode=None
                 ):
        """
        `type` can be used as a shorthand with the default relationships namespace
        otherwise the `Type` must be a fully qualified URL
        """
        if type is not None:
            Type = "{0}/{1}".format(REL_NS, type)
        self.Type = Type
        self.Target = Target
        self.TargetMode = TargetMode
        self.Id = Id
 
 
class RelationshipList(ElementList):
 
    tagname = "Relationships"
    expected_type = Relationship
 
 
    def append(self, value):
        super().append(value)
        if not value.Id:
            value.Id = f"rId{len(self)}"
 
 
    def find(self, content_type):
        """
        Find relationships by content-type
        NB. these content-types namespaced objects and different to the MIME-types
        in the package manifest :-(
        """
        for r in self:
            if r.Type == content_type:
                yield r
 
 
    def get(self, key):
        for r in self:
            if r.Id == key:
                return r
        raise KeyError("Unknown relationship: {0}".format(key))
 
 
    def to_dict(self):
        """Return a dictionary of relations keyed by id"""
        return {r.id:r for r in self}
 
 
    def to_tree(self):
        tree = super().to_tree()
        tree.set("xmlns", PKG_REL_NS)
        return tree
 
 
def get_rels_path(path):
    """
    Convert relative path to absolutes that can be loaded from a zip
    archive.
    The path to be passed in is that of containing object (workbook,
    worksheet, etc.)
    """
    folder, obj = posixpath.split(path)
    filename = posixpath.join(folder, '_rels', '{0}.rels'.format(obj))
    return filename
 
 
def get_dependents(archive, filename):
    """
    Normalise dependency file paths to absolute ones
 
    Relative paths are relative to parent object
    """
    src = archive.read(filename)
    node = fromstring(src)
    try:
        rels = RelationshipList.from_tree(node)
    except TypeError:
        msg = "{0} contains invalid dependency definitions".format(filename)
        warn(msg)
        rels = RelationshipList()
    folder = posixpath.dirname(filename)
    parent = posixpath.split(folder)[0]
    for r in rels:
        if r.TargetMode == "External":
            continue
        elif r.target.startswith("/"):
            r.target = r.target[1:]
        else:
            pth = posixpath.join(parent, r.target)
            r.target = posixpath.normpath(pth)
    return rels
 
 
def get_rel(archive, deps, id=None, cls=None):
    """
    Get related object based on id or rel_type
    """
    if not any([id, cls]):
        raise ValueError("Either the id or the content type are required")
    if id is not None:
        rel = deps.get(id)
    else:
        try:
            rel = next(deps.find(cls.rel_type))
        except StopIteration: # no known dependency
            return
 
    path = rel.target
    src = archive.read(path)
    tree = fromstring(src)
    obj = cls.from_tree(tree)
 
    rels_path = get_rels_path(path)
    try:
        obj.deps = get_dependents(archive, rels_path)
    except KeyError:
        obj.deps = []
 
    return obj