python-docx-oss 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
docx/__init__.py CHANGED
@@ -13,7 +13,7 @@ from docx.api import Document
13
13
  if TYPE_CHECKING:
14
14
  from docx.opc.part import Part
15
15
 
16
- __version__ = "0.2.0"
16
+ __version__ = "0.3.0"
17
17
 
18
18
 
19
19
  __all__ = ["Document"]
docx/document.py CHANGED
@@ -12,6 +12,7 @@ from docx.api import Document as open_document
12
12
  from docx.blkcntnr import BlockItemContainer
13
13
  from docx.enum.section import WD_SECTION
14
14
  from docx.enum.text import WD_BREAK
15
+ from docx.sanitize import SanitizationReport, sanitize_document
15
16
  from docx.section import Section, Sections
16
17
  from docx.shared import ElementProxy, Emu, Inches, Length
17
18
  from docx.text.run import Run
@@ -229,6 +230,27 @@ class Document(ElementProxy):
229
230
  """
230
231
  self._part.save(path_or_stream)
231
232
 
233
+ def sanitize(
234
+ self,
235
+ *,
236
+ personal_metadata: bool = False,
237
+ comments: bool = False,
238
+ web_extensions: bool = False,
239
+ formatting_revisions: bool = False,
240
+ ) -> SanitizationReport:
241
+ """Apply the explicitly selected package-level cleanup operations.
242
+
243
+ Every option defaults to |False| because sanitization is destructive. The
244
+ returned report describes all package and XML changes made to this document.
245
+ """
246
+ return sanitize_document(
247
+ self,
248
+ personal_metadata=personal_metadata,
249
+ comments=comments,
250
+ web_extensions=web_extensions,
251
+ formatting_revisions=formatting_revisions,
252
+ )
253
+
232
254
  @property
233
255
  def sections(self) -> Sections:
234
256
  """|Sections| object providing access to each section in this document."""
docx/opc/coreprops.py CHANGED
@@ -9,6 +9,7 @@ import datetime as dt
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  from docx.oxml.coreprops import CT_CoreProperties
12
+ from docx.oxml.ns import qn
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from docx.oxml.coreprops import CT_CoreProperties
@@ -21,6 +22,40 @@ class CoreProperties:
21
22
  def __init__(self, element: CT_CoreProperties):
22
23
  self._element = element
23
24
 
25
+ def deduplicate(self, *property_names: str) -> set[str]:
26
+ """Remove duplicate XML elements for the named core properties.
27
+
28
+ The first occurrence is retained so callers can subsequently normalize its
29
+ value through the regular property setter.
30
+ """
31
+ element_tags = {
32
+ "author": qn("dc:creator"),
33
+ "category": qn("cp:category"),
34
+ "comments": qn("dc:description"),
35
+ "content_status": qn("cp:contentStatus"),
36
+ "created": qn("dcterms:created"),
37
+ "identifier": qn("dc:identifier"),
38
+ "keywords": qn("cp:keywords"),
39
+ "language": qn("dc:language"),
40
+ "last_modified_by": qn("cp:lastModifiedBy"),
41
+ "last_printed": qn("cp:lastPrinted"),
42
+ "modified": qn("dcterms:modified"),
43
+ "revision": qn("cp:revision"),
44
+ "subject": qn("dc:subject"),
45
+ "title": qn("dc:title"),
46
+ "version": qn("cp:version"),
47
+ }
48
+ changed: set[str] = set()
49
+ for property_name in property_names:
50
+ try:
51
+ tag = element_tags[property_name]
52
+ except KeyError as exc:
53
+ raise ValueError(f"Unsupported core property: {property_name}") from exc
54
+ for duplicate in self._element.findall(tag)[1:]:
55
+ self._element.remove(duplicate)
56
+ changed.add(property_name)
57
+ return changed
58
+
24
59
  @property
25
60
  def author(self):
26
61
  return self._element.author_text
docx/opc/part.py CHANGED
@@ -144,6 +144,10 @@ class Part:
144
144
  rel = self.rels.get_or_add(reltype, cast(Part, target))
145
145
  return rel.rId
146
146
 
147
+ def replace_blob(self, blob: bytes) -> None:
148
+ """Replace the serialized payload of this non-XML part."""
149
+ self._blob = blob
150
+
147
151
  @property
148
152
  def related_parts(self):
149
153
  """Dictionary mapping related parts by rId, so child objects can resolve
@@ -235,6 +239,10 @@ class XmlPart(Part):
235
239
  """The root XML element of this XML part."""
236
240
  return self._element
237
241
 
242
+ def replace_blob(self, blob: bytes) -> None:
243
+ """Reject raw replacement because this part serializes from ``element``."""
244
+ raise TypeError("cannot replace the blob of an XmlPart; replace its element instead")
245
+
238
246
  @element.setter
239
247
  def element(self, e: BaseOxmlElement):
240
248
  self._element = e
@@ -0,0 +1,10 @@
1
+ """Package-level DOCX sanitization.
2
+
3
+ Callers use :meth:`docx.document.Document.sanitize`; this package keeps OPC graph
4
+ mutation and XML cleanup details behind that single method.
5
+ """
6
+
7
+ from docx.sanitize.cleaner import sanitize_document
8
+ from docx.sanitize.result import RemovedRelationship, SanitizationReport
9
+
10
+ __all__ = ["RemovedRelationship", "SanitizationReport", "sanitize_document"]
@@ -0,0 +1,221 @@
1
+ """Implementation of the package-level sanitization boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Iterator
6
+
7
+ from lxml import etree
8
+
9
+ from docx.opc.oxml import serialize_part_xml
10
+ from docx.opc.part import Part, XmlPart
11
+ from docx.oxml.ns import qn
12
+ from docx.oxml.parser import parse_xml
13
+ from docx.sanitize.result import RemovedRelationship, SanitizationReport
14
+
15
+ if TYPE_CHECKING:
16
+ from lxml.etree import _Element as etree_Element # pyright: ignore[reportPrivateUsage]
17
+
18
+ from docx.document import Document
19
+ from docx.opc.package import OpcPackage
20
+ from docx.oxml.xmlchemy import BaseOxmlElement
21
+
22
+
23
+ _COMMENT_MARKER_TAGS = (
24
+ qn("w:commentRangeStart"),
25
+ qn("w:commentRangeEnd"),
26
+ qn("w:commentReference"),
27
+ )
28
+ _COMMENT_RELATIONSHIP_NAMES = frozenset(
29
+ {
30
+ "commentauthors",
31
+ "comments",
32
+ "commentsextended",
33
+ "commentsextensible",
34
+ "commentsids",
35
+ "people",
36
+ }
37
+ )
38
+ _FORMATTING_REVISION_TAGS = tuple(
39
+ qn(tag)
40
+ for tag in (
41
+ "w:rPrChange",
42
+ "w:pPrChange",
43
+ "w:tblPrChange",
44
+ "w:trPrChange",
45
+ "w:tcPrChange",
46
+ "w:sectPrChange",
47
+ )
48
+ )
49
+ _WEB_EXTENSION_RELATIONSHIP_NAMES = frozenset({"webextension", "webextensiontaskpanes"})
50
+
51
+
52
+ def sanitize_document(
53
+ document: Document,
54
+ *,
55
+ personal_metadata: bool,
56
+ comments: bool,
57
+ web_extensions: bool,
58
+ formatting_revisions: bool,
59
+ ) -> SanitizationReport:
60
+ """Apply selected sanitizers while keeping all graph mechanics internal."""
61
+ package = document.part.package
62
+ assert package is not None
63
+
64
+ if not any((personal_metadata, comments, web_extensions, formatting_revisions)):
65
+ return SanitizationReport()
66
+
67
+ parts_before = _reachable_part_names(package)
68
+ scrubbed_properties: list[str] = []
69
+ removed_relationships: list[RemovedRelationship] = []
70
+ warnings: list[str] = []
71
+ removed_comment_markers = 0
72
+ removed_formatting_revisions = 0
73
+
74
+ if personal_metadata:
75
+ scrubbed_properties.extend(_scrub_personal_metadata(package))
76
+
77
+ if comments or formatting_revisions:
78
+ for part, root in _iter_word_xml(package, warnings):
79
+ part_changed = False
80
+ if comments:
81
+ count = _remove_elements(root, _COMMENT_MARKER_TAGS)
82
+ removed_comment_markers += count
83
+ part_changed = part_changed or count > 0
84
+ if formatting_revisions:
85
+ count = _remove_elements(root, _FORMATTING_REVISION_TAGS)
86
+ removed_formatting_revisions += count
87
+ part_changed = part_changed or count > 0
88
+ if part_changed and not isinstance(part, XmlPart):
89
+ part.replace_blob(serialize_part_xml(root))
90
+
91
+ if comments:
92
+ removed_relationships.extend(_drop_relationships(package, _COMMENT_RELATIONSHIP_NAMES))
93
+ if web_extensions:
94
+ removed_relationships.extend(
95
+ _drop_relationships(package, _WEB_EXTENSION_RELATIONSHIP_NAMES)
96
+ )
97
+
98
+ parts_after = _reachable_part_names(package)
99
+ return SanitizationReport(
100
+ scrubbed_properties=tuple(scrubbed_properties),
101
+ removed_comment_markers=removed_comment_markers,
102
+ removed_formatting_revisions=removed_formatting_revisions,
103
+ removed_parts=tuple(sorted(parts_before - parts_after)),
104
+ removed_relationships=tuple(removed_relationships),
105
+ warnings=tuple(warnings),
106
+ )
107
+
108
+
109
+ def _drop_relationships(
110
+ package: OpcPackage, relationship_names: frozenset[str]
111
+ ) -> list[RemovedRelationship]:
112
+ removed: list[RemovedRelationship] = []
113
+ for source_name, source in _relationship_sources(package):
114
+ for rId, rel in list(source.rels.items()):
115
+ if _relationship_name(rel.reltype) not in relationship_names:
116
+ continue
117
+ target = rel.target_ref if rel.is_external else str(rel.target_part.partname)
118
+ removed.append(RemovedRelationship(source_name, rId, rel.reltype, target))
119
+ del source.rels[rId]
120
+ return removed
121
+
122
+
123
+ def _iter_word_xml(
124
+ package: OpcPackage, warnings: list[str]
125
+ ) -> Iterator[tuple[Part, BaseOxmlElement]]:
126
+ for part in _reachable_parts(package):
127
+ if not str(part.partname).startswith("/word/"):
128
+ continue
129
+ if isinstance(part, XmlPart):
130
+ yield part, part.element
131
+ continue
132
+ if not part.content_type.endswith("+xml") and part.content_type != "application/xml":
133
+ continue
134
+ try:
135
+ yield part, parse_xml(part.blob)
136
+ except etree.XMLSyntaxError as exc:
137
+ warnings.append(f"Could not sanitize XML part {part.partname}: {exc}")
138
+
139
+
140
+ def _reachable_parts(package: OpcPackage) -> list[Part]:
141
+ parts: list[Part] = []
142
+ visited: set[int] = set()
143
+
144
+ def walk(source: OpcPackage | Part) -> None:
145
+ for rel in source.rels.values():
146
+ if rel.is_external:
147
+ continue
148
+ part = rel.target_part
149
+ identity = id(part)
150
+ if identity in visited:
151
+ continue
152
+ visited.add(identity)
153
+ parts.append(part)
154
+ walk(part)
155
+
156
+ walk(package)
157
+ return parts
158
+
159
+
160
+ def _reachable_part_names(package: OpcPackage) -> set[str]:
161
+ return {str(part.partname) for part in _reachable_parts(package)}
162
+
163
+
164
+ def _relationship_name(relationship_type: str) -> str:
165
+ return relationship_type.rstrip("/").rsplit("/", 1)[-1].lower()
166
+
167
+
168
+ def _relationship_sources(package: OpcPackage) -> list[tuple[str, OpcPackage | Part]]:
169
+ return [("/", package), *[(str(part.partname), part) for part in _reachable_parts(package)]]
170
+
171
+
172
+ def _remove_elements(root: BaseOxmlElement, tags: tuple[str, ...]) -> int:
173
+ tag_set = set(tags)
174
+ elements = [
175
+ element
176
+ for tag in tags
177
+ for element in root.iter(tag)
178
+ if not any(ancestor.tag in tag_set for ancestor in element.iterancestors())
179
+ ]
180
+ for element in elements:
181
+ parent = element.getparent()
182
+ if parent is not None:
183
+ comment_run = _empty_comment_reference_run(element)
184
+ parent.remove(element)
185
+ if comment_run is not None:
186
+ comment_run_parent = comment_run.getparent()
187
+ if comment_run_parent is not None:
188
+ comment_run_parent.remove(comment_run)
189
+ return len(elements)
190
+
191
+
192
+ def _scrub_personal_metadata(package: OpcPackage) -> list[str]:
193
+ core_properties = package.core_properties
194
+ changed = core_properties.deduplicate("author", "last_modified_by", "revision")
195
+ if core_properties.author:
196
+ changed.add("author")
197
+ if core_properties.last_modified_by:
198
+ changed.add("last_modified_by")
199
+ if core_properties.revision != 1:
200
+ changed.add("revision")
201
+ core_properties.author = ""
202
+ core_properties.last_modified_by = ""
203
+ core_properties.revision = 1
204
+ return [name for name in ("author", "last_modified_by", "revision") if name in changed]
205
+
206
+
207
+ def _empty_comment_reference_run(element: etree_Element) -> etree_Element | None:
208
+ if element.tag != qn("w:commentReference"):
209
+ return None
210
+ run = element.getparent()
211
+ if run is None or run.tag != qn("w:r"):
212
+ return None
213
+ remaining = [child for child in run if child is not element]
214
+ if not remaining:
215
+ return run
216
+ if len(remaining) != 1 or remaining[0].tag != qn("w:rPr"):
217
+ return None
218
+ run_properties = remaining[0]
219
+ if len(run_properties) != 1 or run_properties[0].tag != qn("w:rStyle"):
220
+ return None
221
+ return run if run_properties[0].get(qn("w:val")) == "CommentReference" else None
@@ -0,0 +1,38 @@
1
+ """Value objects returned by document sanitization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class RemovedRelationship:
10
+ """A package relationship removed during sanitization."""
11
+
12
+ source: str
13
+ relationship_id: str
14
+ relationship_type: str
15
+ target: str
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SanitizationReport:
20
+ """Immutable summary of changes made by ``Document.sanitize()``."""
21
+
22
+ scrubbed_properties: tuple[str, ...] = ()
23
+ removed_comment_markers: int = 0
24
+ removed_formatting_revisions: int = 0
25
+ removed_parts: tuple[str, ...] = ()
26
+ removed_relationships: tuple[RemovedRelationship, ...] = ()
27
+ warnings: tuple[str, ...] = ()
28
+
29
+ @property
30
+ def changed(self) -> bool:
31
+ """Whether the document package was modified."""
32
+ return bool(
33
+ self.scrubbed_properties
34
+ or self.removed_comment_markers
35
+ or self.removed_formatting_revisions
36
+ or self.removed_parts
37
+ or self.removed_relationships
38
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-docx-oss
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Create, read, and update Microsoft Word .docx files.
5
5
  Author: Ethan St Lee
6
6
  License-Expression: MIT
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Libraries
23
23
  Requires-Python: >=3.12
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: lxml>=3.1.0
26
+ Requires-Dist: lxml>=6.1.0
27
27
  Requires-Dist: typing_extensions>=4.9.0
28
28
  Dynamic: license-file
29
29
 
@@ -65,7 +65,8 @@ or low-level package and relationship access.
65
65
  | Latin and East Asian font controls | Stable | `run.font.name`, `run.font.hAnsi`, `run.font.eastAsia` |
66
66
  | Low-level OPC parts and relationships | Stable | `document.part.package`, `part.rels` |
67
67
  | In-memory read, export, and independent copies | Stable | `Document()`, `to_bytes()`, `copy()` |
68
- | Package inspection, diff, and sanitization | Planned | — |
68
+ | Document sanitization | Development | `document.sanitize()` |
69
+ | Package inspection and diff | Planned | — |
69
70
 
70
71
  ## Installation
71
72
 
@@ -1,8 +1,8 @@
1
- docx/__init__.py,sha256=aw9boQn8MrzqJFVCGmYy41YRdhhw3Ym0bIhqE643gCA,2102
1
+ docx/__init__.py,sha256=sDaLx2PbYx5dXsjgnP0rhCKyeZZrysYhseDkNNC3Q2Y,2102
2
2
  docx/api.py,sha256=jIojXW4IRDEXQqaqvmOZbzBvNeyRXv9ntHByxd-Kb0M,1518
3
3
  docx/blkcntnr.py,sha256=_xe7Uz3RBCz4TPKXpIzRxtGg-1YPb3gAG9YvsSUyoLU,3525
4
4
  docx/comments.py,sha256=MmaP7U9mZOUFsRhaZNIbXatqOOA8-pOC6UlqWAuTHsc,6385
5
- docx/document.py,sha256=s9oolRg9W37W_8RG8v-rHVZtU9iBevV-NgnJ9JW2z00,12376
5
+ docx/document.py,sha256=IYTU_lmh_f1SILGGc0XqrDMl814hZ937d8iubfsYFos,13163
6
6
  docx/exceptions.py,sha256=WWiMy3ZtsfKRS31Wv09egLZnpPkX0H66B8JkOPCC3FM,454
7
7
  docx/package.py,sha256=Eob2q7R6jXQ3iPD0cAGHzu2RVYgU2te4zfG3oVN_-lM,4288
8
8
  docx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -37,13 +37,13 @@ docx/image/svg.py,sha256=qDtoQerJ7lJscevXM65BlWqmtUKVB05jTnQajjwLpFw,3576
37
37
  docx/image/tiff.py,sha256=LZx-P2uMSzgdzHfIWRqRqtnbhuIqWBY_cLkx9_HTrPs,10379
38
38
  docx/opc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  docx/opc/constants.py,sha256=u7JE0q95EFiAp3jXOntjv1cV0GdGpKtLfx85D2mlch0,17124
40
- docx/opc/coreprops.py,sha256=IfQeqC5o4k9Obj_kQ4ZKfFl6IL7eIm7dcIC1AZkVvac,3468
40
+ docx/opc/coreprops.py,sha256=DUE7xnk0syV2EtwHtJimX5V5efDccsqoB2KW_mmuHEA,4937
41
41
  docx/opc/customprops.py,sha256=oCHxWPNWSekBQrtO07WPSb0WKLQgTgfS3y8m8ZUXN-A,1986
42
42
  docx/opc/exceptions.py,sha256=pqa1GuFxMlGZDBgGBqntZ_8aDINLwo2gtPuNRwADzyM,264
43
43
  docx/opc/oxml.py,sha256=IE3H4F-DWr8QSZi0pxc767apU3Nk-BhFTs7HiTD5ck0,8325
44
44
  docx/opc/package.py,sha256=zWteqHOyPbR4mpcoOZy-sPVWvQR2e2fQVoYdjaHXXfI,10217
45
45
  docx/opc/packuri.py,sha256=1-uRAwjIftl475nozvssEoS-0EBz0w-oJvT-fNYcx9o,3729
46
- docx/opc/part.py,sha256=15TjdwOs137LDdr-EVNeLb4iQdRyIfDZg6Vjx6_8Zdc,9606
46
+ docx/opc/part.py,sha256=WZsVGi25sZhRobXEFRBQCVX6EXan2pE1GPe_zH5nhxk,9977
47
47
  docx/opc/phys_pkg.py,sha256=-eNZDBbLShHo3W1iglf1CwN_nkK8qnmTWp6ABg77cjA,4005
48
48
  docx/opc/pkgreader.py,sha256=iUPZ7Ba4kNXjPB-q1mQEO44eVmbnP08po4WFH-HD2IE,9588
49
49
  docx/opc/pkgwriter.py,sha256=shDeuBtW2BbFOTHrXx8R97PWFaH_fE37JYBfbw6QWRs,4524
@@ -89,6 +89,9 @@ docx/parts/numbering.py,sha256=rrVIiRoBQIy6j8eWGhWyhcJvNx6RRaItWVDhorwrVyo,1715
89
89
  docx/parts/settings.py,sha256=lqcDzDNLEc8ElxmNMKwy6uNgOesfYcvBDhKz7uUMnb4,1717
90
90
  docx/parts/story.py,sha256=WgsvzLkfZ7DigCE9MVSGPfajzennxdPvOtIdoSxmeuk,4577
91
91
  docx/parts/styles.py,sha256=Ly7aJyPgtwQ2TRIwuUtHz_x1Z0FH9JTtsNohml1-ykM,1391
92
+ docx/sanitize/__init__.py,sha256=woy_ORRETalWdjmOsDAV7F4ON0jmOWBWm5b8KHhp_yw,387
93
+ docx/sanitize/cleaner.py,sha256=BwAKuJ_B36J6TLSnePMzzvgB129T3QitO6lHS5LXsB0,7602
94
+ docx/sanitize/result.py,sha256=JL5IrKnFpG4WQrXcMqNNxFmBHib7J0IXp4D5p2_iI5c,1055
92
95
  docx/styles/__init__.py,sha256=aCJqhH4JhG_cIm3iwVJGtWpWW0AXJx3QMag7MORmrdI,1406
93
96
  docx/styles/latent.py,sha256=EDBcgd-1jKmW-wGz64GvcGSopnOuPQG1inU0pnHRsQA,7211
94
97
  docx/styles/style.py,sha256=BoGXQRSyODmPI38bAEWPgTXEPwhXyVYZZ9nSYxRSHPM,8051
@@ -125,8 +128,8 @@ docx/text/paragraph.py,sha256=N3F62_JRQZYaumuoaJXKCDnRSw2DIiOwzyMPV2Jd7GI,6828
125
128
  docx/text/parfmt.py,sha256=70M75HFo7ydpqmIdZjkV711muyrsBGdr4WZscF3YVJY,10339
126
129
  docx/text/run.py,sha256=tsW8dXOE4VnqEM7cJz_uU2Vq3qgdnrMGzyDq9x5jvE4,10800
127
130
  docx/text/tabstops.py,sha256=bJN98GWW22f4Sqg0dQbyUcnuOn65fHsNZuCGswI03_c,3896
128
- python_docx_oss-0.2.0.dist-info/licenses/LICENSE,sha256=lf2yZv-riPwFeah3IMDeG0zqI6fXcV9T9YOToeG54NM,1164
129
- python_docx_oss-0.2.0.dist-info/METADATA,sha256=xgmccioh4zptF-ExSr52_7IrSCIsMQXeiovBXzT7DRk,7851
130
- python_docx_oss-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
131
- python_docx_oss-0.2.0.dist-info/top_level.txt,sha256=zkbrD3gmyqHpD_XhHe7lCbr2M5vT229nJ4UFqF5VjIs,5
132
- python_docx_oss-0.2.0.dist-info/RECORD,,
131
+ python_docx_oss-0.3.0.dist-info/licenses/LICENSE,sha256=lf2yZv-riPwFeah3IMDeG0zqI6fXcV9T9YOToeG54NM,1164
132
+ python_docx_oss-0.3.0.dist-info/METADATA,sha256=hp-fShMO4V9dZLm86V-xoMsQgyphHVcgtLr8-tS7aa0,7900
133
+ python_docx_oss-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
134
+ python_docx_oss-0.3.0.dist-info/top_level.txt,sha256=zkbrD3gmyqHpD_XhHe7lCbr2M5vT229nJ4UFqF5VjIs,5
135
+ python_docx_oss-0.3.0.dist-info/RECORD,,