athena-python-docx 0.1.8__tar.gz → 0.2.1__tar.gz
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.
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/.gitignore +1 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/PKG-INFO +1 -1
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/__init__.py +1 -1
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/document.py +127 -11
- athena_python_docx-0.2.1/docx/enum/section.py +37 -0
- athena_python_docx-0.2.1/docx/enum/style.py +64 -0
- athena_python_docx-0.2.1/docx/enum/table.py +52 -0
- athena_python_docx-0.2.1/docx/enum/text.py +160 -0
- athena_python_docx-0.2.1/docx/opc/__init__.py +1 -0
- athena_python_docx-0.2.1/docx/opc/coreprops.py +145 -0
- athena_python_docx-0.2.1/docx/section.py +358 -0
- athena_python_docx-0.2.1/docx/settings.py +30 -0
- athena_python_docx-0.2.1/docx/shape.py +141 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/shared.py +43 -8
- athena_python_docx-0.2.1/docx/styles/__init__.py +1 -0
- athena_python_docx-0.2.1/docx/styles/style.py +77 -0
- athena_python_docx-0.2.1/docx/styles/styles.py +151 -0
- athena_python_docx-0.2.1/docx/table.py +921 -0
- athena_python_docx-0.2.1/docx/text/hyperlink.py +53 -0
- athena_python_docx-0.2.1/docx/text/paragraph.py +387 -0
- athena_python_docx-0.2.1/docx/text/parfmt.py +395 -0
- athena_python_docx-0.2.1/docx/text/run.py +577 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/pyproject.toml +1 -1
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/conftest.py +8 -2
- athena_python_docx-0.2.1/tests/fidelity/binary_round_trip.py +281 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/cases.py +22 -0
- athena_python_docx-0.2.1/tests/fidelity/complex_cases.py +1907 -0
- athena_python_docx-0.2.1/tests/fidelity/extreme_cases.py +449 -0
- athena_python_docx-0.2.1/tests/fidelity/fake_session.py +792 -0
- athena_python_docx-0.2.1/tests/fidelity/local_runner.py +164 -0
- athena_python_docx-0.2.1/tests/fidelity/mega_cases.py +633 -0
- athena_python_docx-0.2.1/tests/fidelity/real_world_cases.py +266 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_commands.py +15 -16
- athena_python_docx-0.1.8/docx/enum/table.py +0 -15
- athena_python_docx-0.1.8/docx/enum/text.py +0 -29
- athena_python_docx-0.1.8/docx/table.py +0 -227
- athena_python_docx-0.1.8/docx/text/paragraph.py +0 -179
- athena_python_docx-0.1.8/docx/text/run.py +0 -172
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/CLAUDE.md +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/README.md +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/_batching.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/api.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/client.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/errors.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/typing.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/scripts/publish.sh +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/__init__.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_smoke_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack
|
|
5
5
|
Project-URL: Homepage, https://athenaintelligence.ai
|
|
6
6
|
Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
|
|
@@ -98,8 +98,66 @@ class Document:
|
|
|
98
98
|
for item in items
|
|
99
99
|
]
|
|
100
100
|
|
|
101
|
+
@property
|
|
102
|
+
def sections(self):
|
|
103
|
+
"""Return the document's sections collection."""
|
|
104
|
+
from docx.section import Sections
|
|
105
|
+
|
|
106
|
+
self._ensure_open()
|
|
107
|
+
return Sections(session=self._session)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def inline_shapes(self):
|
|
111
|
+
"""Return the document's inline-shape collection."""
|
|
112
|
+
from docx.shape import InlineShapes
|
|
113
|
+
|
|
114
|
+
self._ensure_open()
|
|
115
|
+
return InlineShapes(session=self._session)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def styles(self):
|
|
119
|
+
"""Return the document's styles collection."""
|
|
120
|
+
from docx.styles.styles import Styles
|
|
121
|
+
|
|
122
|
+
self._ensure_open()
|
|
123
|
+
return Styles(session=self._session)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def core_properties(self):
|
|
127
|
+
"""Return a minimal CoreProperties proxy.
|
|
128
|
+
|
|
129
|
+
Most python-docx fields aren't surfaced by Superdoc; accessing an
|
|
130
|
+
unsupported field returns None rather than raising.
|
|
131
|
+
"""
|
|
132
|
+
from docx.opc.coreprops import CoreProperties
|
|
133
|
+
|
|
134
|
+
return CoreProperties(session=self._session)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def settings(self):
|
|
138
|
+
"""Return a minimal Settings proxy (stubbed; Superdoc doesn't surface app settings)."""
|
|
139
|
+
from docx.settings import Settings
|
|
140
|
+
|
|
141
|
+
return Settings(session=self._session)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def element(self):
|
|
145
|
+
"""python-docx returns the underlying lxml element. We return a best-effort proxy."""
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
part = element
|
|
149
|
+
|
|
101
150
|
# ---- Append operations ----
|
|
102
151
|
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _normalize_text(text: str) -> str:
|
|
154
|
+
"""Normalize line endings like python-docx: \\r\\n → \\n\\n, lone \\r → \\n."""
|
|
155
|
+
# python-docx's text IO strips \r and converts \r to \n.
|
|
156
|
+
if not text:
|
|
157
|
+
return text
|
|
158
|
+
# \r\n → \n\n (preserves paragraph-break semantics)
|
|
159
|
+
return text.replace("\r\n", "\n\n").replace("\r", "\n")
|
|
160
|
+
|
|
103
161
|
def add_paragraph(
|
|
104
162
|
self,
|
|
105
163
|
text: str = "",
|
|
@@ -135,7 +193,7 @@ class Document:
|
|
|
135
193
|
return self.add_heading(text=text, level=0)
|
|
136
194
|
|
|
137
195
|
params: dict = {
|
|
138
|
-
"text": text,
|
|
196
|
+
"text": self._normalize_text(text),
|
|
139
197
|
"at": {"kind": "documentEnd"},
|
|
140
198
|
}
|
|
141
199
|
result: dict = run_sync(
|
|
@@ -162,19 +220,38 @@ class Document:
|
|
|
162
220
|
f"level must be in 0..9; got {level}",
|
|
163
221
|
)
|
|
164
222
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
223
|
+
# python-docx semantics: level=0 is Title, 1..9 are Heading N.
|
|
224
|
+
# Superdoc's doc.create.heading only accepts integer levels 1..6,
|
|
225
|
+
# so Title (level=0) routes through create.paragraph followed by a
|
|
226
|
+
# style change to "Title". Levels 7..9 fall through to create.heading
|
|
227
|
+
# and will raise a SuperDocError if the runtime rejects them — that
|
|
228
|
+
# mirrors python-docx's tolerance without silently degrading.
|
|
229
|
+
if level == 0:
|
|
230
|
+
result: dict = run_sync(
|
|
231
|
+
self._session.doc.create.paragraph(
|
|
232
|
+
{"text": text, "at": {"kind": "documentEnd"}},
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
node_id: str = _extract_inserted_node_id(
|
|
236
|
+
result, expected_type="paragraph",
|
|
237
|
+
)
|
|
238
|
+
if not node_id:
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
f"Superdoc did not return a nodeId for add_heading(level=0): {result!r}",
|
|
241
|
+
)
|
|
242
|
+
paragraph = Paragraph(session=self._session, node_id=node_id)
|
|
243
|
+
paragraph.style = "Title"
|
|
244
|
+
return paragraph
|
|
245
|
+
|
|
169
246
|
params: dict = {
|
|
170
247
|
"text": text,
|
|
171
|
-
"level": level
|
|
248
|
+
"level": level,
|
|
172
249
|
"at": {"kind": "documentEnd"},
|
|
173
250
|
}
|
|
174
|
-
result
|
|
251
|
+
result = run_sync(
|
|
175
252
|
self._session.doc.create.heading(params),
|
|
176
253
|
)
|
|
177
|
-
node_id
|
|
254
|
+
node_id = _extract_inserted_node_id(result, expected_type="paragraph")
|
|
178
255
|
if not node_id:
|
|
179
256
|
raise RuntimeError(
|
|
180
257
|
f"Superdoc did not return a nodeId for add_heading: {result!r}",
|
|
@@ -289,14 +366,53 @@ class Document:
|
|
|
289
366
|
),
|
|
290
367
|
)
|
|
291
368
|
|
|
369
|
+
def add_section(self, start_type: object = None):
|
|
370
|
+
"""Append a new section and return its Section proxy.
|
|
371
|
+
|
|
372
|
+
Mirrors python-docx: creating a section adds a trailing empty
|
|
373
|
+
paragraph that marks the section boundary.
|
|
374
|
+
"""
|
|
375
|
+
from docx.enum.section import WD_SECTION_START
|
|
376
|
+
from docx.section import Section
|
|
377
|
+
|
|
378
|
+
self._ensure_open()
|
|
379
|
+
# python-docx always inserts an anchor paragraph at the section break
|
|
380
|
+
self.add_paragraph("")
|
|
381
|
+
break_type: str = "nextPage"
|
|
382
|
+
if start_type is not None:
|
|
383
|
+
if isinstance(start_type, WD_SECTION_START):
|
|
384
|
+
break_type = start_type.to_superdoc()
|
|
385
|
+
elif isinstance(start_type, str):
|
|
386
|
+
break_type = start_type
|
|
387
|
+
run_sync(
|
|
388
|
+
self._session.doc.create.section_break(
|
|
389
|
+
{"at": {"kind": "documentEnd"}, "breakType": break_type},
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
info: object = run_sync(self._session.doc.sections.list({}))
|
|
393
|
+
items: list = []
|
|
394
|
+
if isinstance(info, dict):
|
|
395
|
+
items_obj = info.get("items", [])
|
|
396
|
+
if isinstance(items_obj, list):
|
|
397
|
+
items = items_obj
|
|
398
|
+
if not items:
|
|
399
|
+
raise RuntimeError(
|
|
400
|
+
"Superdoc did not return any sections after add_section",
|
|
401
|
+
)
|
|
402
|
+
last = items[-1]
|
|
403
|
+
addr = last.get("address", {}) if isinstance(last, dict) else {}
|
|
404
|
+
return Section(session=self._session, address=addr if isinstance(addr, dict) else {})
|
|
405
|
+
|
|
292
406
|
def add_page_break(self) -> None:
|
|
293
407
|
"""Append a page break at the end of the document.
|
|
294
408
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
409
|
+
python-docx appends an empty paragraph with a page-break run inside;
|
|
410
|
+
`doc.paragraphs` includes that new paragraph. We mirror by adding
|
|
411
|
+
an anchor paragraph first, then the section break.
|
|
298
412
|
"""
|
|
299
413
|
self._ensure_open()
|
|
414
|
+
# Anchor paragraph so doc.paragraphs reflects the break visually.
|
|
415
|
+
self.add_paragraph("")
|
|
300
416
|
run_sync(
|
|
301
417
|
self._session.doc.create.section_break(
|
|
302
418
|
{"at": {"kind": "documentEnd"}, "breakType": "nextPage"},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Section-related enums — python-docx parity (docx.enum.section)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WD_ORIENTATION(Enum):
|
|
9
|
+
PORTRAIT = "portrait"
|
|
10
|
+
LANDSCAPE = "landscape"
|
|
11
|
+
|
|
12
|
+
def to_superdoc(self) -> str:
|
|
13
|
+
return self.value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# python-docx also exposes a short alias
|
|
17
|
+
WD_ORIENT = WD_ORIENTATION
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WD_SECTION_START(Enum):
|
|
21
|
+
CONTINUOUS = "continuous"
|
|
22
|
+
NEW_COLUMN = "nextColumn"
|
|
23
|
+
NEW_PAGE = "nextPage"
|
|
24
|
+
EVEN_PAGE = "evenPage"
|
|
25
|
+
ODD_PAGE = "oddPage"
|
|
26
|
+
|
|
27
|
+
def to_superdoc(self) -> str:
|
|
28
|
+
if self == WD_SECTION_START.CONTINUOUS:
|
|
29
|
+
return "continuous"
|
|
30
|
+
if self == WD_SECTION_START.EVEN_PAGE:
|
|
31
|
+
return "evenPage"
|
|
32
|
+
if self == WD_SECTION_START.ODD_PAGE:
|
|
33
|
+
return "oddPage"
|
|
34
|
+
return "nextPage"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
WD_SECTION = WD_SECTION_START
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Style-related enums — python-docx parity (docx.enum.style)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WD_STYLE_TYPE(Enum):
|
|
9
|
+
PARAGRAPH = "paragraph"
|
|
10
|
+
CHARACTER = "character"
|
|
11
|
+
LIST = "list"
|
|
12
|
+
TABLE = "table"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
WD_STYLE = WD_STYLE_TYPE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WD_BUILTIN_STYLE(Enum):
|
|
19
|
+
"""Common built-in Word style names. Names match python-docx exactly."""
|
|
20
|
+
|
|
21
|
+
BLOCK_QUOTATION = "Quote"
|
|
22
|
+
BODY_TEXT = "Body Text"
|
|
23
|
+
BODY_TEXT_2 = "Body Text 2"
|
|
24
|
+
BODY_TEXT_3 = "Body Text 3"
|
|
25
|
+
BODY_TEXT_FIRST_INDENT = "Body Text First Indent"
|
|
26
|
+
BODY_TEXT_FIRST_INDENT_2 = "Body Text First Indent 2"
|
|
27
|
+
BODY_TEXT_IND = "Body Text Indent"
|
|
28
|
+
BODY_TEXT_IND_2 = "Body Text Indent 2"
|
|
29
|
+
BODY_TEXT_IND_3 = "Body Text Indent 3"
|
|
30
|
+
CAPTION = "Caption"
|
|
31
|
+
DEFAULT_PARAGRAPH_FONT = "Default Paragraph Font"
|
|
32
|
+
EMPHASIS = "Emphasis"
|
|
33
|
+
HEADING_1 = "Heading 1"
|
|
34
|
+
HEADING_2 = "Heading 2"
|
|
35
|
+
HEADING_3 = "Heading 3"
|
|
36
|
+
HEADING_4 = "Heading 4"
|
|
37
|
+
HEADING_5 = "Heading 5"
|
|
38
|
+
HEADING_6 = "Heading 6"
|
|
39
|
+
HEADING_7 = "Heading 7"
|
|
40
|
+
HEADING_8 = "Heading 8"
|
|
41
|
+
HEADING_9 = "Heading 9"
|
|
42
|
+
INTENSE_EMPHASIS = "Intense Emphasis"
|
|
43
|
+
INTENSE_QUOTE = "Intense Quote"
|
|
44
|
+
INTENSE_REFERENCE = "Intense Reference"
|
|
45
|
+
LIST_BULLET = "List Bullet"
|
|
46
|
+
LIST_BULLET_2 = "List Bullet 2"
|
|
47
|
+
LIST_BULLET_3 = "List Bullet 3"
|
|
48
|
+
LIST_NUMBER = "List Number"
|
|
49
|
+
LIST_NUMBER_2 = "List Number 2"
|
|
50
|
+
LIST_NUMBER_3 = "List Number 3"
|
|
51
|
+
MACRO_TEXT = "Macro Text"
|
|
52
|
+
NAV_PANE = "Nav Pane"
|
|
53
|
+
NORMAL = "Normal"
|
|
54
|
+
NORMAL_INDENT = "Normal Indent"
|
|
55
|
+
NORMAL_TABLE = "Normal Table"
|
|
56
|
+
NO_SPACING = "No Spacing"
|
|
57
|
+
QUOTE = "Quote"
|
|
58
|
+
SUBTITLE = "Subtitle"
|
|
59
|
+
STRONG = "Strong"
|
|
60
|
+
TABLE_GRID = "Table Grid"
|
|
61
|
+
TITLE = "Title"
|
|
62
|
+
TOC_1 = "TOC 1"
|
|
63
|
+
TOC_2 = "TOC 2"
|
|
64
|
+
TOC_3 = "TOC 3"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Table-related enums — python-docx parity (docx.enum.table)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WD_ROW_HEIGHT_RULE(Enum):
|
|
9
|
+
AUTO = "auto"
|
|
10
|
+
AT_LEAST = "atLeast"
|
|
11
|
+
EXACTLY = "exact"
|
|
12
|
+
|
|
13
|
+
def to_superdoc(self) -> str:
|
|
14
|
+
return self.value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Short alias used by python-docx
|
|
18
|
+
WD_ROW_HEIGHT = WD_ROW_HEIGHT_RULE
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WD_TABLE_ALIGNMENT(Enum):
|
|
22
|
+
LEFT = "left"
|
|
23
|
+
CENTER = "center"
|
|
24
|
+
RIGHT = "right"
|
|
25
|
+
|
|
26
|
+
def to_superdoc(self) -> str:
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WD_TABLE_DIRECTION(Enum):
|
|
31
|
+
LTR = "ltr"
|
|
32
|
+
RTL = "rtl"
|
|
33
|
+
|
|
34
|
+
def to_superdoc(self) -> str:
|
|
35
|
+
return self.value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WD_ALIGN_VERTICAL(Enum):
|
|
39
|
+
TOP = "top"
|
|
40
|
+
CENTER = "center"
|
|
41
|
+
BOTTOM = "bottom"
|
|
42
|
+
BOTH = "both"
|
|
43
|
+
|
|
44
|
+
def to_superdoc(self) -> str:
|
|
45
|
+
# Superdoc enum: top | center | bottom
|
|
46
|
+
if self == WD_ALIGN_VERTICAL.BOTH:
|
|
47
|
+
return "center"
|
|
48
|
+
return self.value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# python-docx exposes WD_CELL_VERTICAL_ALIGNMENT as an alias for cells
|
|
52
|
+
WD_CELL_VERTICAL_ALIGNMENT = WD_ALIGN_VERTICAL
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Text-related enums — python-docx parity (docx.enum.text).
|
|
2
|
+
|
|
3
|
+
Values map to Superdoc primitives where supported; unmapped values
|
|
4
|
+
(e.g. Thai/Arabic justification variants) still parse but serialize to
|
|
5
|
+
their best Superdoc equivalent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WD_ALIGN_PARAGRAPH(Enum):
|
|
14
|
+
"""Paragraph alignment (justification)."""
|
|
15
|
+
|
|
16
|
+
LEFT = "left"
|
|
17
|
+
CENTER = "center"
|
|
18
|
+
RIGHT = "right"
|
|
19
|
+
JUSTIFY = "justify"
|
|
20
|
+
DISTRIBUTE = "distribute"
|
|
21
|
+
JUSTIFY_MED = "justifyMed"
|
|
22
|
+
JUSTIFY_HI = "justifyHi"
|
|
23
|
+
JUSTIFY_LOW = "justifyLow"
|
|
24
|
+
THAI_JUSTIFY = "thaiJustify"
|
|
25
|
+
|
|
26
|
+
def to_superdoc(self) -> str:
|
|
27
|
+
# Superdoc only supports left/center/right/justify; coerce the
|
|
28
|
+
# distributed/thai variants to "justify" so set_alignment doesn't
|
|
29
|
+
# reject them.
|
|
30
|
+
if self in (
|
|
31
|
+
WD_ALIGN_PARAGRAPH.DISTRIBUTE,
|
|
32
|
+
WD_ALIGN_PARAGRAPH.JUSTIFY_MED,
|
|
33
|
+
WD_ALIGN_PARAGRAPH.JUSTIFY_HI,
|
|
34
|
+
WD_ALIGN_PARAGRAPH.JUSTIFY_LOW,
|
|
35
|
+
WD_ALIGN_PARAGRAPH.THAI_JUSTIFY,
|
|
36
|
+
):
|
|
37
|
+
return "justify"
|
|
38
|
+
return self.value
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_superdoc(cls, s: str) -> "WD_ALIGN_PARAGRAPH | None":
|
|
42
|
+
try:
|
|
43
|
+
return cls(s)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
try:
|
|
47
|
+
return cls(s.lower())
|
|
48
|
+
except ValueError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class WD_LINE_SPACING(Enum):
|
|
53
|
+
"""Line-spacing rule."""
|
|
54
|
+
|
|
55
|
+
SINGLE = "single"
|
|
56
|
+
ONE_POINT_FIVE = "onePointFive"
|
|
57
|
+
DOUBLE = "double"
|
|
58
|
+
AT_LEAST = "atLeast"
|
|
59
|
+
EXACTLY = "exactly"
|
|
60
|
+
MULTIPLE = "multiple"
|
|
61
|
+
|
|
62
|
+
def to_superdoc(self) -> str:
|
|
63
|
+
# Superdoc's lineRule enum: "auto" | "exact" | "atLeast"
|
|
64
|
+
if self == WD_LINE_SPACING.EXACTLY:
|
|
65
|
+
return "exact"
|
|
66
|
+
if self == WD_LINE_SPACING.AT_LEAST:
|
|
67
|
+
return "atLeast"
|
|
68
|
+
# SINGLE/1.5/DOUBLE/MULTIPLE all map to "auto" with a line value
|
|
69
|
+
return "auto"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WD_TAB_ALIGNMENT(Enum):
|
|
73
|
+
LEFT = "left"
|
|
74
|
+
CENTER = "center"
|
|
75
|
+
RIGHT = "right"
|
|
76
|
+
DECIMAL = "decimal"
|
|
77
|
+
BAR = "bar"
|
|
78
|
+
LIST = "list"
|
|
79
|
+
CLEAR = "clear"
|
|
80
|
+
END = "end"
|
|
81
|
+
NUM = "num"
|
|
82
|
+
START = "start"
|
|
83
|
+
|
|
84
|
+
def to_superdoc(self) -> str:
|
|
85
|
+
if self in (
|
|
86
|
+
WD_TAB_ALIGNMENT.LIST,
|
|
87
|
+
WD_TAB_ALIGNMENT.CLEAR,
|
|
88
|
+
WD_TAB_ALIGNMENT.END,
|
|
89
|
+
WD_TAB_ALIGNMENT.NUM,
|
|
90
|
+
WD_TAB_ALIGNMENT.START,
|
|
91
|
+
):
|
|
92
|
+
return "left"
|
|
93
|
+
return self.value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class WD_TAB_LEADER(Enum):
|
|
97
|
+
SPACES = "none"
|
|
98
|
+
DOTS = "dot"
|
|
99
|
+
DASHES = "hyphen"
|
|
100
|
+
LINES = "underscore"
|
|
101
|
+
HEAVY = "heavy"
|
|
102
|
+
MIDDLE_DOT = "middleDot"
|
|
103
|
+
|
|
104
|
+
def to_superdoc(self) -> str:
|
|
105
|
+
return self.value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class WD_BREAK(Enum):
|
|
109
|
+
LINE = "line"
|
|
110
|
+
PAGE = "page"
|
|
111
|
+
COLUMN = "column"
|
|
112
|
+
LINE_CLEAR_LEFT = "lineClearLeft"
|
|
113
|
+
LINE_CLEAR_RIGHT = "lineClearRight"
|
|
114
|
+
LINE_CLEAR_ALL = "lineClearAll"
|
|
115
|
+
TEXT_WRAPPING = "textWrapping"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class WD_UNDERLINE(Enum):
|
|
119
|
+
NONE = "none"
|
|
120
|
+
SINGLE = "single"
|
|
121
|
+
WORDS = "words"
|
|
122
|
+
DOUBLE = "double"
|
|
123
|
+
DOTTED = "dotted"
|
|
124
|
+
THICK = "thick"
|
|
125
|
+
DASH = "dash"
|
|
126
|
+
DOT_DASH = "dotDash"
|
|
127
|
+
DOT_DOT_DASH = "dotDotDash"
|
|
128
|
+
WAVY = "wavy"
|
|
129
|
+
DOTTED_HEAVY = "dottedHeavy"
|
|
130
|
+
DASH_HEAVY = "dashHeavy"
|
|
131
|
+
DOT_DASH_HEAVY = "dotDashHeavy"
|
|
132
|
+
DOT_DOT_DASH_HEAVY = "dotDotDashHeavy"
|
|
133
|
+
WAVY_HEAVY = "wavyHeavy"
|
|
134
|
+
DASH_LONG = "dashLong"
|
|
135
|
+
DASH_LONG_HEAVY = "dashLongHeavy"
|
|
136
|
+
WAVY_DOUBLE = "wavyDouble"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class WD_COLOR_INDEX(Enum):
|
|
140
|
+
AUTO = "default"
|
|
141
|
+
BLACK = "black"
|
|
142
|
+
BLUE = "blue"
|
|
143
|
+
BRIGHT_GREEN = "green"
|
|
144
|
+
DARK_BLUE = "darkBlue"
|
|
145
|
+
DARK_RED = "darkRed"
|
|
146
|
+
DARK_YELLOW = "darkYellow"
|
|
147
|
+
GRAY_25 = "lightGray"
|
|
148
|
+
GRAY_50 = "darkGray"
|
|
149
|
+
GREEN = "darkGreen"
|
|
150
|
+
PINK = "magenta"
|
|
151
|
+
RED = "red"
|
|
152
|
+
TEAL = "darkCyan"
|
|
153
|
+
TURQUOISE = "cyan"
|
|
154
|
+
VIOLET = "darkMagenta"
|
|
155
|
+
WHITE = "white"
|
|
156
|
+
YELLOW = "yellow"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Alias used by python-docx as well
|
|
160
|
+
WD_COLOR = WD_COLOR_INDEX
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""docx.opc — Open Packaging Convention bindings (stubbed)."""
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""CoreProperties — python-docx docx.opc.coreprops.CoreProperties parity.
|
|
2
|
+
|
|
3
|
+
Superdoc doesn't surface most core document properties (author, title,
|
|
4
|
+
modified, etc.). We expose the attributes with defaults so code that
|
|
5
|
+
reads them doesn't raise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from docx.client import Session
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CoreProperties:
|
|
18
|
+
def __init__(self, *, session: "Session") -> None:
|
|
19
|
+
self._session: "Session" = session
|
|
20
|
+
self._cache: dict[str, object] = {}
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def author(self) -> str:
|
|
24
|
+
return str(self._cache.get("author", ""))
|
|
25
|
+
|
|
26
|
+
@author.setter
|
|
27
|
+
def author(self, value: str) -> None:
|
|
28
|
+
self._cache["author"] = value
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def category(self) -> str:
|
|
32
|
+
return str(self._cache.get("category", ""))
|
|
33
|
+
|
|
34
|
+
@category.setter
|
|
35
|
+
def category(self, value: str) -> None:
|
|
36
|
+
self._cache["category"] = value
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def comments(self) -> str:
|
|
40
|
+
return str(self._cache.get("comments", ""))
|
|
41
|
+
|
|
42
|
+
@comments.setter
|
|
43
|
+
def comments(self, value: str) -> None:
|
|
44
|
+
self._cache["comments"] = value
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def content_status(self) -> str:
|
|
48
|
+
return str(self._cache.get("content_status", ""))
|
|
49
|
+
|
|
50
|
+
@content_status.setter
|
|
51
|
+
def content_status(self, value: str) -> None:
|
|
52
|
+
self._cache["content_status"] = value
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def created(self) -> datetime | None:
|
|
56
|
+
v = self._cache.get("created")
|
|
57
|
+
return v if isinstance(v, datetime) else None
|
|
58
|
+
|
|
59
|
+
@created.setter
|
|
60
|
+
def created(self, value: datetime | None) -> None:
|
|
61
|
+
self._cache["created"] = value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def identifier(self) -> str:
|
|
65
|
+
return str(self._cache.get("identifier", ""))
|
|
66
|
+
|
|
67
|
+
@identifier.setter
|
|
68
|
+
def identifier(self, value: str) -> None:
|
|
69
|
+
self._cache["identifier"] = value
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def keywords(self) -> str:
|
|
73
|
+
return str(self._cache.get("keywords", ""))
|
|
74
|
+
|
|
75
|
+
@keywords.setter
|
|
76
|
+
def keywords(self, value: str) -> None:
|
|
77
|
+
self._cache["keywords"] = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def language(self) -> str:
|
|
81
|
+
return str(self._cache.get("language", ""))
|
|
82
|
+
|
|
83
|
+
@language.setter
|
|
84
|
+
def language(self, value: str) -> None:
|
|
85
|
+
self._cache["language"] = value
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def last_modified_by(self) -> str:
|
|
89
|
+
# Intentionally returns empty — attribution is Keryx-owned.
|
|
90
|
+
return str(self._cache.get("last_modified_by", ""))
|
|
91
|
+
|
|
92
|
+
@last_modified_by.setter
|
|
93
|
+
def last_modified_by(self, value: str) -> None:
|
|
94
|
+
self._cache["last_modified_by"] = value
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def last_printed(self) -> datetime | None:
|
|
98
|
+
v = self._cache.get("last_printed")
|
|
99
|
+
return v if isinstance(v, datetime) else None
|
|
100
|
+
|
|
101
|
+
@last_printed.setter
|
|
102
|
+
def last_printed(self, value: datetime | None) -> None:
|
|
103
|
+
self._cache["last_printed"] = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def modified(self) -> datetime | None:
|
|
107
|
+
v = self._cache.get("modified")
|
|
108
|
+
return v if isinstance(v, datetime) else None
|
|
109
|
+
|
|
110
|
+
@modified.setter
|
|
111
|
+
def modified(self, value: datetime | None) -> None:
|
|
112
|
+
self._cache["modified"] = value
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def revision(self) -> int:
|
|
116
|
+
v = self._cache.get("revision", 0)
|
|
117
|
+
return int(v) if isinstance(v, (int, float, str)) else 0
|
|
118
|
+
|
|
119
|
+
@revision.setter
|
|
120
|
+
def revision(self, value: int) -> None:
|
|
121
|
+
self._cache["revision"] = value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def subject(self) -> str:
|
|
125
|
+
return str(self._cache.get("subject", ""))
|
|
126
|
+
|
|
127
|
+
@subject.setter
|
|
128
|
+
def subject(self, value: str) -> None:
|
|
129
|
+
self._cache["subject"] = value
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def title(self) -> str:
|
|
133
|
+
return str(self._cache.get("title", ""))
|
|
134
|
+
|
|
135
|
+
@title.setter
|
|
136
|
+
def title(self, value: str) -> None:
|
|
137
|
+
self._cache["title"] = value
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def version(self) -> str:
|
|
141
|
+
return str(self._cache.get("version", ""))
|
|
142
|
+
|
|
143
|
+
@version.setter
|
|
144
|
+
def version(self, value: str) -> None:
|
|
145
|
+
self._cache["version"] = value
|