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.
Files changed (55) hide show
  1. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/.gitignore +1 -0
  2. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/PKG-INFO +1 -1
  3. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/__init__.py +1 -1
  4. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/document.py +127 -11
  5. athena_python_docx-0.2.1/docx/enum/section.py +37 -0
  6. athena_python_docx-0.2.1/docx/enum/style.py +64 -0
  7. athena_python_docx-0.2.1/docx/enum/table.py +52 -0
  8. athena_python_docx-0.2.1/docx/enum/text.py +160 -0
  9. athena_python_docx-0.2.1/docx/opc/__init__.py +1 -0
  10. athena_python_docx-0.2.1/docx/opc/coreprops.py +145 -0
  11. athena_python_docx-0.2.1/docx/section.py +358 -0
  12. athena_python_docx-0.2.1/docx/settings.py +30 -0
  13. athena_python_docx-0.2.1/docx/shape.py +141 -0
  14. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/shared.py +43 -8
  15. athena_python_docx-0.2.1/docx/styles/__init__.py +1 -0
  16. athena_python_docx-0.2.1/docx/styles/style.py +77 -0
  17. athena_python_docx-0.2.1/docx/styles/styles.py +151 -0
  18. athena_python_docx-0.2.1/docx/table.py +921 -0
  19. athena_python_docx-0.2.1/docx/text/hyperlink.py +53 -0
  20. athena_python_docx-0.2.1/docx/text/paragraph.py +387 -0
  21. athena_python_docx-0.2.1/docx/text/parfmt.py +395 -0
  22. athena_python_docx-0.2.1/docx/text/run.py +577 -0
  23. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/pyproject.toml +1 -1
  24. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/conftest.py +8 -2
  25. athena_python_docx-0.2.1/tests/fidelity/binary_round_trip.py +281 -0
  26. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/cases.py +22 -0
  27. athena_python_docx-0.2.1/tests/fidelity/complex_cases.py +1907 -0
  28. athena_python_docx-0.2.1/tests/fidelity/extreme_cases.py +449 -0
  29. athena_python_docx-0.2.1/tests/fidelity/fake_session.py +792 -0
  30. athena_python_docx-0.2.1/tests/fidelity/local_runner.py +164 -0
  31. athena_python_docx-0.2.1/tests/fidelity/mega_cases.py +633 -0
  32. athena_python_docx-0.2.1/tests/fidelity/real_world_cases.py +266 -0
  33. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_commands.py +15 -16
  34. athena_python_docx-0.1.8/docx/enum/table.py +0 -15
  35. athena_python_docx-0.1.8/docx/enum/text.py +0 -29
  36. athena_python_docx-0.1.8/docx/table.py +0 -227
  37. athena_python_docx-0.1.8/docx/text/paragraph.py +0 -179
  38. athena_python_docx-0.1.8/docx/text/run.py +0 -172
  39. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/CLAUDE.md +0 -0
  40. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/README.md +0 -0
  41. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/_batching.py +0 -0
  42. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/api.py +0 -0
  43. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/client.py +0 -0
  44. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/enum/__init__.py +0 -0
  45. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/errors.py +0 -0
  46. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/text/__init__.py +0 -0
  47. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/docx/typing.py +0 -0
  48. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/scripts/publish.sh +0 -0
  49. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/__init__.py +0 -0
  50. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/README.md +0 -0
  51. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/__init__.py +0 -0
  52. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/extract.py +0 -0
  53. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/fidelity/runner.py +0 -0
  54. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_python_docx_api_parity.py +0 -0
  55. {athena_python_docx-0.1.8 → athena_python_docx-0.2.1}/tests/test_smoke_integration.py +0 -0
@@ -9,6 +9,7 @@ __pycache__/
9
9
  *.py[cod]
10
10
  *$py.class
11
11
  .venv/
12
+ .venv-*/
12
13
  venv/
13
14
  env/
14
15
  *.egg-info/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.1.8
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>
@@ -6,7 +6,7 @@ See CLAUDE.md for the API parity contract.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "0.1.8"
9
+ __version__ = "0.2.1"
10
10
 
11
11
  from docx.api import Document
12
12
 
@@ -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
- # Map level 0 Title, 1..9 Heading N. create.heading understands
166
- # level directly (level=0 means Title in python-docx semantics; Superdoc
167
- # accepts levels 1..9 for HeadingN and may interpret 0 as Title depending
168
- # on version we pass level as-is).
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 if level >= 1 else 1,
248
+ "level": level,
172
249
  "at": {"kind": "documentEnd"},
173
250
  }
174
- result: dict = run_sync(
251
+ result = run_sync(
175
252
  self._session.doc.create.heading(params),
176
253
  )
177
- node_id: str = _extract_inserted_node_id(result, expected_type="paragraph")
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
- Uses `doc.create.section_break` with OOXML `breakType="nextPage"`,
296
- which is the canonical hard page break (what python-docx's
297
- ``add_page_break()`` produces).
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